-
Notifications
You must be signed in to change notification settings - Fork 1
289 lines (268 loc) · 12.5 KB
/
Copy pathrelease-publish.yml
File metadata and controls
289 lines (268 loc) · 12.5 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
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
name: Release — stage-publish (OIDC) + GitHub release
# Tokenless release pipeline, triggered by pushing a vX.Y.Z tag. Authenticates to
# npm via OIDC trusted publishing (no NPM_TOKEN lives anywhere) and submits every
# workspace package to npm's STAGING area via `npm stage publish` rather than
# publishing live. A maintainer approves the staged tarballs on npmjs.com with 2FA
# to go live — that approval is the release gate (see docs/releasing.md).
#
# A separate, INDEPENDENT job (github-release) cuts a GitHub release for the tag
# from the matching CHANGELOG.md section. It does NOT depend on the npm staging
# approval — the GitHub release documents the tagged commit immediately on push.
#
# Cut a release: after the `release: vX.Y.Z` PR merges to main, push the tag:
# git tag v0.11.0 && git push origin v0.11.0
# The tag push triggers this workflow. (workflow_dispatch is a manual fallback.)
#
# One-time setup (npmjs.com, per package): add a Trusted Publisher pointing at
# org=tpsdev-ai, repo=flair, workflow=release-publish.yml, environment=release
# and allow ONLY "npm stage publish". The `release` GitHub environment must permit
# `v*` tag deploys.
#
# flair-bench is version-bumped/tagged with the other 7 but stages in its own
# continue-on-error step below — it needs a one-time first manual publish +
# Trusted Publisher registration before `npm stage publish` will work for it
# (see docs/releasing.md's "flair-bench bootstrap" section).
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
version:
description: "Fallback only: version to stage (a vX.Y.Z tag must already exist on a merged commit)"
required: true
type: string
# Default to read-only; the publish job opts into exactly what it needs.
permissions:
contents: read
concurrency:
group: release-publish
cancel-in-progress: false
jobs:
stage-publish:
name: Stage-publish all packages
runs-on: ubuntu-latest
# `release` is an OIDC-scoping environment, NOT an approval gate: it pins the
# trusted-publisher trust to this environment. The human gate is the npm
# staging approval (2FA), not a GitHub reviewer.
environment: release
permissions:
id-token: write # mint the short-lived OIDC token for npm trusted publishing
contents: read # checkout + fetch main for the ancestry guard
steps:
- name: Resolve version
id: ver
env:
EVENT: ${{ github.event_name }}
REF_NAME: ${{ github.ref_name }}
INPUT_VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
if [ "$EVENT" = "workflow_dispatch" ]; then
VERSION="$INPUT_VERSION"
else
# tag push: github.ref_name is the tag (vX.Y.Z)
VERSION="${REF_NAME#v}"
fi
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.]+)?$ ]]; then
echo "::error::Invalid version '$VERSION'. Expected semver (e.g. 0.11.0 or 1.0.0-rc.1)."
exit 1
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Resolved version: $VERSION"
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
fetch-depth: 0
- name: Verify tagged commit is on main
# A tag can point at any commit; require it to be merged to main so a tag
# can never ship un-reviewed code past the npm staging gate.
run: |
set -euo pipefail
git fetch --no-tags origin main
if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
echo "::error::Commit ${GITHUB_SHA} is not an ancestor of origin/main. Tag a merged release commit."
exit 1
fi
echo " ok ${GITHUB_SHA} is on main"
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: "22"
# NO registry-url: it makes setup-node write an .npmrc with
# _authToken=${NODE_AUTH_TOKEN} + always-auth, and with NODE_AUTH_TOKEN
# unset npm tries the EMPTY token and fails E401 instead of falling
# through to OIDC trusted publishing. OIDC needs no token config; npm
# defaults to registry.npmjs.org.
- name: Upgrade npm to a staging-capable version
# OIDC trusted publishing needs npm >= 11.5.1; `npm stage` needs >= 11.15.0.
# Node 22 ships npm 10.x, so upgrade explicitly.
run: |
npm install -g npm@^11.15.0
echo "npm $(npm --version)"
- uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2
with:
bun-version: "1.3.10"
- uses: socketdev/action@ba6de6cc0565af1f42295590380973573297e31f # v1.3.2
with:
mode: firewall-free
- name: Install (sfw — retry on intermittent Socket-firewall hang, ops-fumh)
run: |
for attempt in 1 2 3; do
echo "::group::sfw bun install (attempt $attempt/3)"
if timeout 300 sfw bun install --frozen-lockfile; then echo "::endgroup::"; exit 0; fi
echo "::endgroup::"
echo "sfw install attempt $attempt failed or timed out (5m) — the sfw hang is intermittent; retrying in 10s"
sleep 10
done
echo "sfw bun install failed after 3 attempts"; exit 1
- name: Verify all workspace versions match the tag
# All values flow through env, never ${{ }} interpolation inside the
# script body — keeps the resolved version out of the shell parser.
env:
VERSION: ${{ steps.ver.outputs.version }}
run: |
set -euo pipefail
PKG_JSONS=(
package.json
packages/flair-client/package.json
packages/flair-mcp/package.json
packages/openclaw-flair/package.json
packages/pi-flair/package.json
packages/n8n-nodes-flair/package.json
packages/langgraph-flair/package.json
packages/flair-bench/package.json
)
for pj in "${PKG_JSONS[@]}"; do
name="$(node -p "require('./$pj').name")"
pv="$(node -p "require('./$pj').version")"
if [ "$pv" != "$VERSION" ]; then
echo "::error::$name is at $pv, expected $VERSION (tag v$VERSION). Tag a merged release commit."
exit 1
fi
echo " ok $name @ $pv"
done
- name: Build
run: |
set -euo pipefail
npm run build && npm run build:cli
(cd packages/flair-client && npm run build)
(cd packages/flair-mcp && npm run build)
(cd packages/n8n-nodes-flair && npm run build)
- name: Stage-publish all packages (dependency order)
# flair-client first — the other packages depend on it. Staging does not
# resolve dependencies (that happens at install, post-approval), but we
# keep dep order so the approve-and-go-live sequence is consistent.
run: |
set -euo pipefail
DIRS=(
packages/flair-client
packages/flair-mcp
.
packages/openclaw-flair
packages/pi-flair
packages/n8n-nodes-flair
packages/langgraph-flair
)
for dir in "${DIRS[@]}"; do
name="$(node -p "require('./$dir/package.json').name")"
echo "::group::npm stage publish — $name ($dir)"
( cd "$dir" && npm stage publish )
echo "::endgroup::"
done
- name: Stage-publish flair-bench (bootstrap-pending — isolated on purpose)
# `npm stage publish` requires the package already exist on the npm
# registry (npm-stage(1): "Package must exist"), and a Trusted Publisher
# can only be registered for a package that already exists — so this
# step WILL fail until a maintainer does the one-time bootstrap in
# docs/releasing.md (first manual `npm publish` of @tpsdev-ai/flair-bench,
# then register its Trusted Publisher). `continue-on-error: true` keeps
# that expected failure from aborting the other 7 packages' staging (the
# loop above runs under `set -euo pipefail` with no per-package
# isolation — a hard failure here would kill the whole job). Remove
# continue-on-error once the bootstrap is confirmed done and this step
# has staged flair-bench successfully at least once.
continue-on-error: true
run: |
set -euo pipefail
echo "::group::npm stage publish — @tpsdev-ai/flair-bench (packages/flair-bench)"
( cd packages/flair-bench && npm stage publish )
echo "::endgroup::"
- name: Summary
env:
VERSION: ${{ steps.ver.outputs.version }}
run: |
{
echo "### 🚀 v$VERSION staged for release"
echo ""
echo "All 7 lockstep-versioned workspace packages submitted to npm **staging**"
echo "with provenance. They are **not live yet**."
echo ""
echo "flair-bench is included in the version bump/tag but stages in its own"
echo "step, which is allowed to fail until the one-time first-publish +"
echo "Trusted Publisher bootstrap in docs/releasing.md is done (check the"
echo "'Stage-publish flair-bench' step above for its actual result)."
echo ""
echo "**To release:** open [npmjs.com → Staged Packages](https://www.npmjs.com/settings/tpsdev-ai/staging),"
echo "review each tarball, and approve with 2FA. Or: \`npm stage list\` then \`npm stage approve <id>\`."
} >> "$GITHUB_STEP_SUMMARY"
github-release:
name: Cut GitHub release from CHANGELOG
runs-on: ubuntu-latest
# Independent of stage-publish on purpose: the GitHub release documents the
# tagged commit and can be cut immediately on tag push. It does NOT wait on
# the npm staging 2FA approval (a manual external gate) — that gate governs
# what goes LIVE on npm, not whether the tag is recorded on GitHub.
if: github.event_name == 'push'
permissions:
contents: write # create/edit the GitHub release for the tag
steps:
- name: Resolve version
id: ver
env:
REF_NAME: ${{ github.ref_name }}
run: |
set -euo pipefail
# tag push: github.ref_name is the tag (vX.Y.Z)
VERSION="${REF_NAME#v}"
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.]+)?$ ]]; then
echo "::error::Invalid version '$VERSION'. Expected semver (e.g. 0.11.0 or 1.0.0-rc.1)."
exit 1
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Resolved version: $VERSION"
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Extract CHANGELOG section
# The node script reads CHANGELOG.md and writes the matching section body
# to a file. Version flows through env, never ${{ }} interpolation in the
# script body, and the body is captured to a file (never re-emitted into a
# shell string) so neither the tag nor the CHANGELOG text can be parsed as
# shell. Fails loudly (non-zero) if the section is missing or empty.
env:
VERSION: ${{ steps.ver.outputs.version }}
run: |
set -euo pipefail
node scripts/changelog-extract.mjs "$VERSION" CHANGELOG.md > release-notes.md
echo "Extracted $(wc -l < release-notes.md) lines for v$VERSION"
- name: Create or update GitHub release
# Idempotent: if a release for the tag already exists (re-run, or the
# manually-cut v0.16.0), update it in place instead of failing. The notes
# body is passed via --notes-file (a file on disk), and the tag/title via
# env-expanded argv — content is never interpolated into the command text.
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ github.ref_name }}
VERSION: ${{ steps.ver.outputs.version }}
run: |
set -euo pipefail
if gh release view "$TAG" >/dev/null 2>&1; then
echo "Release $TAG exists — updating notes."
gh release edit "$TAG" \
--title "v$VERSION" \
--notes-file release-notes.md
else
echo "Creating release $TAG."
gh release create "$TAG" \
--title "v$VERSION" \
--notes-file release-notes.md \
--verify-tag
fi
echo "Release URL: $(gh release view "$TAG" --json url --jq .url)"