Repository navigation
ci: automate pub.dev publishing in dependency order #176
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| name: Release to pub.dev (orchestrated) | ||
|
|
||
| # One-click release: publishes every package whose pubspec version is not yet on | ||
| # pub.dev, in dependency order, waiting for each to become available before | ||
| # releasing its dependents. Already-published versions are skipped. | ||
| # | ||
| # Requires the `RELEASE_PAT` repo secret (a fine-grained PAT with Contents: write, | ||
| # or a GitHub App token). Tags pushed by the default GITHUB_TOKEN do NOT trigger | ||
| # the publish_*.yaml workflows, so a separate identity is required. | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
| inputs: | ||
| dry_run: | ||
| description: "Log the planned releases without creating tags/releases" | ||
| type: boolean | ||
| default: false | ||
|
|
||
| permissions: | ||
| contents: write | ||
|
|
||
| jobs: | ||
| release: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 0 # full history + tags | ||
|
|
||
| - name: Orchestrate pub.dev releases | ||
| env: | ||
| GH_TOKEN: ${{ secrets.RELEASE_PAT }} | ||
| DRY_RUN: ${{ inputs.dry_run }} | ||
| run: bash scripts/release.sh |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| #!/usr/bin/env bash | ||
| # | ||
| # Orchestrates pub.dev publishing for the solidart monorepo. | ||
| # | ||
| # For every publishable package, in dependency order, this script: | ||
| # 1. reads the `version:` from its pubspec.yaml, | ||
| # 2. skips it if that version is already on pub.dev (no-op for unchanged packages), | ||
| # 3. otherwise creates a GitHub Release + tag (`<name>-v<version>`), which triggers | ||
| # the package's existing `publish_<name>.yaml` workflow, | ||
| # 4. waits until pub.dev actually serves the new version before moving on to the | ||
| # packages that depend on it. | ||
| # | ||
| # It does NOT publish directly and it does NOT bump versions — version bumps and | ||
| # CHANGELOG edits stay manual. The pubspec `version:` is the source of truth. | ||
| # | ||
| # Requires: gh, curl, jq, git (all preinstalled on ubuntu-latest). | ||
| # Environment: | ||
| # GH_TOKEN a non-GITHUB_TOKEN identity (PAT / App token) so the tag push it | ||
| # creates triggers the downstream publish_*.yaml workflows. | ||
| # GITHUB_SHA the commit to tag (auto-set by GitHub Actions). | ||
| # DRY_RUN "true" to log the plan without creating any tags/releases. | ||
| # | ||
| set -euo pipefail | ||
|
|
||
| # Publishable packages in topological order. A package is only released after | ||
| # every in-repo dependency it has is already live on pub.dev. | ||
| # solidart -> (no in-repo deps) | ||
| # flutter_solidart -> solidart | ||
| # solidart_hooks -> flutter_solidart | ||
| # solidart_lint -> solidart, flutter_solidart (dev_dependencies) | ||
| PACKAGES=( | ||
| "solidart:packages/solidart" | ||
| "flutter_solidart:packages/flutter_solidart" | ||
| "solidart_hooks:packages/solidart_hooks" | ||
| "solidart_lint:packages/solidart_lint" | ||
| ) | ||
|
|
||
| DRY_RUN="${DRY_RUN:-false}" | ||
| PUB_API="https://pub.dev/api/packages" | ||
| POLL_ATTEMPTS=60 # 60 * 30s = up to 30 min, to absorb pub.dev propagation lag | ||
| POLL_INTERVAL=30 | ||
|
|
||
| # Read the package version from its pubspec (first `version:` line, value only). | ||
| read_version() { | ||
| awk '/^version:/{print $2; exit}' "$1/pubspec.yaml" | ||
| } | ||
|
|
||
| # True when <version> ($2) is already published for package <name> ($1). | ||
| # A cache-busting query + no-cache header avoid a stale CDN response masking a | ||
| # just-published version. Network/HTTP errors are treated as "not published". | ||
| is_published() { | ||
| local body | ||
| body="$(curl -fsS -H 'Cache-Control: no-cache' "${PUB_API}/$1?_=${RANDOM}" 2>/dev/null || true)" | ||
| [ -n "$body" ] || return 1 | ||
| printf '%s' "$body" | jq -e --arg v "$2" '.versions[]?.version | select(. == $v)' >/dev/null 2>&1 | ||
|
Comment on lines
+48
to
+55
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Fail closed when the pub.dev lookup errors. Treating curl/HTTP failures as “not published” breaks the workflow’s main safety guarantee. A transient pub.dev outage can make this script create a new release/tag for a version that is already live, which then triggers a downstream publish that can only fail. Return a distinct status for lookup failures and abort before 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| # True when the tag <tag> ($1) already exists on the remote. | ||
| tag_exists() { | ||
| [ -n "$(git ls-remote --tags origin "refs/tags/$1")" ] | ||
| } | ||
|
|
||
| # Print the CHANGELOG section for <version> ($2) from <dir>/CHANGELOG.md ($1): | ||
| # everything between the `## <version>` heading and the next `## ` heading. | ||
| # Uses a literal heading compare to avoid regex-escaping the version string. | ||
| changelog_notes() { | ||
| local file="$1/CHANGELOG.md" | ||
| [ -f "$file" ] || return 0 | ||
| awk -v h="## $2" ' | ||
| { line = $0; sub(/[[:space:]]+$/, "", line) } | ||
| line == h { f = 1; next } | ||
| /^## / { if (f) exit } | ||
| f { print } | ||
| ' "$file" | ||
| } | ||
|
|
||
| # Block until <version> ($2) of <name> ($1) is live on pub.dev, or fail. | ||
| wait_for_publish() { | ||
| local name="$1" version="$2" i | ||
| for ((i = 1; i <= POLL_ATTEMPTS; i++)); do | ||
| if is_published "$name" "$version"; then | ||
| return 0 | ||
| fi | ||
| echo " …not on pub.dev yet (attempt ${i}/${POLL_ATTEMPTS}); sleeping ${POLL_INTERVAL}s" | ||
| sleep "$POLL_INTERVAL" | ||
| done | ||
| echo " ✗ timed out after $((POLL_ATTEMPTS * POLL_INTERVAL / 60)) min waiting for ${name} ${version} on pub.dev" >&2 | ||
| return 1 | ||
| } | ||
|
|
||
| [ "$DRY_RUN" = "true" ] && echo "DRY RUN — no tags or releases will be created." | ||
|
|
||
| for entry in "${PACKAGES[@]}"; do | ||
| name="${entry%%:*}" | ||
| dir="${entry#*:}" | ||
| version="$(read_version "$dir")" | ||
|
|
||
| if [ -z "$version" ]; then | ||
| echo "✗ could not read version from ${dir}/pubspec.yaml" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| tag="${name}-v${version}" | ||
| echo "── ${name} ${version} (tag ${tag})" | ||
|
|
||
| if is_published "$name" "$version"; then | ||
| echo " ✓ already on pub.dev — skipping" | ||
| continue | ||
| fi | ||
|
|
||
| if tag_exists "$tag"; then | ||
| echo " ✗ tag ${tag} exists but ${version} is not on pub.dev — a previous publish likely failed." | ||
| echo " Re-run the failed '${name}' publish workflow, or delete the tag and re-run this workflow." | ||
| if [ "$DRY_RUN" = "true" ]; then | ||
| echo " [dry-run] continuing" | ||
| continue | ||
| fi | ||
| exit 1 | ||
| fi | ||
|
|
||
| if [ "$DRY_RUN" = "true" ]; then | ||
| echo " [dry-run] would create release + tag ${tag}, then wait for pub.dev" | ||
| continue | ||
| fi | ||
|
|
||
| notes="$(changelog_notes "$dir" "$version")" | ||
|
|
||
| echo " → creating GitHub release + tag ${tag}" | ||
| release_args=("$tag" --title "$tag" --notes "${notes:-Release $tag}" --target "${GITHUB_SHA}") | ||
| case "$version" in | ||
| *-*) release_args+=(--prerelease) ;; # e.g. 3.0.0-dev.1 | ||
| esac | ||
| gh release create "${release_args[@]}" | ||
|
|
||
| echo " → waiting for ${name} ${version} to appear on pub.dev" | ||
| wait_for_publish "$name" "$version" | ||
| echo " ✓ ${name} ${version} is live on pub.dev" | ||
| done | ||
|
|
||
| echo "Done." | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: nank1ro/solidart
Length of output: 439
🏁 Script executed:
Repository: nank1ro/solidart
Length of output: 7528
🌐 Web query:
GitHub Actions workflow syntax push tags glob pattern square brackets plus literal semver tag matching💡 Result:
GitHub Actions workflow
on.push.tagsfilters support glob patterns, not regular expressions [1]. While glob syntax includes some special characters, it does not support full regex character classes or quantifier-based pattern matching (such as[0-9]+) [1]. Key technical details regarding your query include: 1. Glob vs. Regex: Thetagsfilter uses glob patterns (e.g.,*, ``,?, `!`) [1][4][8]. Attempts to use regex-style syntax like `[0-9]+` will not function as regex; GitHub Actions will interpret these literally or fail to match them against the tag string [1]. 2. Square Brackets in YAML: Square brackets `[` and `]` are special characters in YAML [2]. If your tag pattern starts with `[` or contains them in a way that YAML interprets as a flow sequence, you must enclose the entire pattern in quotes (e.g., `'[pattern]'`) to ensure it is treated as a string literal [2][3]. 3. Literal Matching: If a tag name happens to contain glob characters (like ``, `?`, or `+`) and you want to match that tag literally, you must escape those characters with a backslash `` [4][5][6]. 4. SemVer Matching: Because GitHub Actions does not support regex, there is no native way to perform complex SemVer range matching (e.g., "all tags between 1.0.0 and 2.0.0") directly in the workflow trigger. Common strategies include using broad glob patterns like `v1.` or `v*` and then using a conditional step within the workflow (e.g., `if: ${{ startsWith(github.ref_name, 'v1.') }}`) or a dedicated action to validate the version [7][1]. In summary, you cannot use regex for tag filtering. You should rely on glob patterns for broad inclusion/exclusion and handle specific version logic inside the workflow job itself.Citations:
Fix the tag filter syntax GitHub Actions treats
tags:as globs, not regex, so this pattern does not match the<name>-v<version>tags emitted byscripts/release.sh. Use a valid glob and validate the version in the job if semver constraints are needed. The same pattern is duplicated in the sibling publish workflows.🤖 Prompt for AI Agents