From aa544b98c0e63d4c2acfe18c47fb00ff9f2d8c46 Mon Sep 17 00:00:00 2001 From: Alexandru Mariuti Date: Tue, 30 Jun 2026 15:47:51 +0200 Subject: [PATCH] ci: automate pub.dev publishing in dependency order Add a one-click `release.yaml` (workflow_dispatch) that releases every package whose pubspec version is not yet on pub.dev, in dependency order (solidart -> flutter_solidart -> solidart_hooks/solidart_lint), waiting for each to become available before tagging its dependents, and skipping any version already published. - scripts/release.sh: read pubspec version, skip if already on pub.dev, guard against existing-but-unpublished tags, create GitHub release+tag (notes from CHANGELOG), then poll pub.dev until the version is live. - release.yaml: workflow_dispatch with a dry_run toggle; uses RELEASE_PAT so the pushed tags trigger the existing publish_*.yaml workflows (the default GITHUB_TOKEN would not). - publish_*.yaml: widen tag globs to also match prerelease (-dev.N) and build (+N) tags, e.g. `-v[0-9]+.[0-9]+.[0-9]+*`, so current -dev versions actually trigger publishing. The solidart DevTools-extension build step is untouched; the orchestrator only triggers the existing per-package workflows via tags. --- .../workflows/publish_flutter_solidart.yaml | 3 +- .github/workflows/publish_solidart.yaml | 3 +- .github/workflows/publish_solidart_hooks.yaml | 3 +- .github/workflows/publish_solidart_lint.yaml | 3 +- .github/workflows/release.yaml | 34 +++++ scripts/release.sh | 140 ++++++++++++++++++ 6 files changed, 178 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/release.yaml create mode 100755 scripts/release.sh diff --git a/.github/workflows/publish_flutter_solidart.yaml b/.github/workflows/publish_flutter_solidart.yaml index 167926d2..1213f774 100644 --- a/.github/workflows/publish_flutter_solidart.yaml +++ b/.github/workflows/publish_flutter_solidart.yaml @@ -3,8 +3,7 @@ name: Publish flutter_solidart to pub.dev on: push: tags: - - 'flutter_solidart-v[0-9]+.[0-9]+.[0-9]+' # Matches flutter_solidart-v1.2.3 - - 'flutter_solidart-v[0-9]+.[0-9]+.[0-9]+\+[0-9]+' # Matches flutter_solidart-v1.2.3+1 + - "flutter_solidart-v[0-9]+.[0-9]+.[0-9]+*" # Matches flutter_solidart-v1.2.3, -v1.2.3+1 (build), -v1.2.3-dev.1 (prerelease) # Publish using the reusable workflow from dart-lang. jobs: diff --git a/.github/workflows/publish_solidart.yaml b/.github/workflows/publish_solidart.yaml index de0d7d06..138d475c 100644 --- a/.github/workflows/publish_solidart.yaml +++ b/.github/workflows/publish_solidart.yaml @@ -3,8 +3,7 @@ name: Publish solidart to pub.dev on: push: tags: - - "solidart-v[0-9]+.[0-9]+.[0-9]+" # Matches solidart-v1.2.3 - - 'solidart-v[0-9]+.[0-9]+.[0-9]+\+[0-9]+' # Matches solidart-v1.2.3+1 + - "solidart-v[0-9]+.[0-9]+.[0-9]+*" # Matches solidart-v1.2.3, -v1.2.3+1 (build), -v1.2.3-dev.1 (prerelease) jobs: publish: diff --git a/.github/workflows/publish_solidart_hooks.yaml b/.github/workflows/publish_solidart_hooks.yaml index 686abbdb..bda7cc51 100644 --- a/.github/workflows/publish_solidart_hooks.yaml +++ b/.github/workflows/publish_solidart_hooks.yaml @@ -3,8 +3,7 @@ name: Publish solidart_hooks to pub.dev on: push: tags: - - 'solidart_hooks-v[0-9]+.[0-9]+.[0-9]+' # Matches solidart_hooks-v1.2.3 - - 'solidart_hooks-v[0-9]+.[0-9]+.[0-9]+\+[0-9]+' # Matches solidart_hooks-v1.2.3+1 + - "solidart_hooks-v[0-9]+.[0-9]+.[0-9]+*" # Matches solidart_hooks-v1.2.3, -v1.2.3+1 (build), -v1.2.3-dev.1 (prerelease) # Publish using the reusable workflow from dart-lang. jobs: diff --git a/.github/workflows/publish_solidart_lint.yaml b/.github/workflows/publish_solidart_lint.yaml index 6887f99c..37309844 100644 --- a/.github/workflows/publish_solidart_lint.yaml +++ b/.github/workflows/publish_solidart_lint.yaml @@ -3,8 +3,7 @@ name: Publish solidart_lint to pub.dev on: push: tags: - - 'solidart_lint-v[0-9]+.[0-9]+.[0-9]+' # Matches solidart_lint-v1.2.3 - - 'solidart_lint-v[0-9]+.[0-9]+.[0-9]+\+[0-9]+' # Matches solidart_lint-v1.2.3+1 + - "solidart_lint-v[0-9]+.[0-9]+.[0-9]+*" # Matches solidart_lint-v1.2.3, -v1.2.3+1 (build), -v1.2.3-dev.1 (prerelease) # Publish using the reusable workflow from dart-lang. jobs: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 00000000..7639919b --- /dev/null +++ b/.github/workflows/release.yaml @@ -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 diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100755 index 00000000..bd574b94 --- /dev/null +++ b/scripts/release.sh @@ -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 (`-v`), which triggers +# the package's existing `publish_.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 ($2) is already published for package ($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 +} + +# True when the tag ($1) already exists on the remote. +tag_exists() { + [ -n "$(git ls-remote --tags origin "refs/tags/$1")" ] +} + +# Print the CHANGELOG section for ($2) from /CHANGELOG.md ($1): +# everything between the `## ` 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 ($2) of ($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."