-
Notifications
You must be signed in to change notification settings - Fork 15
227 lines (204 loc) · 9.76 KB
/
Copy pathrelease.yaml
File metadata and controls
227 lines (204 loc) · 9.76 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
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