-
Notifications
You must be signed in to change notification settings - Fork 44
538 lines (506 loc) · 26.6 KB
/
Copy pathcc-drift-auto-release.yml
File metadata and controls
538 lines (506 loc) · 26.6 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
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
name: Auto release on version bump
# Three trigger paths into the same release pipeline:
#
# 1. `pull_request: closed` + merged — fast path. Fires both when a
# maintainer hits the merge button manually and when `gh pr merge
# --auto` consummates a queued auto-merge (verified across v4.0.0,
# v4.0.1, v4.1.0 — the auto-merge attributes to the queueing user,
# not GITHUB_TOKEN, so GitHub's loop protection does NOT suppress
# the event). Release ships within a couple of minutes of merge.
#
# 2. `schedule` (hourly) — fallback for the path the fast path can't
# cover: workflow-bot-merged PRs where the merge IS attributed to
# GITHUB_TOKEN (e.g. a `bot/cc-drift-v<X>` PR auto-merged by the
# drafting workflow's own token). GitHub deliberately suppresses
# downstream workflows from GITHUB_TOKEN-attributed events to
# prevent workflow loops; schedule events are system-initiated and
# bypass that suppression. The schedule catches missed releases
# within an hour. v3.32.1 / v3.32.2 (2026-04-29) landed on master
# without npm-publishing because the bot-token merge path silently
# bypassed the pull_request trigger; this fallback exists for that
# case specifically.
#
# 3. `workflow_dispatch` — manual rescue / re-run trigger. Useful when
# you've manually edited the CHANGELOG or version on master without
# going through a PR and want to force the pipeline to re-evaluate.
#
# Idempotency: the `gate` step checks for an existing `v<version>`
# tag and short-circuits the rest of the workflow when found. Every
# trigger path is therefore safe to over-fire; if the pull_request
# path ships a release and the schedule path fires later, the second
# run sees the tag and skips cleanly (no failure email).
#
# Inline-publish: `gh release create` from this workflow uses
# GITHUB_TOKEN, so `release: published` doesn't fire downstream
# workflows here. The build/smoke/tag/release/npm-publish chain
# therefore runs as one workflow run. (The standalone publish.yml that
# listened for release:published was removed in #369 — this inline chain
# is now the ONLY release path, so a manual `gh release create` will
# NOT publish to npm.)
#
# Filename retains `cc-drift-` prefix for git-blame continuity with the
# originally-named workflow; the scope was generalized on 2026-04-23
# after v3.31.8–3.31.11 landed on master without reaching npm (see
# v3.31.11 release notes). The schedule fallback was added on
# 2026-04-29 after v3.32.1 and v3.32.2 both required manual release
# creation because the bot-auto-merge path silently bypassed the
# pull_request trigger.
#
# End-to-end loops covered:
# - CC drift bot: auto-drafter opens `bot/cc-drift-v<X>` PR → bot
# auto-merges when CI green → schedule trigger picks it up within
# the hour → build + release + npm publish + close matching drift
# issues.
# - Manual feature PR with version bump: maintainer merges → fast path
# fires → build + release + npm publish.
# - Any merge / scheduled tick without an unreleased version: gate
# short-circuits, workflow exits in seconds.
on:
pull_request:
types: [closed]
branches: [master]
schedule:
# 15 past each hour, offset from cc-drift-watch's :00 cron so the
# two workflows don't race for the same runner pool. CC patches
# drop ~weekly, so up-to-an-hour fallback latency is fine.
- cron: '15 * * * *'
workflow_dispatch:
permissions:
contents: write
# id-token: write required for `npm publish --provenance` SLSA
# attestation.
id-token: write
# `issues: write` is required for the post-release cleanup step that
# closes any open `cc-drift` issues matching the CC version just
# shipped.
issues: write
# pull-requests: read is needed by the `pr` step to look up the
# merged PR's head ref via `gh pr view` when the workflow is
# triggered by schedule (no `github.event.pull_request` payload).
pull-requests: read
# packages: write is required for the inline docker build/push to
# ghcr.io/askalf/dario at the end of the release pipeline. Same loop-
# protection reasoning as the inline npm publish — `gh release create`
# via GITHUB_TOKEN doesn't fire `release: published`, so the docker
# build/push runs inline here (the standalone docker-publish.yml was
# removed in #369).
packages: write
jobs:
release:
# For pull_request: require `merged == true` (rules out close-
# without-merge). For schedule / workflow_dispatch: always proceed;
# the gate step short-circuits when there's nothing to ship.
if: github.event_name != 'pull_request' || github.event.pull_request.merged == true
runs-on: ubuntu-latest
# Serializes overlapping triggers so a pull_request:closed and a
# schedule tick that fire within seconds of each other don't both
# race past the `gate` step (TOCTOU on `gh release view`) and end
# up with one run failing on `gh release create` because the other
# already created the tag. cancel-in-progress=false because the
# in-flight run is doing real work (build/test/publish); we want
# the second to wait, not abort the first.
concurrency:
group: cc-drift-auto-release-master
cancel-in-progress: false
steps:
- name: Checkout master (post-merge state)
uses: askalf/checkout-with-retry@744195501c3e2b794c50370b753a7b8c93d084f5 # v1.0.0
with:
ref: master
fetch-depth: 2 # need HEAD^1 for the pull_request fast-exit
- name: Read package.json version
id: ver
run: |
set -eo pipefail
CURRENT=$(node -pe "require('./package.json').version")
if ! [[ "$CURRENT" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Refusing to release — package.json version '$CURRENT' doesn't match X.Y.Z" >&2
exit 1
fi
# Fast-exit for pull_request triggers when the merge didn't
# touch package.json's version. Most feature merges fall into
# this case — saving the build/setup steps below keeps the
# workflow under 10s for those runs. For schedule /
# workflow_dispatch we skip this fast-exit; the gate step
# below handles idempotency uniformly via the tag-exists
# check, and we still want to catch unreleased version bumps
# made in commits older than HEAD^1 (e.g., a bot-merged
# version bump followed by an unrelated docs commit on
# master).
if [ "${{ github.event_name }}" = "pull_request" ]; then
PARENT=$(git show HEAD^1:package.json 2>/dev/null \
| node -pe "JSON.parse(require('fs').readFileSync(0,'utf-8')).version" \
|| echo "")
if [ "$CURRENT" = "$PARENT" ]; then
echo "Version unchanged at $CURRENT (pull_request fast-exit)."
echo "needs_release=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "Version bumped on this merge: $PARENT → $CURRENT"
else
echo "Trigger ${{ github.event_name }} — skipping HEAD^1 fast-exit."
fi
echo "version=$CURRENT" >> "$GITHUB_OUTPUT"
echo "needs_release=true" >> "$GITHUB_OUTPUT"
- name: Idempotency gate (release + npm-publish + ghcr check)
id: gate
if: steps.ver.outputs.needs_release == 'true'
# Three-axis check: a release ships only when ALL of the GitHub
# release tag, the npm registry version, AND the ghcr image exist.
# Earlier, narrower gates each let a half-shipped release pass as
# green and become unrecoverable by rerun:
# - tag-only gate skipped npm when a run tagged but failed npm
# publish (v4.8.2 / v4.8.3, 2026-05-19 — releases exist, npm
# latest stayed pinned at 4.8.1).
# - gh+npm gate skipped docker when a run released + npm-published
# but failed the multi-arch docker push, the last/longest leg
# (v4.8.102, 2026-06-30 — release + npm present, ghcr image
# missing, Hetzner auto-deploy stuck at 4.8.101).
#
# Now: emit fine-grained outputs so downstream steps can skip the
# work that's already done and retry the work that isn't:
# - gh_release_exists → "Create GitHub release" skips
# - npm_published → "npm publish" skips
# - ghcr_published → "Build and push (multi-arch)" skips
# - proceed → false ONLY when all three are true (fully shipped)
#
# Queries each registry directly rather than `git rev-parse` against
# the local checkout — the checkout uses `fetch-depth: 2` for HEAD^1
# version-diff and does not fetch tags. gh via `gh release view`;
# npm via `npm view` (tolerates 404 = not published); ghcr via a
# registry v2 manifest probe that FAILS CLOSED (any token/registry
# error → treat as absent → re-push; never skip an unproven push).
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Same github.actor + GITHUB_TOKEN basic-auth the docker/login-
# action step below uses for ghcr; consumed by the manifest probe.
GHCR_ACTOR: ${{ github.actor }}
run: |
TAG="v${{ steps.ver.outputs.version }}"
VERSION="${{ steps.ver.outputs.version }}"
PKG_NAME="$(node -p 'require("./package.json").name')"
# ghcr image path — mirrors `images:` in the docker metadata step
# below; used by the third-axis manifest probe.
IMAGE_PATH="askalf/dario"
if gh release view "$TAG" >/dev/null 2>&1; then
gh_exists=true
else
gh_exists=false
fi
# `npm view <name>@<version> version` prints the version if
# published, exits non-zero with an empty stdout otherwise.
# Wrap with `|| true` so missing packages don't fail the step.
if [ -n "$(npm view "$PKG_NAME@$VERSION" version 2>/dev/null || true)" ]; then
npm_exists=true
else
npm_exists=false
fi
# ── ghcr image existence (third axis) ──
# Exchange GITHUB_TOKEN for a ghcr pull token, then probe the
# registry v2 manifest for this tag. FAILS CLOSED: any curl or
# parse error leaves ghcr_exists=false, so the build/push runs —
# we never skip an unproven push. Closes the hole where a gh+npm-
# only gate let a docker-push failure (v4.8.102) go green with no
# image and stay unrecoverable on rerun.
ghcr_exists=false
if curl -fsS -u "$GHCR_ACTOR:$GH_TOKEN" \
"https://ghcr.io/token?service=ghcr.io&scope=repository:${IMAGE_PATH}:pull" \
-o ghcr-token.json 2>/dev/null; then
GHCR_PULL_TOKEN="$(node -pe 'JSON.parse(require("fs").readFileSync(process.argv[1],"utf-8")).token || ""' ghcr-token.json 2>/dev/null || true)"
if [ -n "$GHCR_PULL_TOKEN" ]; then
MANIFEST_CODE="$(curl -s -o /dev/null -w '%{http_code}' \
-H "Authorization: Bearer $GHCR_PULL_TOKEN" \
-H "Accept: application/vnd.oci.image.index.v1+json" \
-H "Accept: application/vnd.docker.distribution.manifest.list.v2+json" \
-H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
"https://ghcr.io/v2/${IMAGE_PATH}/manifests/${TAG}" 2>/dev/null || echo 000)"
if [ "$MANIFEST_CODE" = "200" ]; then
ghcr_exists=true
fi
echo "ghcr manifest probe for ${TAG}: HTTP ${MANIFEST_CODE} (ghcr_exists=${ghcr_exists})"
else
echo "ghcr token exchange returned empty token — treating image as absent (fail-closed)."
fi
else
echo "ghcr token exchange failed — treating image as absent (fail-closed)."
fi
{
echo "gh_release_exists=$gh_exists"
echo "npm_published=$npm_exists"
echo "ghcr_published=$ghcr_exists"
} >> "$GITHUB_OUTPUT"
if [ "$gh_exists" = "true" ] && [ "$npm_exists" = "true" ] && [ "$ghcr_exists" = "true" ]; then
echo "Release $TAG fully shipped (GitHub + npm + ghcr) — idempotent skip."
echo "proceed=false" >> "$GITHUB_OUTPUT"
else
echo "Release $TAG incomplete (gh=$gh_exists, npm=$npm_exists, ghcr=$ghcr_exists) — proceeding to fill the gaps."
echo "proceed=true" >> "$GITHUB_OUTPUT"
fi
- name: Resolve merged PR context
id: pr
if: steps.gate.outputs.proceed == 'true'
# Identify the PR whose merge bumped the version, regardless of
# how this workflow fired. For pull_request events the payload
# gives us number + head ref directly. For schedule /
# workflow_dispatch we parse the HEAD commit message for `(#N)`
# (squash-merge convention on master) and resolve via
# `gh pr view N --json url,headRefName`. Outputs (all may be
# empty if the PR can't be derived):
# - pr_number PR number
# - pr_url PR HTML url
# - head_ref PR head branch (UNTRUSTED — pass via env in
# consumers, never inline-interpolate)
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Pull-request payload fields are surfaced as env vars (not
# inline-interpolated) — head.ref is attacker-controlled
# (PR branch name) and inline `${{ }}` substitution at
# shell-parse time would allow injection. Number / url are
# server-generated and lower-risk, but kept consistent.
EVENT_NAME: ${{ github.event_name }}
EVENT_PR_NUMBER: ${{ github.event.pull_request.number }}
EVENT_PR_URL: ${{ github.event.pull_request.html_url }}
EVENT_PR_HEAD_REF: ${{ github.event.pull_request.head.ref }}
run: |
set -eo pipefail
if [ "$EVENT_NAME" = "pull_request" ]; then
PR_NUMBER="$EVENT_PR_NUMBER"
PR_URL="$EVENT_PR_URL"
PR_HEAD_REF="$EVENT_PR_HEAD_REF"
else
COMMIT_MSG=$(git log -1 --pretty=%s HEAD)
echo "HEAD commit subject: $COMMIT_MSG"
PR_NUMBER=$(echo "$COMMIT_MSG" | sed -nE 's/.*\(#([0-9]+)\)$/\1/p')
if [ -z "$PR_NUMBER" ]; then
echo "No (#N) suffix on HEAD commit — proceeding without PR context."
PR_URL=""
PR_HEAD_REF=""
else
PR_INFO=$(gh pr view "$PR_NUMBER" --json url,headRefName 2>/dev/null || echo "{}")
PR_URL=$(echo "$PR_INFO" | node -pe "JSON.parse(require('fs').readFileSync(0,'utf-8')).url || ''")
PR_HEAD_REF=$(echo "$PR_INFO" | node -pe "JSON.parse(require('fs').readFileSync(0,'utf-8')).headRefName || ''")
fi
fi
echo "Resolved PR: number=$PR_NUMBER head_ref=$PR_HEAD_REF"
{
echo "pr_number=$PR_NUMBER"
echo "pr_url=$PR_URL"
echo "head_ref=$PR_HEAD_REF"
} >> "$GITHUB_OUTPUT"
# ─── Build + smoke pipeline (was publish.yml, removed in #369) ──
# We do this BEFORE cutting the GitHub release, so a failing
# build/smoke aborts everything — no release, no npm publish, no
# half-shipped state.
- name: Set up Node
if: steps.gate.outputs.proceed == 'true'
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22
registry-url: https://registry.npmjs.org
- name: npm ci
if: steps.gate.outputs.proceed == 'true'
run: npm ci
- name: build
if: steps.gate.outputs.proceed == 'true'
run: npm run build
- name: --help smoke
if: steps.gate.outputs.proceed == 'true'
run: node dist/cli.js help
# ─── Release + publish ─────────────────────────────────────────
- name: Extract release notes from CHANGELOG
id: notes
if: steps.gate.outputs.proceed == 'true'
# Pull the section for this version from CHANGELOG.md so the
# GitHub release body shows what was in the bump, not a generic
# "see changelog" link.
#
# The extraction logic lives in `scripts/extract-release-notes.mjs`
# with full unit-test coverage in `test/extract-release-notes.mjs`
# — a future regression to the empty-capture state breaks CI
# before it ships. Pre-refactor history (inline `node -e` regex
# with /m flag that silently captured the empty string for 39
# consecutive releases) is documented in the script's header
# and on PR #276.
run: |
set -eo pipefail
VERSION="${{ steps.ver.outputs.version }}"
node scripts/extract-release-notes.mjs "$VERSION" --file CHANGELOG.md > release-notes.md
echo "--- release-notes.md ---"
cat release-notes.md
- name: Create GitHub release
# Skip if the GH release tag already exists — a prior run created
# it but failed downstream (npm or ghcr). The gate left proceed=true
# so the remaining publish steps retry; this one is already done.
if: steps.gate.outputs.proceed == 'true' && steps.gate.outputs.gh_release_exists != 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ steps.pr.outputs.pr_number }}
PR_URL: ${{ steps.pr.outputs.pr_url }}
TRIGGER: ${{ github.event_name }}
run: |
TAG="v${{ steps.ver.outputs.version }}"
{
if [ -n "$PR_NUMBER" ] && [ -n "$PR_URL" ]; then
echo "Auto-released from merge of [PR #${PR_NUMBER}](${PR_URL}) (trigger: ${TRIGGER})."
else
echo "Auto-released from master HEAD (trigger: ${TRIGGER}; no PR context resolved)."
fi
echo ""
cat release-notes.md
echo ""
echo "---"
echo ""
echo "Built + tested + npm-published inline by \`cc-drift-auto-release.yml\` on this run. See the workflow log for the provenance-attested publish output."
} > body.md
gh release create "$TAG" \
--title "$TAG" \
--notes-file body.md
- name: npm publish
# Skip if npm already has this version. Lets a re-run target only
# the missing publish (e.g. retrying after a transient npm 5xx)
# without re-publishing what's already on the registry.
if: steps.gate.outputs.proceed == 'true' && steps.gate.outputs.npm_published != 'true'
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npm publish --access public --provenance
# ─── Inline Docker publish to GHCR ─────────────────────────────
# Same loop-protection reason as the inline npm publish above:
# `gh release create` via GITHUB_TOKEN doesn't fire
# `release: published`. The standalone docker-publish.yml was
# removed in #369; its build/push steps live inline here.
- name: Set up QEMU
if: steps.gate.outputs.proceed == 'true'
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
- name: Set up Docker Buildx
if: steps.gate.outputs.proceed == 'true'
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Log in to GHCR
if: steps.gate.outputs.proceed == 'true'
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Generate Docker tag matrix
if: steps.gate.outputs.proceed == 'true'
id: docker_meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: ghcr.io/askalf/dario
tags: |
type=raw,value=v${{ steps.ver.outputs.version }}
type=raw,value=latest
type=match,pattern=v\d+\.\d+,value=v${{ steps.ver.outputs.version }}
type=match,pattern=v\d+,value=v${{ steps.ver.outputs.version }}
- name: Build and push (multi-arch)
# Skip when the image for this tag is already in ghcr — lets a rerun
# that's only missing gh/npm avoid re-pushing an image that already
# landed. Mirrors the per-axis skips on "Create GitHub release" and
# "npm publish" above.
if: steps.gate.outputs.proceed == 'true' && steps.gate.outputs.ghcr_published != 'true'
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
provenance: true
sbom: true
tags: ${{ steps.docker_meta.outputs.tags }}
labels: ${{ steps.docker_meta.outputs.labels }}
cache-from: type=gha
# ignore-error: the gha cache export is best-effort and must NOT fail
# the release. Two distinct failures have hit this, both guarded by the
# single ignore-error below:
# 1. Cache backend "failed to reserve cache" (quota/contention) — bit
# v4.8.102 and v4.8.103 deterministically (survived `gh run rerun`),
# leaving ghcr without the image. #594 made the gate retry the push;
# this stops the push itself dying on the cache.
# 2. A merged bot-rebake PR fires `pull_request: closed`, and on the
# pull_request path GitHub issues a READ-ONLY Actions cache token, so
# the cache *export* is denied ("token has no writable scopes") and
# buildkit treats that as fatal — failing the whole multi-arch push
# even though the build is fine. The schedule path gets a writable
# token and still warms the cache; cache-from reads work on both.
# ignore-error degrades the export to a warning in both cases.
cache-to: type=gha,mode=max,ignore-error=true
- name: Close matching cc-drift issues
if: |
steps.gate.outputs.proceed == 'true' &&
startsWith(steps.pr.outputs.head_ref, 'bot/cc-drift-')
# The bot's branch name encodes the CC version (`bot/cc-drift-
# v2.1.120`). The watcher's hourly run opens issues with titles
# `CC drift detected: v<version>` and `CC authorize-probe drift:
# v<version>` for the same version. Once the release ships,
# those issues are resolved — close them so the tracker shows
# the loop closed cleanly. Idempotent: `gh issue close` on a
# closed issue is a no-op that exits 0.
#
# VERSION-INCLUSIVE (issue #592 orphan): close every open cc-drift
# issue whose title version is <= the CC version just shipped, not
# only the exact match. Two orphan paths the exact match left open:
# 1. A release run that dies before reaching this step — v4.8.102
# (bot/cc-drift-v2.1.196) failed at the docker-push leg, so the
# maxTested bump merged but "CC drift detected: v2.1.196" stayed
# open forever.
# 2. Version skips — the later v2.1.197/v2.1.198 drift releases each
# closed only their own title and walked straight past the
# stranded v2.1.196 issue.
# With the inclusive sweep, the next successful drift release (or a
# gate-retry rerun of the same version) self-heals all predecessors:
# maxTested is monotonic, so a shipped v(N) proves every v(<=N) drift
# is resolved. `sort -V` does the version comparison.
#
# Only runs for bot-drift PRs; manual-feature PRs skip this
# step (no bot-tracked issues to close).
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# head_ref via env rather than inline ${{ }} interpolation —
# the branch name is attacker-controlled and inline
# substitution at shell-parse time would allow script
# injection via a PR branch named e.g.
# `bot/cc-drift-v1.0.0$(malicious)`. The step `if:` restricts
# to bot/cc-drift-* by prefix, but `startsWith()` doesn't
# reject trailing shell metacharacters.
PR_HEAD_REF: ${{ steps.pr.outputs.head_ref }}
run: |
BRANCH="$PR_HEAD_REF"
if [[ "$BRANCH" != bot/cc-drift-v* ]]; then
echo "Branch $BRANCH doesn't match expected shape; skipping issue cleanup."
exit 0
fi
CC_VERSION="${BRANCH#bot/cc-drift-v}"
DARIO_VERSION="${{ steps.ver.outputs.version }}"
gh issue list --label cc-drift --state open --limit 100 \
--json number,title --jq '.[] | "\(.number)\t\(.title)"' \
| while IFS="$(printf '\t')" read -r n title; do
# Extract the version from the two watcher title shapes; skip
# anything else under the label (hand-filed issues etc.).
v=$(printf '%s' "$title" | sed -nE 's/^(CC drift detected|CC authorize-probe drift): v([0-9.]+)$/\2/p')
if [ -z "$v" ]; then
echo "Skipping #$n (unrecognized title shape): $title"
continue
fi
# v <= CC_VERSION iff CC_VERSION sorts last (or equal) under -V.
if [ "$(printf '%s\n%s\n' "$v" "$CC_VERSION" | sort -V | tail -1)" = "$CC_VERSION" ]; then
gh issue close "$n" --comment "Resolved by v${DARIO_VERSION} (release: https://github.com/${GITHUB_REPOSITORY}/releases/tag/v${DARIO_VERSION}) — shipped CC v${CC_VERSION} supersedes this issue's v${v}. Closed automatically by \`cc-drift-auto-release.yml\`."
echo "Closed issue #$n ($title — v${v} <= shipped v${CC_VERSION})"
else
echo "Leaving #$n open ($title — v${v} is NEWER than shipped v${CC_VERSION})"
fi
done
- name: Post-release summary
if: steps.gate.outputs.proceed == 'true'
env:
PR_HEAD_REF: ${{ steps.pr.outputs.head_ref }}
PR_NUMBER: ${{ steps.pr.outputs.pr_number }}
run: |
echo "✓ Released v${{ steps.ver.outputs.version }}"
if [ -n "$PR_NUMBER" ]; then
echo "✓ Triggered by ${{ github.event_name }} (PR #$PR_NUMBER, head: $PR_HEAD_REF)"
else
echo "✓ Triggered by ${{ github.event_name }} (no PR context resolved)"
fi
echo "✓ Published to npm (with SLSA provenance)"
if [[ "$PR_HEAD_REF" == bot/cc-drift-v* ]]; then
echo "→ matching cc-drift issues were closed"
fi