-
Notifications
You must be signed in to change notification settings - Fork 4
290 lines (267 loc) · 14.9 KB
/
Copy pathrelease.yml
File metadata and controls
290 lines (267 loc) · 14.9 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
290
# Publish the pi-dispatch npm packages and cut each one's GitHub Release when its workspace's version
# changes on main: @edgehero/pi-dispatch (worker/, tagged worker-v*), @edgehero/pi-dispatch-receiver
# (receiver/, tagged receiver-v*) and @edgehero/pi-dispatch-admin (admin/, tagged admin-v*). The whole
# TOOL's releases (v*) are cut by repo-release.yml off the ROOT package.json version — no workflow shares
# a tag namespace with another.
#
# One job, three sequenced step-groups, in publish ORDER worker → receiver → admin. The order is load-
# bearing: receiver depends on the worker package being on the registry, and a matrix — even with
# max-parallel: 1 — promises serialization, not order. Steps in one job run top to bottom, and a failure
# in an earlier group stops the later ones, which is what you want when the later package depends on the
# earlier one landing.
#
# main is branch-protected (a PR is required and the five contract checks in pi-upgrade-check.yml must be
# green, admins included), so a push here is an already-gated merge — the human gate is the merge, not this
# workflow (consistent with CONST-MERGE-NEVER-AUTOMATIC: nothing here merges anything). Publishing is
# idempotent PER PACKAGE: each group fires only when its workspace's version
# is not already on npm, so re-runs, reverts, and pushes that touch only one workspace are no-ops for the
# other two. Bump a workspace's version in your PR to ship that package.
#
# Required repo secret: NPM_TOKEN — an npm *automation* (or granular, "bypass 2FA") token that can publish to
# the @edgehero scope. Interactive 2FA cannot run in CI; the token is how publishing works here.
# Optional repo secret: ANTHROPIC_API_KEY — enables AI-written release notes; without it, each release falls
# back to a plain commit list. Optional repo variable: RELEASE_MODEL (default: claude-sonnet-5).
name: release
on:
push:
branches: [main]
paths:
- "worker/**"
- "receiver/**"
- "admin/**"
- ".github/workflows/release.yml"
- ".github/scripts/release-notes.mjs"
# Manual re-run (available once this file is on the default branch): re-runs are safe — publish/release
# skip when the version is already on npm / the tag exists.
workflow_dispatch: {}
permissions:
contents: write # create tags + releases
concurrency:
group: release
cancel-in-progress: false
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # full history for the release-notes commit ranges
- uses: actions/setup-node@v4
with:
node-version: "22.19"
registry-url: "https://registry.npmjs.org"
- name: Install
run: npm ci
# The whole suite once, up front, rather than each workspace's tests just before its publish. Simpler —
# one step instead of three — and stricter: a worker change that breaks receiver's tests should block
# worker's publish too, and per-workspace gating would have let it through.
- name: Test everything being shipped
run: npm test
# ── @edgehero/pi-dispatch (worker/) ─────────────────────────────────────────────────────────────────
# Name and version are read from the workspace's package.json, never restated here — the package.json
# is the fact, this file just acts on it.
- name: "worker: resolve version + decide publish/release"
id: worker
env:
GH_TOKEN: ${{ github.token }}
run: |
V=$(node -p "require('./worker/package.json').version")
NAME=$(node -p "require('./worker/package.json').name")
echo "version=$V" >> "$GITHUB_OUTPUT"
echo "name=$NAME" >> "$GITHUB_OUTPUT"
# npm view exits 0 printing the version when NAME@V is on the registry, and exits non-zero with
# E404 both when the package has never been published (worker/receiver's first release) and when
# only this version is new — verified against npm ≥9; both mean "go". Any OTHER failure — auth,
# network, registry outage — must not be read as "unpublished": a gate that publishes on a flake
# is not a gate, so those fail the run instead.
CODE=0; VIEW=$(npm view "$NAME@$V" version 2>&1) || CODE=$?
if [ "$CODE" -eq 0 ]; then
echo "publish=no" >> "$GITHUB_OUTPUT"; echo "$NAME@$V already on npm — skip publish"
elif printf '%s' "$VIEW" | grep -q "E404"; then
echo "publish=yes" >> "$GITHUB_OUTPUT"
else
echo "npm view $NAME@$V failed for a reason other than 404 — refusing to guess:"
printf '%s\n' "$VIEW"; exit 1
fi
# This package's GitHub releases are tagged worker-v* so the tool's own releases keep the v*
# namespace (see repo-release.yml). npm publish is unaffected — it keys off the package version.
if gh release view "worker-v$V" >/dev/null 2>&1; then
echo "release=no" >> "$GITHUB_OUTPUT"; echo "release worker-v$V already exists — skip release"
else
echo "release=yes" >> "$GITHUB_OUTPUT"
fi
- name: "worker: publish to npm"
if: steps.worker.outputs.publish == 'yes'
run: npm publish --workspace worker --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: "worker: build release notes"
if: steps.worker.outputs.release == 'yes'
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
RELEASE_MODEL: ${{ vars.RELEASE_MODEL }}
# Which artifact is being described — the script reads this from its caller (see release-notes.mjs
# on why). The name comes from the resolve step, so a rename never leaves this stale.
PRODUCT: ${{ steps.worker.outputs.name }}
PRODUCT_DESC: the pi-dispatch worker — drains the queue and runs one isolated container per job
run: |
PREV=$(git tag --list 'worker-v*' --sort=-v:refname | head -n1)
RANGE=${PREV:+$PREV..HEAD}
COMMITS=$(git log $RANGE --no-merges --pretty=format:'- %s' -- worker/ | head -n 60)
if [ -n "$ANTHROPIC_API_KEY" ] && COMMITS="$COMMITS" VERSION="v${{ steps.worker.outputs.version }}" \
node .github/scripts/release-notes.mjs > /tmp/notes-worker.md 2>/tmp/notes-worker.err; then
echo "release notes: AI-written"
else
echo "release notes: AI unavailable ($(head -n1 /tmp/notes-worker.err 2>/dev/null)) — using commit list"
# Capped like the AI notes are, and the remainder counted rather than dropped silently.
TOTAL=$(printf '%s\n' "$COMMITS" | wc -l | tr -d ' ')
{
echo "### Changes"; echo
printf '%s\n' "$COMMITS" | head -n 12
if [ "$TOTAL" -gt 12 ]; then printf -- '- …and %s more\n' "$((TOTAL - 12))"; fi
} > /tmp/notes-worker.md
fi
# Appended on BOTH paths -- this link is what earns the brevity above.
if [ -n "$PREV" ]; then
printf '\n**Full changelog**: %s/%s/compare/%s...worker-v%s\n' \
"${{ github.server_url }}" "${{ github.repository }}" "$PREV" "${{ steps.worker.outputs.version }}" >> /tmp/notes-worker.md
fi
- name: "worker: create GitHub Release"
if: steps.worker.outputs.release == 'yes'
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "worker-v${{ steps.worker.outputs.version }}" \
--title "${{ steps.worker.outputs.name }} v${{ steps.worker.outputs.version }}" \
--notes-file /tmp/notes-worker.md
# ── @edgehero/pi-dispatch-receiver (receiver/) ──────────────────────────────────────────────────────
# Same shape as worker's group above (the gate's reasoning lives there). Runs after worker on purpose:
# receiver depends on the worker package, which must be on the registry first.
- name: "receiver: resolve version + decide publish/release"
id: receiver
env:
GH_TOKEN: ${{ github.token }}
run: |
V=$(node -p "require('./receiver/package.json').version")
NAME=$(node -p "require('./receiver/package.json').name")
echo "version=$V" >> "$GITHUB_OUTPUT"
echo "name=$NAME" >> "$GITHUB_OUTPUT"
CODE=0; VIEW=$(npm view "$NAME@$V" version 2>&1) || CODE=$?
if [ "$CODE" -eq 0 ]; then
echo "publish=no" >> "$GITHUB_OUTPUT"; echo "$NAME@$V already on npm — skip publish"
elif printf '%s' "$VIEW" | grep -q "E404"; then
echo "publish=yes" >> "$GITHUB_OUTPUT"
else
echo "npm view $NAME@$V failed for a reason other than 404 — refusing to guess:"
printf '%s\n' "$VIEW"; exit 1
fi
if gh release view "receiver-v$V" >/dev/null 2>&1; then
echo "release=no" >> "$GITHUB_OUTPUT"; echo "release receiver-v$V already exists — skip release"
else
echo "release=yes" >> "$GITHUB_OUTPUT"
fi
- name: "receiver: publish to npm"
if: steps.receiver.outputs.publish == 'yes'
run: npm publish --workspace receiver --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: "receiver: build release notes"
if: steps.receiver.outputs.release == 'yes'
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
RELEASE_MODEL: ${{ vars.RELEASE_MODEL }}
PRODUCT: ${{ steps.receiver.outputs.name }}
PRODUCT_DESC: the pi-dispatch webhook receiver — turns forge events into queued jobs
run: |
PREV=$(git tag --list 'receiver-v*' --sort=-v:refname | head -n1)
RANGE=${PREV:+$PREV..HEAD}
COMMITS=$(git log $RANGE --no-merges --pretty=format:'- %s' -- receiver/ | head -n 60)
if [ -n "$ANTHROPIC_API_KEY" ] && COMMITS="$COMMITS" VERSION="v${{ steps.receiver.outputs.version }}" \
node .github/scripts/release-notes.mjs > /tmp/notes-receiver.md 2>/tmp/notes-receiver.err; then
echo "release notes: AI-written"
else
echo "release notes: AI unavailable ($(head -n1 /tmp/notes-receiver.err 2>/dev/null)) — using commit list"
TOTAL=$(printf '%s\n' "$COMMITS" | wc -l | tr -d ' ')
{
echo "### Changes"; echo
printf '%s\n' "$COMMITS" | head -n 12
if [ "$TOTAL" -gt 12 ]; then printf -- '- …and %s more\n' "$((TOTAL - 12))"; fi
} > /tmp/notes-receiver.md
fi
if [ -n "$PREV" ]; then
printf '\n**Full changelog**: %s/%s/compare/%s...receiver-v%s\n' \
"${{ github.server_url }}" "${{ github.repository }}" "$PREV" "${{ steps.receiver.outputs.version }}" >> /tmp/notes-receiver.md
fi
- name: "receiver: create GitHub Release"
if: steps.receiver.outputs.release == 'yes'
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "receiver-v${{ steps.receiver.outputs.version }}" \
--title "${{ steps.receiver.outputs.name }} v${{ steps.receiver.outputs.version }}" \
--notes-file /tmp/notes-receiver.md
# ── @edgehero/pi-dispatch-admin (admin/) ────────────────────────────────────────────────────────────
# Same shape again. admin's prepublishOnly builds its bundle, so publish needs nothing extra here.
- name: "admin: resolve version + decide publish/release"
id: admin
env:
GH_TOKEN: ${{ github.token }}
run: |
V=$(node -p "require('./admin/package.json').version")
NAME=$(node -p "require('./admin/package.json').name")
echo "version=$V" >> "$GITHUB_OUTPUT"
echo "name=$NAME" >> "$GITHUB_OUTPUT"
CODE=0; VIEW=$(npm view "$NAME@$V" version 2>&1) || CODE=$?
if [ "$CODE" -eq 0 ]; then
echo "publish=no" >> "$GITHUB_OUTPUT"; echo "$NAME@$V already on npm — skip publish"
elif printf '%s' "$VIEW" | grep -q "E404"; then
echo "publish=yes" >> "$GITHUB_OUTPUT"
else
echo "npm view $NAME@$V failed for a reason other than 404 — refusing to guess:"
printf '%s\n' "$VIEW"; exit 1
fi
if gh release view "admin-v$V" >/dev/null 2>&1; then
echo "release=no" >> "$GITHUB_OUTPUT"; echo "release admin-v$V already exists — skip release"
else
echo "release=yes" >> "$GITHUB_OUTPUT"
fi
- name: "admin: publish to npm"
if: steps.admin.outputs.publish == 'yes'
run: npm publish --workspace admin --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: "admin: build release notes"
if: steps.admin.outputs.release == 'yes'
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
RELEASE_MODEL: ${{ vars.RELEASE_MODEL }}
PRODUCT: ${{ steps.admin.outputs.name }}
PRODUCT_DESC: the operator console for pi-dispatch, shipped as a pi extension
run: |
PREV=$(git tag --list 'admin-v*' --sort=-v:refname | head -n1)
RANGE=${PREV:+$PREV..HEAD}
COMMITS=$(git log $RANGE --no-merges --pretty=format:'- %s' -- admin/ | head -n 60)
if [ -n "$ANTHROPIC_API_KEY" ] && COMMITS="$COMMITS" VERSION="v${{ steps.admin.outputs.version }}" \
node .github/scripts/release-notes.mjs > /tmp/notes-admin.md 2>/tmp/notes-admin.err; then
echo "release notes: AI-written"
else
echo "release notes: AI unavailable ($(head -n1 /tmp/notes-admin.err 2>/dev/null)) — using commit list"
TOTAL=$(printf '%s\n' "$COMMITS" | wc -l | tr -d ' ')
{
echo "### Changes"; echo
printf '%s\n' "$COMMITS" | head -n 12
if [ "$TOTAL" -gt 12 ]; then printf -- '- …and %s more\n' "$((TOTAL - 12))"; fi
} > /tmp/notes-admin.md
fi
if [ -n "$PREV" ]; then
printf '\n**Full changelog**: %s/%s/compare/%s...admin-v%s\n' \
"${{ github.server_url }}" "${{ github.repository }}" "$PREV" "${{ steps.admin.outputs.version }}" >> /tmp/notes-admin.md
fi
- name: "admin: create GitHub Release"
if: steps.admin.outputs.release == 'yes'
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "admin-v${{ steps.admin.outputs.version }}" \
--title "${{ steps.admin.outputs.name }} v${{ steps.admin.outputs.version }}" \
--notes-file /tmp/notes-admin.md