Skip to content

Merge pull request #144 from bsv-blockchain/automation/sync-published… #28

Merge pull request #144 from bsv-blockchain/automation/sync-published…

Merge pull request #144 from bsv-blockchain/automation/sync-published… #28

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)
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@8912a9102ac27614460f54aedde9e1e7f9aec20d # 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
pnpm sync-versions
pnpm install --lockfile-only
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'
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')"
if [[ -n "$existing_pr" ]]; then
gh pr edit "$existing_pr" \
--title "chore: sync published workspace versions" \
--body "Updates workspace package references and pnpm-lock.yaml after a successful npm publish."
else
gh pr create \
--base main \
--head "$SYNC_BRANCH" \
--title "chore: sync published workspace versions" \
--body "Updates workspace package references and pnpm-lock.yaml after a successful npm publish."
fi