refactor(overlay-topics): extract output classification helpers to cu… #65
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| on: | |
| push: | |
| tags: | |
| - '**/v*' # e.g. packages/sdk/v1.2.3 — single-package release | |
| - 'release/v*' # e.g. release/v2026-05-17 — cascade release (multi-package) | |
| - 'v*' # monorepo-wide tag (publishes everything changed since origin/main) | |
| - '!infra/v*' # infra/v* tags drive infra-release.yaml (Docker images), not npm publish | |
| workflow_dispatch: | |
| inputs: | |
| mode: | |
| description: 'Release mode' | |
| type: choice | |
| options: [cascade, single] | |
| default: cascade | |
| filter: | |
| description: 'pnpm filter (single mode only, e.g. ./packages/sdk)' | |
| required: false | |
| default: '' | |
| jobs: | |
| publish: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| id-token: write # required for npm OIDC provenance | |
| pull-requests: write | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| with: | |
| fetch-depth: 0 | |
| - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6 | |
| - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 | |
| with: | |
| node-version: 24 | |
| registry-url: https://registry.npmjs.org | |
| cache: pnpm | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Build all packages | |
| run: pnpm -r --filter '!@bsv/ts-stack' run build | |
| # Resolve release mode + filter. Three shapes: | |
| # | |
| # 1. Tag `packages/<path>/v*` → single-package publish, filter=./packages/<path> | |
| # 2. Tag `release/v*` → cascade publish across every workspace package | |
| # whose package.json version is ahead of npm. Topo | |
| # order via `pnpm -r publish` (built-in). | |
| # 3. Tag `v*` or manual dispatch → same as cascade, but filter scoped to packages | |
| # changed since origin/main (legacy shape). | |
| - name: Resolve release plan | |
| id: plan | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| DISPATCH_MODE: ${{ inputs.mode }} | |
| DISPATCH_FILTER: ${{ inputs.filter }} | |
| run: | | |
| mode="" | |
| filter="" | |
| if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then | |
| mode="$DISPATCH_MODE" | |
| filter="$DISPATCH_FILTER" | |
| if [[ "$mode" == "single" && -z "$filter" ]]; then | |
| echo "::error::single mode requires a filter input" | |
| exit 1 | |
| fi | |
| else | |
| tag="${GITHUB_REF_NAME}" | |
| if [[ "$tag" == release/v* ]]; then | |
| mode="cascade" | |
| filter="" | |
| elif [[ "$tag" == v* ]]; then | |
| mode="cascade" | |
| filter="...[origin/main]" | |
| else | |
| package_path="${tag%/v*}" | |
| if [[ "$package_path" == "$tag" || ! -f "$package_path/package.json" ]]; then | |
| echo "::error::Could not resolve package path from tag: $tag" | |
| exit 1 | |
| fi | |
| mode="single" | |
| filter="./$package_path" | |
| fi | |
| fi | |
| echo "mode=$mode" >> "$GITHUB_OUTPUT" | |
| echo "filter=$filter" >> "$GITHUB_OUTPUT" | |
| echo "Resolved release plan: mode=$mode filter='${filter:-<all workspace packages>}'" | |
| # Cascade publish: `pnpm -r publish` walks the workspace in topological order | |
| # by default, so an upstream (e.g. @bsv/sdk) is on the registry before downstream | |
| # packages (e.g. @bsv/wallet-toolbox) attempt to install it. `workspace:` protocol | |
| # ranges are rewritten to plain semver by pnpm at publish time — downstream | |
| # packages ship with concrete `^X.Y.Z` deps. | |
| # | |
| # pnpm publish does not accept `--workspace-concurrency`; topo ordering of the | |
| # recursive publish is sequential by design. | |
| # Publish rule: a package is published iff its package.json does NOT set | |
| # `"private": true`. pnpm publish honors this by default — examples and other | |
| # non-shipping workspaces just mark themselves private and pnpm skips them. | |
| # No name-based allow/deny lists in this workflow. | |
| # | |
| # Visibility: list every package about to be published before doing it, so | |
| # release logs make it obvious what landed (and surface a missing-private | |
| # mistake before it 404s halfway through). | |
| - name: List packages selected for publish | |
| env: | |
| PNPM_FILTER: ${{ steps.plan.outputs.filter }} | |
| run: | | |
| if [[ -n "$PNPM_FILTER" ]]; then | |
| pnpm -r --filter="$PNPM_FILTER" ls --depth=-1 --json \ | |
| | node -e 'JSON.parse(require("fs").readFileSync(0,"utf8")).filter(p=>!p.private).forEach(p=>console.log(` publish: ${p.name}@${p.version}`))' | |
| else | |
| pnpm -r ls --depth=-1 --json \ | |
| | node -e 'JSON.parse(require("fs").readFileSync(0,"utf8")).filter(p=>!p.private).forEach(p=>console.log(` publish: ${p.name}@${p.version}`))' | |
| fi | |
| - name: Cascade publish | |
| if: steps.plan.outputs.mode == 'cascade' | |
| env: | |
| PNPM_FILTER: ${{ steps.plan.outputs.filter }} | |
| run: | | |
| if [[ -n "$PNPM_FILTER" ]]; then | |
| pnpm -r --filter="$PNPM_FILTER" \ | |
| publish --access public --no-git-checks --provenance | |
| else | |
| pnpm -r \ | |
| publish --access public --no-git-checks --provenance | |
| fi | |
| - name: Single-package publish | |
| if: steps.plan.outputs.mode == 'single' | |
| env: | |
| PNPM_FILTER: ${{ steps.plan.outputs.filter }} | |
| run: | | |
| pnpm -r --filter="$PNPM_FILTER" \ | |
| publish --access public --no-git-checks --provenance | |
| # CDN propagation. With multiple new packages published in the same run, give | |
| # the npm CDN time to make every just-published tarball available everywhere. | |
| # 5 min is conservative; needed because the downstream `pnpm install --lockfile-only` | |
| # below would otherwise miss a freshly published version on some mirrors. | |
| - name: Wait for npm registry propagation | |
| run: sleep 300 | |
| # After publish, the workspace's `^X.Y.Z` cross-references for *downstream* packages | |
| # that we didn't bump still point at old versions. sync-versions rewrites them to the | |
| # newly-published versions and opens a PR with the diff + lockfile update. The PR is | |
| # idempotent — re-running the workflow updates the existing branch in place. | |
| - name: Sync published workspace versions | |
| id: sync-versions | |
| env: | |
| SYNC_BRANCH: automation/sync-published-versions | |
| run: | | |
| git fetch origin main | |
| git switch --force-create "$SYNC_BRANCH" origin/main | |
| # `git switch -C` does NOT discard uncommitted working-tree changes. | |
| # The publish step above runs each package's publish lifecycle, and | |
| # wallet-toolbox's `prepublish` (syncVersions.js) rewrites | |
| # client/package.json + mobile/package.json to the parent version and | |
| # leaves those edits uncommitted. Carried across the switch, they make | |
| # `pnpm sync-versions` read a version that was never published and | |
| # rewrite consumers to an unresolvable range (ERR_PNPM_NO_MATCHING_VERSION). | |
| # Hard-reset + clean so sync-versions operates only on committed main. | |
| git reset --hard origin/main | |
| git clean -fd | |
| pnpm sync-versions | |
| pnpm install --lockfile-only | |
| # Infra components are NOT in the pnpm workspace — each has its own | |
| # npm package-lock.json that goes stale when sync-versions rewrites | |
| # their @bsv/* ranges. Refresh each infra lockfile so the Dockerfile | |
| # `npm ci` step won't fail with "lockfile doesn't match" on the next | |
| # infra-release build. --package-lock-only skips node_modules I/O. | |
| # Peer-aware (no --legacy-peer-deps): @bsv/* libraries declare @bsv/sdk | |
| # as a peerDependency, so the lockfile MUST record that peer or the | |
| # Dockerfile `npm ci` fails with "Missing: @bsv/sdk from lock file". | |
| for d in infra/*/; do | |
| [ -f "$d/package.json" ] || continue | |
| [ -f "$d/package-lock.json" ] || continue | |
| echo "Refreshing lockfile in $d" | |
| (cd "$d" && npm install --package-lock-only --ignore-scripts) | |
| done | |
| if git diff --quiet; then | |
| echo "changed=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git add pnpm-lock.yaml 'packages/**/package.json' 'infra/**/package.json' 'infra/**/package-lock.json' | |
| git commit -m "chore: sync published workspace versions" | |
| git push --force-with-lease origin "HEAD:$SYNC_BRANCH" | |
| echo "changed=true" >> "$GITHUB_OUTPUT" | |
| echo "branch=$SYNC_BRANCH" >> "$GITHUB_OUTPUT" | |
| - name: Open version sync PR | |
| if: steps.sync-versions.outputs.changed == 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| SYNC_BRANCH: ${{ steps.sync-versions.outputs.branch }} | |
| run: | | |
| existing_pr="$(gh pr list --base main --head "$SYNC_BRANCH" --state open --json number --jq '.[0].number')" | |
| body="Updates workspace package references, pnpm-lock.yaml, and infra/*/package-lock.json after a successful npm publish." | |
| if [[ -n "$existing_pr" ]]; then | |
| gh pr edit "$existing_pr" \ | |
| --title "chore: sync published workspace versions" \ | |
| --body "$body" | |
| else | |
| gh pr create \ | |
| --base main \ | |
| --head "$SYNC_BRANCH" \ | |
| --title "chore: sync published workspace versions" \ | |
| --body "$body" | |
| fi |