Skip to content

refactor(overlay-topics): extract output classification helpers to cu… #65

refactor(overlay-topics): extract output classification helpers to cu…

refactor(overlay-topics): extract output classification helpers to cu… #65

Workflow file for this run

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