Repository navigation
336 lines (317 loc) · 15.5 KB
/
Copy pathrelease-bugfix.yml
File metadata and controls
336 lines (317 loc) · 15.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
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
name: Bugfix Release (carve-out)
# Dedicated emergency / hotfix publish channel (#2011), PR-gated since #2851.
#
# Publishes a version cut from a NON-master ref -- e.g. a released tag plus a
# single fix -- STRICTLY excluding the unreleased work that has since
# accumulated on master. First used for the v0.1.182 emergency: ship ONLY the
# integer max_tokens fix without dragging in the ~70 commits already merged to
# master after v0.1.181.
#
# Unlike release.yml (which builds the POST-MERGE MASTER tree on push-to-master,
# so it can never exclude master's existing commits), this checks out and
# publishes the CARVE-OUT PR HEAD tree itself -- the artifact is exactly that
# tree, nothing else.
#
# GATE MODEL (#2851): the carve-out branch is opened as a PR against master and
# publishes ONLY when that PR is MERGED -- the same human review gate as every
# normal release. Pushing a bugfix-release/** branch triggers NOTHING (the old
# silent-push auto-publish was removed). The head branch keeps the
# bugfix-release/ prefix as the carve-out MARKER: release.yml's detection
# deliberately does not match it, so merging the PR cannot double-publish the
# post-merge master through the normal channel.
#
# NOTE 2026-10-25: the workflow_dispatch trigger was REMOVED after a concurrent
# agent session abused manual dispatch to publish v0.1.208 outside the intended
# flow (see #4718); the human PR merge became the only ignition shape.
# NOTE 2026-10-28 (owner decision; revives the 5de4103c/#3613 emergency valve
# whose step gates this file kept all along): workflow_dispatch returns AS THE
# EMERGENCY VALVE ONLY — and hardened against a repeat of #4718: fast mode
# engages solely when `fast` is checked AND `confirm_fast` is typed exactly
# "FAST PUBLISH CONFIRMED" (see FAST_ON below). Any default or programmatic
# dispatch silently keeps the slow safe path.
#
# Guarded so it can never:
# - move `latest` backwards (target must be strictly newer than npm latest);
# - re-publish an already-live version;
# - ship a sub-0.1.0 line (#1385);
# - publish a stable version with no release-notes entry (#1870);
# - act on fork PRs (secrets unavailable) or on non-merged closes.
#
# Master interaction: merging the carve-out PR brings the version bump INTO
# master (master then equals npm latest). Master advances independently on its
# next normal release, which MUST be higher than the carve-out version.
on:
pull_request:
types: [closed]
branches: [master]
workflow_dispatch:
# Emergency valve only — see the 2026-10-28 header note. Slow-safe unless
# BOTH signals arrive together.
inputs:
fast:
description: "EMERGENCY fast publish: skip the full-CI dispatch/gate and the full npm test. Keeps every version guard + npm ci + typecheck + build. Owner-owned risk."
type: boolean
required: false
default: false
confirm_fast:
description: "TYPE EXACTLY: FAST PUBLISH CONFIRMED — anything else (blank included) keeps the normal slow path."
type: string
required: false
default: ""
concurrency:
group: bugfix-release
cancel-in-progress: false
jobs:
release:
runs-on: ubuntu-latest
timeout-minutes: 90
env:
# Fast mode = BOTH explicit signals (typed-phrase guard vs accidental/
# automated dispatches, #4718). Non-dispatch events see FAST_ON=false.
FAST_ON: ${{ inputs.fast == true && inputs.confirm_fast == 'FAST PUBLISH CONFIRMED' }}
permissions:
contents: write
id-token: write
actions: write
steps:
- name: Ensure jq (2026-10 runner images are unreliable about it — always install our own)
run: |
type -a jq || true
mkdir -p "$HOME/.local/bin"
curl -fsSL "https://github.com/jqlang/jq/releases/download/jq-1.8.1/jq-linux-amd64" -o "$HOME/.local/bin/jq"
chmod +x "$HOME/.local/bin/jq"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
"$HOME/.local/bin/jq" --version
- name: Validate trigger + resolve the publish ref
id: resolve
run: |
if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then
MERGED="$(jq -r '.pull_request.merged' "$GITHUB_EVENT_PATH")"
BRANCH="$(jq -r '.pull_request.head.ref' "$GITHUB_EVENT_PATH")"
SAME_REPO="$(jq -r '(.pull_request.base.repo.full_name == .pull_request.head.repo.full_name)' "$GITHUB_EVENT_PATH")"
SHA="$(jq -r '.pull_request.head.sha' "$GITHUB_EVENT_PATH")"
if [ "$MERGED" != "true" ]; then
echo "::notice::PR $GITHUB_EVENT_NUMBER closed without merging — nothing to publish"
exit 0
fi
case "$BRANCH" in
bugfix-release/*) ;;
*)
echo "::notice::head branch '$BRANCH' is not a bugfix-release/* carve-out branch — nothing to publish"
exit 0
;;
esac
if [ "$SAME_REPO" != "true" ]; then
echo "::error::fork PRs cannot use the carve-out channel (repo secrets are unavailable for them)"
exit 1
fi
else
BRANCH="$GITHUB_REF_NAME"
SHA=""
fi
echo "branch=$BRANCH" >> "$GITHUB_OUTPUT"
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
echo "proceed=true" >> "$GITHUB_OUTPUT"
- name: Checkout
if: steps.resolve.outputs.proceed == 'true'
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Switch to the carve-out PR head (pre-merge tree)
if: steps.resolve.outputs.proceed == 'true' && github.event_name == 'pull_request'
run: |
SHA="${{ steps.resolve.outputs.sha }}"
git rev-parse --verify --quiet "${SHA}^{commit}" >/dev/null || git fetch origin "$SHA"
git checkout --detach "$SHA"
- uses: actions/setup-node@v4
if: steps.resolve.outputs.proceed == 'true'
with:
node-version: 22
registry-url: https://registry.npmjs.org
- name: Read version
id: version
if: steps.resolve.outputs.proceed == 'true'
run: |
V="$(node -p "require('./package.json').version")"
case "$V" in
0.0.*)
echo "::error::package.json version $V is below 0.1.0 — official releases must always sort above the 0.0.x PR-build line (#1385)"
exit 1
;;
esac
echo "version=$V" >> $GITHUB_OUTPUT
REF="${{ steps.resolve.outputs.branch }}"
SHA="${{ steps.resolve.outputs.sha }}"
[ -z "$SHA" ] && SHA="$(git rev-parse HEAD)"
echo "::notice::Publishing carve-out from '$REF' @ ${SHA:0:12} -> v$V (master NOT involved)"
- name: Verify plugin manifests match package.json (#4449)
# The four plugin manifests carry their own version fields — the
# marketplaces use them for UPDATE DETECTION, so a release whose
# manifests disagree with package.json ships a plugin that never
# prompts users to upgrade. Drift = red release, nothing shipped.
# See reference/release.md.
if: steps.resolve.outputs.proceed == 'true'
# #4576 follow-up: lanes cut from a pre-#4449 tag do not carry the
# manifest sync script at all -- fail-open with a notice there instead
# of MODULE_NOT_FOUND (same existence-guard philosophy as release-manual's
# lane guard).
run: |
if [ -f scripts/sync-codex-plugin-version.mjs ]; then
node scripts/sync-codex-plugin-version.mjs --check
else
echo "::notice::this tree predates #4449 (no scripts/sync-codex-plugin-version.mjs) — manifest sync check skipped for the pre-#4449 lane"
fi
- name: Publish guard (monotonic + not-already-live)
if: steps.resolve.outputs.proceed == 'true'
run: |
VER="${{ steps.version.outputs.version }}"
LATEST="$(npm view billion-context version)"
if [ -z "$LATEST" ]; then
echo "::error::cannot read npm latest for billion-context"; exit 1
fi
LIVE="$(npm view "billion-context@$VER" version 2>/dev/null || true)"
if [ "$LIVE" = "$VER" ]; then
echo "::error::$VER is already published on npm; pick a newer version"; exit 1
fi
node -e '
const base = (s) => s.split("-")[0].split(".").map(Number);
const cmp = (a,b) => { for (let i=0;i<3;i++){ if(a[i]!==b[i]) return a[i]-b[i]; } return 0; };
const v = process.argv[1], l = process.argv[2];
const c = cmp(base(v), base(l));
if (c <= 0) { console.error("::error::target " + v + " is not strictly newer than npm latest " + l + " — refusing to hold/downgrade latest"); process.exit(1); }
console.log("guard ok: " + v + " > latest " + l);
' "$VER" "$LATEST"
- name: Verify release-notes entry (#1870)
# Policy (2026-10-04 revision of #1870): entries are OPT-IN severity
# declarations — only severe releases need one; ordinary releases ship
# silently with NO entry. Missing entry => notice + pass; present
# entry => validated and logged.
if: steps.resolve.outputs.proceed == 'true'
run: |
VER="${{ steps.version.outputs.version }}"
case "$VER" in
*-*) echo "prerelease $VER — release-notes gate skipped"; exit 0 ;;
esac
node -e "
const notes = require('./release-notes/package.json').billionContextReleaseNotes;
const entry = notes && notes.releases && notes.releases.find((e) => e.version === process.argv[1]);
if (!entry) {
console.log('::notice::release ' + process.argv[1] + ' has no release-notes entry — shipping silently (entries are opt-in; only severe releases need one). See release-notes/README.md (#1870).');
process.exit(0);
}
console.log('release-notes entry ok:', entry.version, '[' + entry.tier + ']', entry.summary.slice(0, 80));
" "$VER"
- name: Full CI dispatch (all lanes, publish sha)
# Policy (2026-10-10, after the v0.1.192 carve-out): every release
# channel must run the COMPLETE CI set at the exact publish sha before
# npm publish. In the PR-gated flow the lanes already ran on the PR
# head sha (path-filtered pull_request triggers) — dispatch only what
# has no run there (idempotent); the Full CI gate after the pre-flight
# blocks publish until green.
# EMERGENCY fast mode (owner 2026-10-17): dispatch is already a
# human-picked ref; with fast=true the human explicitly owns skipping
# this lane matrix — version guards and the build sanity below still
# run.
if: steps.resolve.outputs.proceed == 'true' && env.FAST_ON != 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
GITHUB_SHA="${{ steps.resolve.outputs.sha }}"
[ -z "$GITHUB_SHA" ] && GITHUB_SHA="$(git rev-parse HEAD)"
export GITHUB_SHA
export GITHUB_REF_NAME="${{ steps.resolve.outputs.branch }}"
node scripts/release-ci-gate.mjs --dispatch
- name: Pre-flight gate (full suite)
# keep the claude-native live e2e subtests in this pre-publish gate (#1248)
if: steps.resolve.outputs.proceed == 'true' && env.FAST_ON != 'true'
env:
ACP_TEST_CLAUDE_NATIVE: "1"
run: |
npm ci
npm run typecheck
npm test
npm run build
- name: Pre-flight gate (fast mode — build sanity only)
# EMERGENCY fast mode: keep the cheap sanity (install/typecheck/build)
# so a broken tree still fails loudly, but skip the full npm test —
# the lane matrix above was skipped by explicit human choice.
if: steps.resolve.outputs.proceed == 'true' && env.FAST_ON == 'true'
run: |
echo "::warning::FAST MODE by '${{ github.actor }}' (workflow_dispatch fast=true + typed confirmation) — full CI lanes and the full npm test SKIPPED. Publish sanity = version guards + npm ci + typecheck + build only."
npm ci
npm run typecheck
npm run build
- name: Full CI gate (all lanes green at this sha)
# Hard precondition for publish: lanes dispatched above (plus the PR's
# own lane runs at the head sha) ran in parallel with the pre-flight;
# wait them out now. Any red lane or timeout ⇒ release fails, nothing
# is published.
if: steps.resolve.outputs.proceed == 'true' && env.FAST_ON != 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CI_GATE_TIMEOUT_MIN: "60"
run: |
GITHUB_SHA="${{ steps.resolve.outputs.sha }}"
[ -z "$GITHUB_SHA" ] && GITHUB_SHA="$(git rev-parse HEAD)"
export GITHUB_SHA
export GITHUB_REF_NAME="${{ steps.resolve.outputs.branch }}"
node scripts/release-ci-gate.mjs --wait
- name: Check if prerelease
id: pre
if: steps.resolve.outputs.proceed == 'true'
run: |
if echo "${{ steps.version.outputs.version }}" | grep -q '-'; then
echo "tag=dev" >> $GITHUB_OUTPUT
echo "prerelease=true" >> $GITHUB_OUTPUT
else
echo "tag=latest" >> $GITHUB_OUTPUT
echo "prerelease=false" >> $GITHUB_OUTPUT
fi
- name: Create tag
if: steps.resolve.outputs.proceed == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag "v${{ steps.version.outputs.version }}" || true
git push origin "v${{ steps.version.outputs.version }}" || true
- name: Publish
if: steps.resolve.outputs.proceed == 'true'
run: npm publish --tag ${{ steps.pre.outputs.tag }} --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Create GitHub Release
if: steps.resolve.outputs.proceed == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VER="${{ steps.version.outputs.version }}"
# nearest ancestor tag other than the one we just made (=> v0.1.181 for this carve-out)
LAST="$(git describe --match 'v*' --exclude "v$VER*" --tags --abbrev=0 2>/dev/null || echo "")"
NOTES="$RUNNER_TEMP/release-notes.md"
if [ -n "$LAST" ] && [ "$LAST" != "v$VER" ]; then
{
echo "## Changes since $LAST"
echo ""
git log --no-merges --pretty='- %s' "$LAST..HEAD"
} > "$NOTES"
else
echo "- initial release" > "$NOTES"
fi
node -e '
const fs = require("fs");
const [notesFile, ver, prerelease, out] = process.argv.slice(1);
const body = {
tag_name: "v" + ver,
name: "v" + ver,
prerelease: prerelease === "true",
body: fs.readFileSync(notesFile, "utf8").trim()
};
fs.writeFileSync(out, JSON.stringify(body));
' "$NOTES" "$VER" "${{ steps.pre.outputs.prerelease }}" "$RUNNER_TEMP/release.json"
curl -sS -f -X POST \
-H "Authorization: Bearer $GH_TOKEN" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
-H "Content-Type: application/json" \
"https://api.github.com/repos/${{ github.repository }}/releases" \
-d @"$RUNNER_TEMP/release.json"