@1aboveio/skills 0.20.1 → 0.20.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/runtime/skills/distribution/generated/recipes.json +17 -17
- package/runtime/skills/engineering/engineering-runtime/scripts/workflow-policy.mjs +1 -1
- package/skills/cicd-pipeline/cloud-build/SKILL.md +6 -6
- package/skills/engineering/engineering-runtime/coherence/workflow.json +15 -15
- package/skills/engineering/engineering-runtime/scripts/workflow-policy.mjs +1 -1
- package/skills/engineering/rush-release/SKILL.md +46 -25
- package/skills/engineering/rush-release/evals/evals.json +22 -8
- package/skills/engineering/rush-release/references/cut.md +21 -4
- package/skills/engineering/rush-release/references/preflight.md +25 -6
- package/skills/engineering/rush-release/references/promotion.md +70 -0
- package/skills/engineering/rush-release/references/publish.md +49 -16
- package/skills/engineering/rush-release/scripts/plan.mjs +33 -7
- package/skills/engineering/smoke/SKILL.md +4 -4
- package/skills/engineering/smoke/references/manifest.md +3 -3
package/package.json
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"id": "first-party",
|
|
25
25
|
"type": "first-party",
|
|
26
26
|
"package": "@1aboveio/skills",
|
|
27
|
-
"version": "0.20.
|
|
27
|
+
"version": "0.20.2",
|
|
28
28
|
"updatePolicy": "pinned-npm-version",
|
|
29
29
|
"members": [
|
|
30
30
|
{
|
|
@@ -218,7 +218,7 @@
|
|
|
218
218
|
"sourceId": "first-party",
|
|
219
219
|
"sourceType": "first-party",
|
|
220
220
|
"package": "@1aboveio/skills",
|
|
221
|
-
"version": "0.20.
|
|
221
|
+
"version": "0.20.2",
|
|
222
222
|
"installPath": null,
|
|
223
223
|
"members": [
|
|
224
224
|
"harness-runtime",
|
|
@@ -252,7 +252,7 @@
|
|
|
252
252
|
"sourceId": "first-party",
|
|
253
253
|
"sourceType": "first-party",
|
|
254
254
|
"package": "@1aboveio/skills",
|
|
255
|
-
"version": "0.20.
|
|
255
|
+
"version": "0.20.2",
|
|
256
256
|
"installPath": null,
|
|
257
257
|
"members": [
|
|
258
258
|
"harness-runtime",
|
|
@@ -296,7 +296,7 @@
|
|
|
296
296
|
"sourceId": "first-party",
|
|
297
297
|
"sourceType": "first-party",
|
|
298
298
|
"package": "@1aboveio/skills",
|
|
299
|
-
"version": "0.20.
|
|
299
|
+
"version": "0.20.2",
|
|
300
300
|
"installPath": null,
|
|
301
301
|
"members": [
|
|
302
302
|
"harness-runtime",
|
|
@@ -329,7 +329,7 @@
|
|
|
329
329
|
"sourceId": "first-party",
|
|
330
330
|
"sourceType": "first-party",
|
|
331
331
|
"package": "@1aboveio/skills",
|
|
332
|
-
"version": "0.20.
|
|
332
|
+
"version": "0.20.2",
|
|
333
333
|
"installPath": null,
|
|
334
334
|
"members": [
|
|
335
335
|
"harness-runtime",
|
|
@@ -372,7 +372,7 @@
|
|
|
372
372
|
"sourceId": "first-party",
|
|
373
373
|
"sourceType": "first-party",
|
|
374
374
|
"package": "@1aboveio/skills",
|
|
375
|
-
"version": "0.20.
|
|
375
|
+
"version": "0.20.2",
|
|
376
376
|
"installPath": null,
|
|
377
377
|
"members": [
|
|
378
378
|
"harness-runtime",
|
|
@@ -400,7 +400,7 @@
|
|
|
400
400
|
"sourceId": "first-party",
|
|
401
401
|
"sourceType": "first-party",
|
|
402
402
|
"package": "@1aboveio/skills",
|
|
403
|
-
"version": "0.20.
|
|
403
|
+
"version": "0.20.2",
|
|
404
404
|
"installPath": null,
|
|
405
405
|
"members": [
|
|
406
406
|
"harness-runtime",
|
|
@@ -438,7 +438,7 @@
|
|
|
438
438
|
"sourceId": "first-party",
|
|
439
439
|
"sourceType": "first-party",
|
|
440
440
|
"package": "@1aboveio/skills",
|
|
441
|
-
"version": "0.20.
|
|
441
|
+
"version": "0.20.2",
|
|
442
442
|
"installPath": null,
|
|
443
443
|
"members": [
|
|
444
444
|
"harness-runtime",
|
|
@@ -466,7 +466,7 @@
|
|
|
466
466
|
"sourceId": "first-party",
|
|
467
467
|
"sourceType": "first-party",
|
|
468
468
|
"package": "@1aboveio/skills",
|
|
469
|
-
"version": "0.20.
|
|
469
|
+
"version": "0.20.2",
|
|
470
470
|
"installPath": null,
|
|
471
471
|
"members": [
|
|
472
472
|
"harness-runtime",
|
|
@@ -504,7 +504,7 @@
|
|
|
504
504
|
"sourceId": "first-party",
|
|
505
505
|
"sourceType": "first-party",
|
|
506
506
|
"package": "@1aboveio/skills",
|
|
507
|
-
"version": "0.20.
|
|
507
|
+
"version": "0.20.2",
|
|
508
508
|
"installPath": null,
|
|
509
509
|
"members": [
|
|
510
510
|
"harness-runtime",
|
|
@@ -533,7 +533,7 @@
|
|
|
533
533
|
"sourceId": "first-party",
|
|
534
534
|
"sourceType": "first-party",
|
|
535
535
|
"package": "@1aboveio/skills",
|
|
536
|
-
"version": "0.20.
|
|
536
|
+
"version": "0.20.2",
|
|
537
537
|
"installPath": null,
|
|
538
538
|
"members": [
|
|
539
539
|
"harness-runtime",
|
|
@@ -572,7 +572,7 @@
|
|
|
572
572
|
"sourceId": "first-party",
|
|
573
573
|
"sourceType": "first-party",
|
|
574
574
|
"package": "@1aboveio/skills",
|
|
575
|
-
"version": "0.20.
|
|
575
|
+
"version": "0.20.2",
|
|
576
576
|
"installPath": null,
|
|
577
577
|
"members": [
|
|
578
578
|
"harness-runtime",
|
|
@@ -604,7 +604,7 @@
|
|
|
604
604
|
"sourceId": "first-party",
|
|
605
605
|
"sourceType": "first-party",
|
|
606
606
|
"package": "@1aboveio/skills",
|
|
607
|
-
"version": "0.20.
|
|
607
|
+
"version": "0.20.2",
|
|
608
608
|
"installPath": null,
|
|
609
609
|
"members": [
|
|
610
610
|
"harness-runtime",
|
|
@@ -644,7 +644,7 @@
|
|
|
644
644
|
"sourceId": "first-party",
|
|
645
645
|
"sourceType": "first-party",
|
|
646
646
|
"package": "@1aboveio/skills",
|
|
647
|
-
"version": "0.20.
|
|
647
|
+
"version": "0.20.2",
|
|
648
648
|
"installPath": null,
|
|
649
649
|
"members": [
|
|
650
650
|
"harness-runtime"
|
|
@@ -670,7 +670,7 @@
|
|
|
670
670
|
"sourceId": "first-party",
|
|
671
671
|
"sourceType": "first-party",
|
|
672
672
|
"package": "@1aboveio/skills",
|
|
673
|
-
"version": "0.20.
|
|
673
|
+
"version": "0.20.2",
|
|
674
674
|
"installPath": null,
|
|
675
675
|
"members": [
|
|
676
676
|
"harness-runtime"
|
|
@@ -702,7 +702,7 @@
|
|
|
702
702
|
"sourceId": "first-party",
|
|
703
703
|
"sourceType": "first-party",
|
|
704
704
|
"package": "@1aboveio/skills",
|
|
705
|
-
"version": "0.20.
|
|
705
|
+
"version": "0.20.2",
|
|
706
706
|
"installPath": null,
|
|
707
707
|
"members": [
|
|
708
708
|
"harness-runtime",
|
|
@@ -756,7 +756,7 @@
|
|
|
756
756
|
"sourceId": "first-party",
|
|
757
757
|
"sourceType": "first-party",
|
|
758
758
|
"package": "@1aboveio/skills",
|
|
759
|
-
"version": "0.20.
|
|
759
|
+
"version": "0.20.2",
|
|
760
760
|
"installPath": null,
|
|
761
761
|
"members": [
|
|
762
762
|
"harness-runtime",
|
|
@@ -361,7 +361,7 @@ gcloud builds triggers create github --name="{service}-prod" \
|
|
|
361
361
|
|
|
362
362
|
So under GitHub Flow the SHA-keyed staging verdict is a **pre-filter, not the release gate**. The gate is the prod candidate at 0% traffic, which validates the *exact shipped SHA* on real infrastructure before any user reaches it. That is sound precisely because of the 0%-candidate design — under a deploy-then-smoke pipeline the same topology would ship unvalidated commits straight to users.
|
|
363
363
|
|
|
364
|
-
**Order is forced: merge first, then tag.**
|
|
364
|
+
**Order is forced: merge first, then tag.** The release tag must name a SHA on the production branch, so the release branch cannot be rc-tagged before it lands on `main`. Merge the release PR, then mint the rc on the merge commit. `rush-release` owns this sequencing.
|
|
365
365
|
|
|
366
366
|
## Build Notifications
|
|
367
367
|
|
|
@@ -377,8 +377,8 @@ A "deploy SUCCESS" only proves the revision is `Ready` — not that the real bui
|
|
|
377
377
|
|
|
378
378
|
**Where it blocks depends on who the environment serves.** The blocking placement protects *users*, so it belongs on the **production** path. An integration environment (`dev`) has no users to protect — there, a blocking smoke only converts small bugs and infra flakes into failed builds and full rebuild cycles. So the same seam cuts differently per fork, and each fork's contract output is different:
|
|
379
379
|
|
|
380
|
-
- **`dev` output: a SHA-keyed verdict.** Deploy at 100%, smoke as a non-blocking recorder, publish the verdict as a commit status.
|
|
381
|
-
- **prod output: a 0%-traffic candidate revision.** Deploy with `--no-traffic`, surface the revision name and candidate URL, and **stop**. The build does not smoke it and does not shift traffic — `
|
|
380
|
+
- **`dev` output: a SHA-keyed verdict.** Deploy at 100%, smoke as a non-blocking recorder, and publish the verdict as a commit status. When configured as required CI, it helps `rush-release` select the newest green trunk SHA; the production candidate smoke remains the definitive release gate.
|
|
381
|
+
- **prod output: a 0%-traffic candidate revision.** Deploy with `--no-traffic`, surface the revision name and candidate URL, and **stop**. The build does not smoke it and does not shift traffic — `rush-release` does both.
|
|
382
382
|
|
|
383
383
|
**Why the prod gate moved out of the build.** The gate did not disappear; it moved to where it can be operated:
|
|
384
384
|
|
|
@@ -397,7 +397,7 @@ The prod fork's entire contract is three facts:
|
|
|
397
397
|
```
|
|
398
398
|
tag v*-rc.* → build → deploy --no-traffic --tag candidate → publish {revision, url} → END
|
|
399
399
|
│
|
|
400
|
-
|
|
400
|
+
rush-release: smoke --profile prod-preview (read-only) ──┤
|
|
401
401
|
ALIVE → update-traffic 100%, tag v<version>
|
|
402
402
|
DEAD → delete the 0% revision (traffic never moved)
|
|
403
403
|
```
|
|
@@ -468,7 +468,7 @@ Because traffic never moved, a DEAD verdict is not a rollback and has no user im
|
|
|
468
468
|
cat /workspace/candidate.json
|
|
469
469
|
```
|
|
470
470
|
|
|
471
|
-
**The `candidate` tag moves; the revision name does not.** Each prod build re-points `--tag=candidate` at its own new revision, so the tag URL always means "the newest candidate," not "the candidate I built." That is why the published JSON carries the **revision name** and why the release lane deletes a failed candidate **by name
|
|
471
|
+
**The `candidate` tag moves; the revision name does not.** Each prod build re-points `--tag=candidate` at its own new revision, so the tag URL always means "the newest candidate," not "the candidate I built." That is why the published JSON carries the **revision name** and why the release lane deletes a failed candidate **by name**. `rush-release` therefore refuses a second release in flight: its moving URL would silently point the earlier release at the later build.
|
|
472
472
|
|
|
473
473
|
**Surface the coordinates as a GCS object keyed by the RC tag, not as log output.** The release lane already knows the tag it pushed, so it can fetch `candidates/{service}-<rc-tag>.json` without knowing the build ID — no build lookup, no log scraping, and the object outlives log retention. The `cat` at the end is a convenience for humans reading the build, not the interface. Derive the URL from `gcloud run services describe` rather than from `gcloud run deploy` output: the tag URL lives in `status.traffic[]`, and describing the service after the deploy also proves the tag actually landed (the guard above fails the build if it did not).
|
|
474
474
|
|
|
@@ -535,7 +535,7 @@ availableSecrets:
|
|
|
535
535
|
```
|
|
536
536
|
|
|
537
537
|
- After traffic shifts, **confirm the serving revision is running this commit's image**, then run smoke against the live service URL. Capture the exit code instead of letting it fail the step. Compare **images, not revision names** — Cloud Run names revisions from its own counter, so a name-based check silently never matches and degrades to no check at all. On a mismatch, publish `failure` and **run no smoke**: traffic is on older code, so any verdict it produced would be a green light keyed to a SHA that never served.
|
|
538
|
-
- **Publish the verdict as a commit status on the SHA** (context `dev-smoke`, state `success`/`failure`, description naming the broken path on DEAD).
|
|
538
|
+
- **Publish the verdict as a commit status on the SHA** (context `dev-smoke`, state `success`/`failure`, description naming the broken path on DEAD). When branch protection requires that status, it is part of the green CI set from which `rush-release` selects its trunk SHA. The image for a SHA is immutable, so the recorded verdict stays useful after `dev` moves on; production still gets its own `prod-preview` gate.
|
|
539
539
|
- The step then **exits 0 regardless of verdict**: a DEAD verdict marks the SHA unpromotable; it must not block the next dev iteration.
|
|
540
540
|
- **Non-blocking must not mean silent.** On DEAD, still notify (a Pub/Sub message from the step, or a webhook on the `dev-smoke` status change) so a broken dev is known in minutes, not discovered at release time.
|
|
541
541
|
|
|
@@ -342,7 +342,7 @@
|
|
|
342
342
|
"id": "first-party",
|
|
343
343
|
"type": "first-party",
|
|
344
344
|
"package": "@1aboveio/skills",
|
|
345
|
-
"version": "0.20.
|
|
345
|
+
"version": "0.20.2"
|
|
346
346
|
},
|
|
347
347
|
"contentDigest": "eff6c7b5931bce5b2265a619bccddd371a89df6ce7bc2edc74f11d00060a71dd",
|
|
348
348
|
"digestExcludes": []
|
|
@@ -355,7 +355,7 @@
|
|
|
355
355
|
"id": "first-party",
|
|
356
356
|
"type": "first-party",
|
|
357
357
|
"package": "@1aboveio/skills",
|
|
358
|
-
"version": "0.20.
|
|
358
|
+
"version": "0.20.2"
|
|
359
359
|
},
|
|
360
360
|
"contentDigest": "d97abeff7bdc5ae2e4be2f631d09dba46d4753ce1e67334793364329af59f066",
|
|
361
361
|
"digestExcludes": []
|
|
@@ -368,7 +368,7 @@
|
|
|
368
368
|
"id": "first-party",
|
|
369
369
|
"type": "first-party",
|
|
370
370
|
"package": "@1aboveio/skills",
|
|
371
|
-
"version": "0.20.
|
|
371
|
+
"version": "0.20.2"
|
|
372
372
|
},
|
|
373
373
|
"contentDigest": "00cfdd3e5bfbc3e37b1369ad93aae91161e671829760a8867ea9be25047ba461",
|
|
374
374
|
"digestExcludes": []
|
|
@@ -381,7 +381,7 @@
|
|
|
381
381
|
"id": "first-party",
|
|
382
382
|
"type": "first-party",
|
|
383
383
|
"package": "@1aboveio/skills",
|
|
384
|
-
"version": "0.20.
|
|
384
|
+
"version": "0.20.2"
|
|
385
385
|
},
|
|
386
386
|
"contentDigest": "58b0556228a9271cf9e33727a05fc08a4fdfd29512ec0936b169f0a55d60e6ef",
|
|
387
387
|
"digestExcludes": []
|
|
@@ -394,7 +394,7 @@
|
|
|
394
394
|
"id": "first-party",
|
|
395
395
|
"type": "first-party",
|
|
396
396
|
"package": "@1aboveio/skills",
|
|
397
|
-
"version": "0.20.
|
|
397
|
+
"version": "0.20.2"
|
|
398
398
|
},
|
|
399
399
|
"contentDigest": "4664873230f22de2fcf46b7b529570cfedabdc850620031d8f96baf89fcdc5b4",
|
|
400
400
|
"digestExcludes": []
|
|
@@ -407,9 +407,9 @@
|
|
|
407
407
|
"id": "first-party",
|
|
408
408
|
"type": "first-party",
|
|
409
409
|
"package": "@1aboveio/skills",
|
|
410
|
-
"version": "0.20.
|
|
410
|
+
"version": "0.20.2"
|
|
411
411
|
},
|
|
412
|
-
"contentDigest": "
|
|
412
|
+
"contentDigest": "cd9f398f6d8d1df7c2d944d3bec014e788535638014f327783f074757bfb4f49",
|
|
413
413
|
"digestExcludes": []
|
|
414
414
|
},
|
|
415
415
|
{
|
|
@@ -420,7 +420,7 @@
|
|
|
420
420
|
"id": "first-party",
|
|
421
421
|
"type": "first-party",
|
|
422
422
|
"package": "@1aboveio/skills",
|
|
423
|
-
"version": "0.20.
|
|
423
|
+
"version": "0.20.2"
|
|
424
424
|
},
|
|
425
425
|
"contentDigest": "f048fd00c69f2dc666fc7a3096cfeee6dfba73933bfb69602f3f0e26159798cc",
|
|
426
426
|
"digestExcludes": []
|
|
@@ -433,9 +433,9 @@
|
|
|
433
433
|
"id": "first-party",
|
|
434
434
|
"type": "first-party",
|
|
435
435
|
"package": "@1aboveio/skills",
|
|
436
|
-
"version": "0.20.
|
|
436
|
+
"version": "0.20.2"
|
|
437
437
|
},
|
|
438
|
-
"contentDigest": "
|
|
438
|
+
"contentDigest": "d533684db4483cfc90be2aa7161980b6548057ef18a4b54f0cdbcd53c79dd46d",
|
|
439
439
|
"digestExcludes": []
|
|
440
440
|
},
|
|
441
441
|
{
|
|
@@ -446,15 +446,15 @@
|
|
|
446
446
|
"id": "first-party",
|
|
447
447
|
"type": "first-party",
|
|
448
448
|
"package": "@1aboveio/skills",
|
|
449
|
-
"version": "0.20.
|
|
449
|
+
"version": "0.20.2"
|
|
450
450
|
},
|
|
451
|
-
"contentDigest": "
|
|
451
|
+
"contentDigest": "f7f56f7e33b2b12c3043df0f4729447dfccd181daa79936ff62da38df91d5175",
|
|
452
452
|
"digestExcludes": [
|
|
453
453
|
"coherence/workflow.json"
|
|
454
454
|
]
|
|
455
455
|
}
|
|
456
456
|
],
|
|
457
|
-
"releaseIdentity": "
|
|
457
|
+
"releaseIdentity": "455424f618fddc62d553f7c89282887d6b449b278eb0335b72bf415f760f1b34",
|
|
458
458
|
"lifecycleAuthority": "native-skills-cli",
|
|
459
459
|
"repairRecipe": {
|
|
460
460
|
"id": "engineering-workflow-dependency-first",
|
|
@@ -512,7 +512,7 @@
|
|
|
512
512
|
"sourceId": "first-party",
|
|
513
513
|
"sourceType": "first-party",
|
|
514
514
|
"package": "@1aboveio/skills",
|
|
515
|
-
"version": "0.20.
|
|
515
|
+
"version": "0.20.2",
|
|
516
516
|
"installPath": null,
|
|
517
517
|
"members": [
|
|
518
518
|
"harness-runtime",
|
|
@@ -528,7 +528,7 @@
|
|
|
528
528
|
"commands": [
|
|
529
529
|
{
|
|
530
530
|
"transport": "npm",
|
|
531
|
-
"command": "npx @1aboveio/skills@0.20.
|
|
531
|
+
"command": "npx @1aboveio/skills@0.20.2 install --group engineering-workflow --yes"
|
|
532
532
|
}
|
|
533
533
|
],
|
|
534
534
|
"onFailure": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: rush-release
|
|
3
|
-
description: "Slash/explicit-only (/rush-release). Cut a GitHub Flow release from main: freeze the latest CI-green trunk SHA, write the changelog, choose SemVer, merge the metadata cut through a verified PR,
|
|
3
|
+
description: "Slash/explicit-only (/rush-release). Cut a GitHub Flow release from main: freeze the latest CI-green trunk SHA, write the changelog, choose SemVer, merge the metadata cut through a verified PR, publish npm artifacts, or validate a Cloud Run candidate at 0% before shifting it to 100%. One complete-plan confirmation covers the conditional release. Do not auto-select. Use only on explicit user/orchestrator invoke. NOT for GitFlow/release-branch promotion and NOT for implementing a spec (rush-issues)."
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -10,9 +10,10 @@ Cut a **GitHub Flow** release from **main**. Freeze one already-green trunk
|
|
|
10
10
|
SHA, write a metadata-only version cut on that parent, merge it through a
|
|
11
11
|
verified PR, tag the exact merged SHA on `main`, and watch the publisher.
|
|
12
12
|
|
|
13
|
-
The orchestrator sequences local programs and one confirmation
|
|
14
|
-
assemble a release branch
|
|
15
|
-
|
|
13
|
+
The orchestrator sequences local programs and exactly one user confirmation per
|
|
14
|
+
release plan. It does not assemble a long-lived release branch. For Cloud Run,
|
|
15
|
+
it owns one bounded candidate-at-0% -> validate -> shift-to-100% -> verify-live
|
|
16
|
+
sequence; the confirmation conditionally authorizes that whole sequence.
|
|
16
17
|
|
|
17
18
|
```text
|
|
18
19
|
preflight (main + publisher)
|
|
@@ -21,8 +22,9 @@ preflight (main + publisher)
|
|
|
21
22
|
-> confirm
|
|
22
23
|
-> bump version files + commit the cut
|
|
23
24
|
-> merge metadata PR + verify merged main SHA
|
|
24
|
-
->
|
|
25
|
-
->
|
|
25
|
+
-> npm: final tag -> watch publish
|
|
26
|
+
-> Cloud Run: rc tag -> candidate at 0% -> validate -> shift to 100%
|
|
27
|
+
-> verify live -> final tag
|
|
26
28
|
```
|
|
27
29
|
|
|
28
30
|
This file is the workflow and index. Read the linked procedure immediately
|
|
@@ -33,7 +35,8 @@ before its stage; do not invent an unlinked substitute.
|
|
|
33
35
|
| Trunk, publisher, version files | [references/preflight.md](references/preflight.md) |
|
|
34
36
|
| Latest CI-green HEAD | [references/candidate.md](references/candidate.md) |
|
|
35
37
|
| Changelog, SemVer, file bump, commit | [references/cut.md](references/cut.md) |
|
|
36
|
-
| Tag, push, watch
|
|
38
|
+
| Tag, push, watch publisher | [references/publish.md](references/publish.md) |
|
|
39
|
+
| Cloud Run 0% -> 100%, verification, rollback | [references/promotion.md](references/promotion.md) |
|
|
37
40
|
|
|
38
41
|
Run every script as
|
|
39
42
|
`node <skillsRoot>/rush-release/scripts/<name>.mjs …` with the absolute skill
|
|
@@ -54,8 +57,22 @@ target repo and will not find these files.
|
|
|
54
57
|
release tree's CI evidence.
|
|
55
58
|
- Tags are immutable annotated `vMAJOR.MINOR.PATCH` names. Do not move, delete,
|
|
56
59
|
or force-push a tag that reached a remote.
|
|
57
|
-
-
|
|
58
|
-
|
|
60
|
+
- A Cloud Run release uses one immutable `vMAJOR.MINOR.PATCH-rc.N` tag to build
|
|
61
|
+
the candidate at 0%. The final tag is created only after that exact candidate
|
|
62
|
+
serves 100% and passes live verification; it must not trigger another deploy.
|
|
63
|
+
- Ask once, on the complete plan, before any git mutation or push. That answer
|
|
64
|
+
authorizes the listed branch/commit, metadata PR delivery, candidate build,
|
|
65
|
+
conditional traffic shift, rollback, final tag, optional GitHub Release, and
|
|
66
|
+
publisher watch as one operation. Do not ask separate cut, merge, RC,
|
|
67
|
+
exposure, tag, finalization, or publish questions.
|
|
68
|
+
- Preflight facts are detected, not confirmed by the user. Fold any genuinely
|
|
69
|
+
missing choice into the one plan question instead of asking a preliminary
|
|
70
|
+
question. If the plan's frozen SHA, version/tag, metadata diff, publisher, or
|
|
71
|
+
outward actions later change, stop and present one replacement plan; otherwise
|
|
72
|
+
continue without re-confirming.
|
|
73
|
+
- A pre-exposure failure is a hand-back with production untouched. A known
|
|
74
|
+
post-exposure failure automatically restores and verifies the recorded
|
|
75
|
+
incumbent. Ambiguous production state is a stop for operator recovery.
|
|
59
76
|
- The cut is metadata only (changelog + version identity files). Refuse a
|
|
60
77
|
cut that changes product code.
|
|
61
78
|
|
|
@@ -63,8 +80,8 @@ target repo and will not find these files.
|
|
|
63
80
|
|
|
64
81
|
### 1. Preflight
|
|
65
82
|
|
|
66
|
-
|
|
67
|
-
|
|
83
|
+
Verify `main` is the trunk, detect npm and/or the staged Cloud Run contract,
|
|
84
|
+
and list the files that carry the version. Details:
|
|
68
85
|
[preflight.md](references/preflight.md).
|
|
69
86
|
|
|
70
87
|
### 2. Candidate
|
|
@@ -72,28 +89,32 @@ npm, or both, and list the files that carry the version. Details:
|
|
|
72
89
|
Fetch `origin/main` and pick the newest green SHA. Freeze it. Details:
|
|
73
90
|
[candidate.md](references/candidate.md).
|
|
74
91
|
|
|
75
|
-
### 3. Plan and confirm
|
|
92
|
+
### 3. Plan and confirm once
|
|
76
93
|
|
|
77
|
-
Derive the changelog and SemVer bump from `baselineTag..frozenSHA`.
|
|
78
|
-
|
|
79
|
-
|
|
94
|
+
Derive the changelog and SemVer bump from `baselineTag..frozenSHA`. For Cloud
|
|
95
|
+
Run, pass `--cloud-run` so the plan also reserves the next unused RC tag. Show
|
|
96
|
+
SHA, bump, version, tags, files, publisher, notes, and every outward action. Ask one
|
|
97
|
+
question that both settles any unresolved choice and authorizes the whole listed
|
|
98
|
+
sequence. Do not mutate until the human confirms, and do not ask again while the
|
|
99
|
+
approved plan remains unchanged. Details: [cut.md](references/cut.md).
|
|
80
100
|
|
|
81
101
|
### 4. Cut
|
|
82
102
|
|
|
83
103
|
Branch from the frozen SHA, apply the changelog and version files, commit.
|
|
84
104
|
Keep the diff metadata-only.
|
|
85
105
|
|
|
86
|
-
### 5. Merge
|
|
106
|
+
### 5. Merge and publish
|
|
87
107
|
|
|
88
|
-
Push the cut ref, merge its PR through the repository queue, verify the
|
|
89
|
-
merged SHA on `main
|
|
90
|
-
|
|
91
|
-
|
|
108
|
+
Push the cut ref, merge its PR through the repository queue, and verify the
|
|
109
|
+
exact merged SHA on `main`. An artifact-only release pushes the final tag and
|
|
110
|
+
watches npm. A staged Cloud Run release pushes its RC tag, executes
|
|
111
|
+
[promotion.md](references/promotion.md), and creates the final tag only after
|
|
112
|
+
live verification. Details: [publish.md](references/publish.md).
|
|
92
113
|
|
|
93
114
|
## Done
|
|
94
115
|
|
|
95
|
-
Success is the confirmed version tagged at the verified merged `main` SHA
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
the
|
|
116
|
+
Success is the confirmed version tagged at the verified merged `main` SHA and
|
|
117
|
+
visible on the remote. npm must report the version when selected. Cloud Run
|
|
118
|
+
must report the exact candidate serving 100%, the incumbent at 0%, and an ALIVE
|
|
119
|
+
`prod-active` verdict. Hand back the cut SHA, merged SHA, RC/final tags,
|
|
120
|
+
changelog excerpt, publisher and traffic evidence, and the merged metadata PR.
|
|
@@ -11,20 +11,21 @@
|
|
|
11
11
|
"Proposes a minor SemVer bump to 1.5.0 because of feat: export csv.",
|
|
12
12
|
"Includes changelog entries derived from both the feat and the fix.",
|
|
13
13
|
"Tags only the queue-verified merged main SHA whose tree exactly equals the version-cut tree.",
|
|
14
|
-
"Watches npm publish (npm view) and does not describe a 0% traffic candidate or GitFlow release branch."
|
|
14
|
+
"Watches npm publish (npm view) and does not describe a 0% traffic candidate or GitFlow release branch.",
|
|
15
|
+
"Presents one complete-plan confirmation covering cut, PR delivery, tag push, and publisher watch, with no staged confirmations."
|
|
15
16
|
]
|
|
16
17
|
},
|
|
17
18
|
{
|
|
18
19
|
"id": 2,
|
|
19
|
-
"prompt": "Dry-run only. Main is protected. Latest green HEAD is 2222222. Plan says v2.0.0 from a breaking change. The metadata PR merges through the queue as 3333333 and its tree exactly equals the approved cut. After you would push
|
|
20
|
-
"expected_output": "A dry-run that verifies 3333333 is on main and tree-identical to the cut, tags merged SHA 3333333 with annotated v2.0.0, pushes
|
|
20
|
+
"prompt": "Dry-run only. Main is protected. Latest green HEAD is 2222222. Plan says v2.0.0 from a breaking change. The metadata PR merges through the queue as 3333333 and its tree exactly equals the approved cut. The repository has a verified Cloud Run staged contract: RC tags deploy immutable candidates with --no-traffic and final tags do not deploy. After you would push v2.0.0-rc.1, Cloud Build returns FAILURE. What do you tag, how do you push it, and what happens after the failed watch?",
|
|
21
|
+
"expected_output": "A dry-run that verifies 3333333 is on main and tree-identical to the cut, tags merged SHA 3333333 with annotated v2.0.0-rc.1, pushes that fully qualified ref with --no-verify, watches Cloud Build for the RC tag and exact SHA, then on FAILURE leaves the RC tag immutable, keeps v2.0.0 unused, and hands back with production untouched.",
|
|
21
22
|
"files": [],
|
|
22
23
|
"expectations": [
|
|
23
|
-
"Creates an annotated v2.0.0 tag on the verified merged main SHA 3333333.",
|
|
24
|
-
"Pushes the fully-qualified tag ref with --no-verify.",
|
|
25
|
-
"Watches Cloud Build filtered by
|
|
26
|
-
"On FAILURE,
|
|
27
|
-
"
|
|
24
|
+
"Creates an annotated v2.0.0-rc.1 tag on the verified merged main SHA 3333333.",
|
|
25
|
+
"Pushes the fully-qualified RC tag ref with --no-verify.",
|
|
26
|
+
"Watches Cloud Build filtered by the RC tag and exact merged SHA.",
|
|
27
|
+
"On FAILURE, leaves the RC tag immutable and production untouched.",
|
|
28
|
+
"Does not create the final v2.0.0 tag or claim the final version is burned."
|
|
28
29
|
]
|
|
29
30
|
},
|
|
30
31
|
{
|
|
@@ -39,6 +40,19 @@
|
|
|
39
40
|
"Restarts the changelog and SemVer plan from a new eligible main state before watching Cloud Build and npm.",
|
|
40
41
|
"Does not squash-merge the metadata PR onto moved main."
|
|
41
42
|
]
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"id": 4,
|
|
46
|
+
"prompt": "Dry-run only. One complete Cloud Run release plan was already confirmed. Candidate v1.8.0-rc.1 for merged SHA aaaaaaa is at 0%, incumbent revision old-42 is at 100%, and prod-preview is fresh, read-only, and ALIVE. Describe the rest of the release if the shift reaches candidate revision new-43 at 100% but prod-active is DEAD. Do not ask another question.",
|
|
47
|
+
"expected_output": "A dry-run that rechecks candidate identity and the 0/100 split, shifts under the existing authorization, observes the failed live smoke, automatically restores old-42 to 100%, verifies the restored incumbent with prod-active, leaves v1.8.0 final unused and the RC tag immutable, and hands back without another confirmation.",
|
|
48
|
+
"files": [],
|
|
49
|
+
"expectations": [
|
|
50
|
+
"Does not request a second exposure, rollback, or finalization confirmation.",
|
|
51
|
+
"Rechecks fresh ALIVE evidence, candidate identity, and candidate 0% / incumbent 100% immediately before shifting.",
|
|
52
|
+
"Treats prod-active DEAD after the shift as a post-exposure failure.",
|
|
53
|
+
"Automatically restores old-42 and verifies it at 100% with a prod-active ALIVE result.",
|
|
54
|
+
"Leaves the final v1.8.0 tag unused and v1.8.0-rc.1 immutable."
|
|
55
|
+
]
|
|
42
56
|
}
|
|
43
57
|
]
|
|
44
58
|
}
|
|
@@ -8,10 +8,16 @@ along for the ride.
|
|
|
8
8
|
```bash
|
|
9
9
|
node <skillsRoot>/rush-release/scripts/plan.mjs --sha <candidateSha> --json \
|
|
10
10
|
> /tmp/rush-release-plan.json
|
|
11
|
+
# Add --cloud-run when preflight selected cloud-run-staged.
|
|
11
12
|
```
|
|
12
13
|
|
|
13
14
|
The plan names `baselineTag`, `currentVersion`, `bump`, `reason`,
|
|
14
|
-
`nextVersion`, `tag`, `changelogMarkdown`, and
|
|
15
|
+
`nextVersion`, final `tag`, optional `candidateTag`, `changelogMarkdown`, and
|
|
16
|
+
`versionFiles`.
|
|
17
|
+
|
|
18
|
+
Before showing the plan, use `git ls-remote --tags origin` to prove the proposed
|
|
19
|
+
candidate and final tags are both unused remotely. Local tag discovery chooses
|
|
20
|
+
the next RC ordinal; remote absence is the collision authority.
|
|
15
21
|
|
|
16
22
|
SemVer (0.x treats breaking as minor unless the human asks for strict):
|
|
17
23
|
|
|
@@ -26,13 +32,24 @@ Changelog entries come from conventional-commit subjects in
|
|
|
26
32
|
tag). Keep an existing `## [Unreleased]` body — derived entries merge with it,
|
|
27
33
|
they do not replace it.
|
|
28
34
|
|
|
29
|
-
## 2. Confirm
|
|
35
|
+
## 2. Confirm once
|
|
30
36
|
|
|
31
|
-
Show the frozen SHA, proposed version/tag, bump reason, changelog, version
|
|
32
|
-
files,
|
|
37
|
+
Show the frozen SHA, proposed version/tag(s), bump reason, changelog, version
|
|
38
|
+
files, publisher/deployment mode, and the complete outward-action sequence: create the cut
|
|
39
|
+
branch/commit, push it, deliver its metadata PR through the queue, push the
|
|
40
|
+
immutable tag(s), validate and conditionally promote Cloud Run when applicable,
|
|
41
|
+
create a GitHub Release when required, and watch the publisher.
|
|
42
|
+
Ask exactly one question. Its answer both settles any displayed unresolved
|
|
43
|
+
choice and authorizes that entire sequence. A different bump the human names
|
|
33
44
|
wins, as long as it is still SemVer.
|
|
34
45
|
|
|
35
46
|
Do not create a branch, edit files, or tag before that confirmation.
|
|
47
|
+
After it, proceed through publication without asking separate cut, merge, RC,
|
|
48
|
+
exposure, tag, finalization, or publish questions. Repository/provider checks
|
|
49
|
+
are gates the orchestrator evaluates, not reasons to ask the human to confirm
|
|
50
|
+
facts. Ask again only by replacing the whole plan when its frozen SHA,
|
|
51
|
+
version/tag, metadata diff, publisher, or outward actions change. A retry or
|
|
52
|
+
resume of the unchanged plan inherits the original authorization.
|
|
36
53
|
|
|
37
54
|
## 3. Apply and commit
|
|
38
55
|
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# Preflight
|
|
2
2
|
|
|
3
|
-
Preflight
|
|
4
|
-
files that carry the version. It does not pick the SHA, bump the
|
|
5
|
-
push.
|
|
3
|
+
Preflight outputs trunk `main`, publishers, deployment mode, release commands,
|
|
4
|
+
and the files that carry the version. It does not pick the SHA, bump the
|
|
5
|
+
version, or push. Resolve these facts from repository and provider state without asking the
|
|
6
|
+
human to confirm each one. If a required choice cannot be inferred, carry it
|
|
7
|
+
into the single complete-plan question in [cut.md](cut.md); do not create a
|
|
8
|
+
separate preflight confirmation.
|
|
6
9
|
|
|
7
10
|
## 1. Trunk
|
|
8
11
|
|
|
9
|
-
Fetch `origin
|
|
12
|
+
Fetch `origin/main` and all remote tags. Verify the release trunk is `main` from those fetched refs
|
|
10
13
|
(`origin/main` exists). A repo whose production branch is `master` or `release`
|
|
11
14
|
is not this workflow.
|
|
12
15
|
|
|
@@ -17,12 +20,28 @@ otherwise look at the tree:
|
|
|
17
20
|
|
|
18
21
|
| Signal | Publisher |
|
|
19
22
|
|---|---|
|
|
20
|
-
|
|
|
23
|
+
| Cloud Build production trigger matching only `v*-rc.*`, whose prod path emits immutable candidate coordinates after `--no-traffic` | `cloudbuild`, mode `cloud-run-staged` |
|
|
21
24
|
| `package.json` with a publish workflow, `publishConfig`, or an explicit npm release | `npm` |
|
|
22
25
|
| both | watch both |
|
|
23
26
|
| neither, and the human did not name one | refuse — nothing to watch |
|
|
24
27
|
|
|
25
|
-
|
|
28
|
+
Cloud Build presence alone is not a staged contract. For `cloud-run-staged`,
|
|
29
|
+
verify from repository policy and live trigger configuration:
|
|
30
|
+
|
|
31
|
+
- the production trigger matches RC tags and excludes the final `vX.Y.Z` tag;
|
|
32
|
+
- each affected service deploys an immutable revision with `--no-traffic` and
|
|
33
|
+
publishes one receipt binding RC tag, merged SHA, revision, image digest, and
|
|
34
|
+
candidate URL;
|
|
35
|
+
- `smoke.manifest.json` defines read-only `prod-preview` and `prod-active`
|
|
36
|
+
profiles with at least one eligible path;
|
|
37
|
+
- exact inspect, traffic-shift, rollback, and candidate-delete commands are
|
|
38
|
+
documented, including service dependency order;
|
|
39
|
+
- any durable processing affected by a shift has exact pause, quiescence,
|
|
40
|
+
resume, and restoration checks.
|
|
41
|
+
|
|
42
|
+
Refuse staged release when any applicable item is absent; never improvise
|
|
43
|
+
production commands. Record `publisher` as `cloudbuild`, `npm`, or `both` and
|
|
44
|
+
`deploymentMode` as `artifact-only` or `cloud-run-staged`. If a GitHub Actions
|
|
26
45
|
workflow publishes on `release: published` rather than on tag push, note that
|
|
27
46
|
`gh release create` is required at publish time.
|
|
28
47
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Cloud Run staged promotion
|
|
2
|
+
|
|
3
|
+
Promote one immutable candidate from 0% to 100% under the complete-plan
|
|
4
|
+
authorization. Load `smoke` for both validation profiles and the applicable
|
|
5
|
+
platform skill for repository-declared Cloud Run commands. Do not restore the
|
|
6
|
+
archived `resolve-release` state machine or introduce another confirmation.
|
|
7
|
+
|
|
8
|
+
## 1. Bind the candidate
|
|
9
|
+
|
|
10
|
+
After the RC-tag build reports `SUCCESS`, read its durable candidate receipt.
|
|
11
|
+
For every affected service, independently verify from one fresh Cloud Run
|
|
12
|
+
snapshot that:
|
|
13
|
+
|
|
14
|
+
- receipt RC tag and source SHA equal the approved plan and `<mergedSha>`;
|
|
15
|
+
- the named immutable revision runs the receipt's image digest;
|
|
16
|
+
- the candidate URL resolves to that revision;
|
|
17
|
+
- the candidate has exactly 0% traffic and the recorded incumbent exactly 100%;
|
|
18
|
+
- no second release or unknown traffic-bearing revision is in flight.
|
|
19
|
+
|
|
20
|
+
Missing, duplicate, stale, or contradictory identity is a pre-exposure failure.
|
|
21
|
+
Delete the candidate by immutable revision only when the preflight contract
|
|
22
|
+
declared that deletion safe; otherwise leave it at 0% and hand back. Never move
|
|
23
|
+
the RC tag.
|
|
24
|
+
|
|
25
|
+
## 2. Validate at 0%
|
|
26
|
+
|
|
27
|
+
Run the repository's smoke workflow against the exact candidate URL with
|
|
28
|
+
`--profile prod-preview`. Accept only a non-empty `ALIVE` verdict whose stamped
|
|
29
|
+
target is that URL, profile is `prod-preview`, and `noMutations` is `true`.
|
|
30
|
+
Record its `generatedAt`. A failed or unavailable gate leaves production on the
|
|
31
|
+
incumbent and does not consume the final tag.
|
|
32
|
+
|
|
33
|
+
## 3. Recheck and shift
|
|
34
|
+
|
|
35
|
+
Immediately before exposure, re-read candidate identity and traffic. Require
|
|
36
|
+
the candidate still at 0%, incumbent still at 100%, and the ALIVE verdict still
|
|
37
|
+
within the plan's declared freshness window. If durable processing is affected,
|
|
38
|
+
run the preflight-declared pause order and prove quiescence before traffic moves.
|
|
39
|
+
|
|
40
|
+
Shift all affected services to the candidate in the declared dependency order.
|
|
41
|
+
After each command, read traffic back. If a later service cannot shift, restore
|
|
42
|
+
already-shifted services in reverse order before doing anything else. The plan's
|
|
43
|
+
single confirmation already authorized this conditional shift and restoration.
|
|
44
|
+
|
|
45
|
+
## 4. Verify live
|
|
46
|
+
|
|
47
|
+
From fresh control-plane and service observations, require for every service:
|
|
48
|
+
|
|
49
|
+
- the exact candidate revision holds 100% and the incumbent 0%;
|
|
50
|
+
- serving source SHA and image digest equal the candidate receipt;
|
|
51
|
+
- the live URL passes a non-empty, read-only `prod-active` smoke as `ALIVE`.
|
|
52
|
+
|
|
53
|
+
Resume durable processing only against the verified serving candidate, then
|
|
54
|
+
prove scheduling/admission and any required publication advancement are healthy.
|
|
55
|
+
Only this state permits the final release tag.
|
|
56
|
+
|
|
57
|
+
## 5. Failure routing
|
|
58
|
+
|
|
59
|
+
- **Before any traffic moved:** production is untouched. Delete or retain the
|
|
60
|
+
0% candidate according to the recorded policy and hand back.
|
|
61
|
+
- **After a known failed shift or live check:** automatically restore every
|
|
62
|
+
service to its recorded incumbent in reverse dependency order, restore
|
|
63
|
+
durable processing against that incumbent, and verify incumbent 100%,
|
|
64
|
+
candidate 0%, and `prod-active` ALIVE before hand-back.
|
|
65
|
+
- **Ambiguous traffic, identity, or processing state:** stop all writes and hand
|
|
66
|
+
back the last known-good evidence for operator recovery. Never guess, retry a
|
|
67
|
+
shift, delete a revision, or claim rollback succeeded.
|
|
68
|
+
|
|
69
|
+
The final tag remains unused on every failure path. An RC tag is immutable and
|
|
70
|
+
remains as attempt evidence.
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
# Publish
|
|
2
2
|
|
|
3
|
-
Push the cut, tag it, and watch the publisher.
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Push the cut, tag it, and watch the publisher. A pushed final tag that fails to
|
|
4
|
+
publish **burns the version**. A Cloud Run RC tag that fails burns that RC
|
|
5
|
+
ordinal, not the final version. Do not move or delete either tag to "reuse" it.
|
|
6
|
+
|
|
7
|
+
The complete-plan confirmation in [cut.md](cut.md) already authorizes every
|
|
8
|
+
action in this procedure and [promotion.md](promotion.md). Do not insert another
|
|
9
|
+
confirmation before the branch push, PR queue, candidate validation, traffic
|
|
10
|
+
shift, rollback, tag push, GitHub Release, or publisher watch. A provider or
|
|
11
|
+
repository gate either passes and the sequence continues, or fails and the run
|
|
12
|
+
hands back. If resolving a failure would change the approved identity or action
|
|
13
|
+
set, return to planning and ask once on the replacement plan.
|
|
6
14
|
|
|
7
15
|
## 1. Push and merge the metadata PR
|
|
8
16
|
|
|
@@ -27,7 +35,7 @@ documented release path, a successful fast-forward of `<cutSha>` to `main`
|
|
|
27
35
|
may use `<cutSha>` as `<mergedSha>`. Never infer this from local state: re-fetch
|
|
28
36
|
and verify the remote ref.
|
|
29
37
|
|
|
30
|
-
## 2. Verify
|
|
38
|
+
## 2. Verify the merged main SHA
|
|
31
39
|
|
|
32
40
|
The release tag must name a commit reachable from the remote default branch.
|
|
33
41
|
This is the ancestry gate used by release publishers to refuse off-main
|
|
@@ -40,8 +48,6 @@ git merge-base --is-ancestor "<mergedSha>" origin/main
|
|
|
40
48
|
test "$(git rev-parse "<cutSha>^{tree}")" = \
|
|
41
49
|
"$(git rev-parse "<mergedSha>^{tree}")"
|
|
42
50
|
test "$(git show "<mergedSha>:package.json" | jq -r .version)" = "<nextVersion>"
|
|
43
|
-
git tag -a "<tag>" "<mergedSha>" -m "Release <tag>"
|
|
44
|
-
git push --no-verify origin "refs/tags/<tag>"
|
|
45
51
|
```
|
|
46
52
|
|
|
47
53
|
The tree equality check proves the provider merge introduced no post-freeze
|
|
@@ -52,6 +58,30 @@ when it is not `package.json`. Re-read the exact required queue/CI result for
|
|
|
52
58
|
`<mergedSha>` before tagging. A green cut head is not evidence for a different
|
|
53
59
|
merge commit.
|
|
54
60
|
|
|
61
|
+
## 3. Publish by mode
|
|
62
|
+
|
|
63
|
+
**Artifact-only:** create and push the final annotated tag on `<mergedSha>`,
|
|
64
|
+
then continue to the publisher watch:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
git tag -a "<tag>" "<mergedSha>" -m "Release <tag>"
|
|
68
|
+
git push --no-verify origin "refs/tags/<tag>"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Cloud Run staged:** create and push `<candidateTag>` on `<mergedSha>`. Watch
|
|
72
|
+
Cloud Build until it emits the exact candidate receipt, then execute
|
|
73
|
+
[promotion.md](promotion.md). Only after live verification succeeds, create and
|
|
74
|
+
push the final tag on the same `<mergedSha>`. The final tag must not trigger a
|
|
75
|
+
second Cloud Run build.
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
git tag -a "<candidateTag>" "<mergedSha>" -m "Candidate <candidateTag>"
|
|
79
|
+
git push --no-verify origin "refs/tags/<candidateTag>"
|
|
80
|
+
# After promotion.md succeeds:
|
|
81
|
+
git tag -a "<tag>" "<mergedSha>" -m "Release <tag>"
|
|
82
|
+
git push --no-verify origin "refs/tags/<tag>"
|
|
83
|
+
```
|
|
84
|
+
|
|
55
85
|
`--no-verify` is for tag refs, because a Mergify (or similar) pre-push hook
|
|
56
86
|
that rewrites `git push` into a stack push is correct for branches and wrong
|
|
57
87
|
for immutable tags.
|
|
@@ -64,19 +94,21 @@ If preflight recorded a workflow that publishes only on
|
|
|
64
94
|
remote (`gh release create <tag> --notes-file …`). Otherwise the tag push is
|
|
65
95
|
the trigger.
|
|
66
96
|
|
|
67
|
-
##
|
|
97
|
+
## 4. Watch
|
|
68
98
|
|
|
69
99
|
Launch one background watcher; do not poll in the foreground.
|
|
70
100
|
|
|
71
|
-
**Cloud Build** (`publisher` `cloudbuild` or `both`)
|
|
101
|
+
**Cloud Build** (`publisher` `cloudbuild` or `both`) is watched first for the RC
|
|
102
|
+
tag and exact merged SHA. `SUCCESS` means only that candidate production
|
|
103
|
+
succeeded; Cloud Run success requires [promotion.md](promotion.md):
|
|
72
104
|
|
|
73
105
|
```bash
|
|
74
106
|
gcloud builds list --limit=1 --format='value(id,status,substitutions.TAG_NAME)' \
|
|
75
|
-
--filter="substitutions.TAG_NAME=<
|
|
107
|
+
--filter="substitutions.TAG_NAME=<candidateTag> OR tags=<candidateTag>"
|
|
76
108
|
```
|
|
77
109
|
|
|
78
|
-
|
|
79
|
-
|
|
110
|
+
`FAILURE` / `TIMEOUT` / `CANCELLED` is a hand-back. Load `cloud-debug` only to
|
|
111
|
+
diagnose, not to rerun the release.
|
|
80
112
|
|
|
81
113
|
**npm** (`publisher` `npm` or `both`):
|
|
82
114
|
|
|
@@ -92,9 +124,10 @@ When a GitHub Actions workflow is the npm publisher, `gh run list` /
|
|
|
92
124
|
`gh run watch` for the tag SHA is the wait; `npm view` remains the proof the
|
|
93
125
|
artifact exists.
|
|
94
126
|
|
|
95
|
-
##
|
|
127
|
+
## 5. Failure
|
|
96
128
|
|
|
97
|
-
If
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
129
|
+
If an artifact watch fails after the final tag, leave it in place and hand
|
|
130
|
+
back that the version is burned. If a staged candidate build fails, leave its
|
|
131
|
+
RC tag, keep the final tag unused, and hand back with production untouched.
|
|
132
|
+
Promotion failures follow [promotion.md](promotion.md). A fix starts from a new
|
|
133
|
+
green HEAD and a replacement plan; tags are never moved or deleted.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Changelog + SemVer plan from baseline tag..SHA.
|
|
3
3
|
//
|
|
4
4
|
// Usage:
|
|
5
|
-
// node plan.mjs --sha <sha> [--trunk main] [--json] [--strict]
|
|
5
|
+
// node plan.mjs --sha <sha> [--trunk main] [--json] [--strict] [--cloud-run]
|
|
6
6
|
//
|
|
7
7
|
// Exit: 0 ok · 1 git/inspect failed · 2 usage
|
|
8
8
|
import { existsSync, readFileSync } from 'node:fs'
|
|
@@ -50,6 +50,16 @@ export function latestReleaseTag(tags) {
|
|
|
50
50
|
return parsed[parsed.length - 1]
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
export function nextReleaseCandidateTag(finalTag, tags) {
|
|
54
|
+
const escaped = finalTag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
55
|
+
const pattern = new RegExp(`^${escaped}-rc\\.([1-9]\\d*)$`)
|
|
56
|
+
const ordinals = tags
|
|
57
|
+
.map((tag) => pattern.exec(tag))
|
|
58
|
+
.filter(Boolean)
|
|
59
|
+
.map((match) => Number(match[1]))
|
|
60
|
+
return `${finalTag}-rc.${ordinals.length ? Math.max(...ordinals) + 1 : 1}`
|
|
61
|
+
}
|
|
62
|
+
|
|
53
63
|
export function classifySubject(subject, body = '') {
|
|
54
64
|
const title = String(subject || '').trim()
|
|
55
65
|
const match = TITLE_RE.exec(title)
|
|
@@ -207,9 +217,16 @@ function parseLog(stdout) {
|
|
|
207
217
|
})
|
|
208
218
|
}
|
|
209
219
|
|
|
210
|
-
export function planRelease({
|
|
220
|
+
export function planRelease({
|
|
221
|
+
sha,
|
|
222
|
+
trunk = 'main',
|
|
223
|
+
strict = false,
|
|
224
|
+
cloudRun = false,
|
|
225
|
+
cwd = process.cwd(),
|
|
226
|
+
now = new Date(),
|
|
227
|
+
} = {}) {
|
|
211
228
|
if (!sha) {
|
|
212
|
-
const err = new Error('usage: plan.mjs --sha <sha> [--trunk main] [--json] [--strict]')
|
|
229
|
+
const err = new Error('usage: plan.mjs --sha <sha> [--trunk main] [--json] [--strict] [--cloud-run]')
|
|
213
230
|
err.exitCode = 2
|
|
214
231
|
throw err
|
|
215
232
|
}
|
|
@@ -230,6 +247,13 @@ export function planRelease({ sha, trunk = 'main', strict = false, cwd = process
|
|
|
230
247
|
const { bump, reason } = bumpFor(changes, current, { strict })
|
|
231
248
|
const next = nextVersion(current, bump)
|
|
232
249
|
const version = formatVersion(next)
|
|
250
|
+
const tag = `v${version}`
|
|
251
|
+
const candidateTag = cloudRun
|
|
252
|
+
? nextReleaseCandidateTag(tag, git(['tag', '--list', `${tag}-rc.*`], cwd)
|
|
253
|
+
.split('\n')
|
|
254
|
+
.map((line) => line.trim())
|
|
255
|
+
.filter(Boolean))
|
|
256
|
+
: null
|
|
233
257
|
const date = now.toISOString().slice(0, 10)
|
|
234
258
|
return {
|
|
235
259
|
trunk,
|
|
@@ -239,7 +263,8 @@ export function planRelease({ sha, trunk = 'main', strict = false, cwd = process
|
|
|
239
263
|
bump,
|
|
240
264
|
reason,
|
|
241
265
|
nextVersion: version,
|
|
242
|
-
tag
|
|
266
|
+
tag,
|
|
267
|
+
candidateTag,
|
|
243
268
|
versionFiles: files,
|
|
244
269
|
changes,
|
|
245
270
|
date,
|
|
@@ -248,11 +273,12 @@ export function planRelease({ sha, trunk = 'main', strict = false, cwd = process
|
|
|
248
273
|
}
|
|
249
274
|
|
|
250
275
|
function parseArgs(argv) {
|
|
251
|
-
const opts = { json: false, strict: false, trunk: 'main', sha: null }
|
|
276
|
+
const opts = { json: false, strict: false, cloudRun: false, trunk: 'main', sha: null }
|
|
252
277
|
for (let i = 0; i < argv.length; i += 1) {
|
|
253
278
|
const arg = argv[i]
|
|
254
279
|
if (arg === '--json') opts.json = true
|
|
255
280
|
else if (arg === '--strict') opts.strict = true
|
|
281
|
+
else if (arg === '--cloud-run') opts.cloudRun = true
|
|
256
282
|
else if (arg === '--sha') opts.sha = argv[++i]
|
|
257
283
|
else if (arg === '--trunk') opts.trunk = argv[++i]
|
|
258
284
|
else if (arg === '--help' || arg === '-h') opts.help = true
|
|
@@ -264,7 +290,7 @@ function parseArgs(argv) {
|
|
|
264
290
|
}
|
|
265
291
|
if (opts.help) return opts
|
|
266
292
|
if (!opts.sha || !opts.trunk) {
|
|
267
|
-
const err = new Error('usage: plan.mjs --sha <sha> [--trunk main] [--json] [--strict]')
|
|
293
|
+
const err = new Error('usage: plan.mjs --sha <sha> [--trunk main] [--json] [--strict] [--cloud-run]')
|
|
268
294
|
err.exitCode = 2
|
|
269
295
|
throw err
|
|
270
296
|
}
|
|
@@ -274,7 +300,7 @@ function parseArgs(argv) {
|
|
|
274
300
|
export function main(argv = process.argv.slice(2)) {
|
|
275
301
|
const opts = parseArgs(argv)
|
|
276
302
|
if (opts.help) {
|
|
277
|
-
process.stdout.write('plan.mjs --sha <sha> [--trunk main] [--json] [--strict]\n')
|
|
303
|
+
process.stdout.write('plan.mjs --sha <sha> [--trunk main] [--json] [--strict] [--cloud-run]\n')
|
|
278
304
|
return 0
|
|
279
305
|
}
|
|
280
306
|
const result = planRelease(opts)
|
|
@@ -63,7 +63,7 @@ node <smoke>/scripts/smoke.mjs plan --manifest smoke.manifest.json --profile pro
|
|
|
63
63
|
|
|
64
64
|
`--target` is now **just the address to drive against** (the deployed candidate/service URL; defaults to `local`) — it no longer participates in selection. The profile does. For a `local` profile, boot the build yourself the `/verify` way (a `run-*`/`verifier-*` skill in `.claude/skills/`, else the **`run`** skill, else cold-start from README/package.json, timebox ~15 min → BLOCKED); for a deployed profile, point `--target` at the revision and confirm it is the SHA you mean to verify.
|
|
65
65
|
|
|
66
|
-
**Prod safety is mechanical, and it lives on the profile.** A path runs in a profile only if it declares that profile AND the profile's `mutationPolicy` admits it. The **production profiles (`prod-preview`, `prod-active`) are read-only** — every `mutates:true` path is excluded, **tenant or not** (a synthetic tenant still writes to the real production database, and a revision at 0% traffic is not a reason those writes are safe
|
|
66
|
+
**Prod safety is mechanical, and it lives on the profile.** A path runs in a profile only if it declares that profile AND the profile's `mutationPolicy` admits it. The **production profiles (`prod-preview`, `prod-active`) are read-only** — every `mutates:true` path is excluded, **tenant or not** (a synthetic tenant still writes to the real production database, and a revision at 0% traffic is not a reason those writes are safe). `dev-active` allows a `mutates:true` path **only with a declared synthetic tenant**; the local profiles allow isolated writes. This is the old `--no-mutations` flag turned into a property of the *named profile*, so nobody has to remember to pass it — and because one name drives both the run and the verdict, the two **cannot disagree** about the selected set (the old run/verdict flag-mismatch is unrepresentable).
|
|
67
67
|
|
|
68
68
|
**Gate before promote.** On a deployed target the smoke is a gate only if it runs *before* the revision is irreversible — a smoke after traffic shifts is a monitor, not a gate. Deploy the candidate at **0% traffic** (stable, addressable) → smoke that URL under `prod-preview` → **promote only on ALIVE** → confirm the serving revision is the one you smoked, then re-smoke the live service under `prod-active`. Bonus: a DEAD is re-investigable against the same 0%-traffic revision with no rebuild — most of what a deployed smoke catches is config/secret/packaging. **Reuse the platform's deploy notification** (the CI/deploy build-failure → Pub/Sub/webhook path), carrying `smoke-verdict.json` so the alert names the broken path; the concrete placement lives in `cloud-build` / `cloud-deploy`.
|
|
69
69
|
|
|
@@ -82,7 +82,7 @@ node <smoke>/scripts/smoke.mjs verdict --manifest smoke.manifest.json --profile
|
|
|
82
82
|
```
|
|
83
83
|
**ALIVE** iff every selected path PASSED. Any `fail`, `blocked`, or `missing` → **DEAD**, naming the path(s). Exit code is the gate (0 = ALIVE, 1 = DEAD). Selection is a pure function of `--profile` + the manifest, so the run and the verdict select the **same** set by construction — there is no second flag assembly for the verdict to disagree with, which is the whole point: the run/verdict mismatch that free-form targeting permitted is now unrepresentable.
|
|
84
84
|
|
|
85
|
-
`smoke-verdict.json` is stamped with the facts an automated consumer needs to judge the evidence rather than trust it: **`profile`** (the Validation Profile it selected under), **`target`** (which URL it ran against), **`noMutations`** (DERIVED from the profile's `mutationPolicy` — `true` for the read-only production profiles), and **`generatedAt`** (an ISO-8601 instant — *when*, so a consumer can bound how old the evidence is). `
|
|
85
|
+
`smoke-verdict.json` is stamped with the facts an automated consumer needs to judge the evidence rather than trust it: **`profile`** (the Validation Profile it selected under), **`target`** (which URL it ran against), **`noMutations`** (DERIVED from the profile's `mutationPolicy` — `true` for the read-only production profiles), and **`generatedAt`** (an ISO-8601 instant — *when*, so a consumer can bound how old the evidence is). `rush-release`'s traffic-shift gate compares these; an ALIVE from the wrong target, a mutating run, or too long ago is not a green light.
|
|
86
86
|
|
|
87
87
|
**`generatedAt` dates the verdict, not the run.** `verdict` reduces whatever `--results` file you hand it, however old, and stamps the current instant on the result — so re-running `verdict` alone re-mints freshness without re-validating anything, and a consumer bounding the age cannot tell the difference. That is not a substitute for a stale gate's re-check: when what expired is the *evidence about a live target*, re-drive the paths into a new `smoke-results.json` — re-minting a verdict is not re-validation.
|
|
88
88
|
|
|
@@ -113,8 +113,8 @@ The **per-PR golden-path liveness floor** (`ensure-coverage` CI contract, gate 8
|
|
|
113
113
|
- **CI (gate 8, blocking)** — wired in `e2e-test/assets/ci-gates.*`; boots the app, drives the paths into `smoke-results.json`, runs `smoke.mjs verdict`. The mechanical backstop.
|
|
114
114
|
- **`resolve-issues`** — final step of the [epic integration gate](../resolve-issues/references/deliverables.md#deliverables-the-shippable-component-not-the-whole-epic): boot the assembled integration branch and run the golden paths; a DEAD whose broken paths are `fail` belongs to a specific unit → reopen its fix loop. **A DEAD built from `blocked`/`missing` paths is a gate-environment problem, not a unit's defect** — report it and fix the environment; reopening a fix loop on it spends a producer spawn and an adversarial review on code that is fine, and the round lands in the unit's budget all the same.
|
|
115
115
|
- **`/smoke`** — standalone, any time, against `local` or a URL.
|
|
116
|
-
-
|
|
117
|
-
- **The dev-deploy recorder** (`cloud-build` → Post-deploy smoke gate) — the integration pipeline runs this gate after its 100%-traffic deploy and publishes the verdict as a **SHA-keyed commit status**, never failing the build; that recorded ALIVE
|
|
116
|
+
- **`rush-release` Cloud Run flow** — the `prod-preview` profile against the 0%-traffic candidate URL (read-only, so it excludes every mutating path), then the `prod-active` profile against the live service after the shift. A DEAD **before** exposure leaves production untouched; a DEAD after it triggers the recorded rollback policy.
|
|
117
|
+
- **The dev-deploy recorder** (`cloud-build` → Post-deploy smoke gate) — the integration pipeline runs this gate after its 100%-traffic deploy and publishes the verdict as a **SHA-keyed commit status**, never failing the build; when configured as required CI, that recorded ALIVE helps `rush-release` select its green trunk SHA.
|
|
118
118
|
|
|
119
119
|
## Reference Map
|
|
120
120
|
|
|
@@ -82,7 +82,7 @@ JSON (`smoke.manifest.json`). A repo may author in YAML and project to JSON —
|
|
|
82
82
|
| `command` | cli | the command to run, for `cli` paths (alternative to `start`) |
|
|
83
83
|
| `observable` | api/cli | the **concrete thing that proves the path worked** — the 200 body, the exit code. Required for `api`/`cli` (e2e-test doesn't cover them); for `browser` the referenced journey's assertions *are* the observable. |
|
|
84
84
|
| `intent` | no | optional one-line human summary of what the browser journey proves — documentation only, not enforced. |
|
|
85
|
-
| `mutates` | no | `true` if the path writes durable state. Forces a `tenant` and gates it out of read-only profiles. Under a **read-only profile** (any production profile), a tenant grants nothing: every `mutates:true` path is excluded regardless
|
|
85
|
+
| `mutates` | no | `true` if the path writes durable state. Forces a `tenant` and gates it out of read-only profiles. Under a **read-only profile** (any production profile), a tenant grants nothing: every `mutates:true` path is excluded regardless. |
|
|
86
86
|
| `tenant` | when `mutates` | a **synthetic/test tenant** id so the mutating path can run under a `synthetic-tenant` profile (e.g. `dev-active`) without touching real data. A `mutates:true` path with no `tenant` is excluded from a `synthetic-tenant` profile. This is **not** a production permit: the production database is real whether or not the revision serves traffic, so the read-only production profiles exclude the path either way. Name it with the **`e2e-` prefix** (`e2e-synthetic`) per the e2e-* account convention (`e2e-test` → `references/authoring/auth-flows.md`) — `validate` warns on an unprefixed tenant, because that's the one mistaken for live data. |
|
|
87
87
|
| `profiles` | **when a `profiles` registry exists** | the [Validation Profiles](#validation-profiles-the-profiles-registry) this path is required in — a non-empty array of names from the registry. A path is only ever selected in a profile it declares; a path that declares none is a silent non-runner, so `validate` **fails closed** on it once a registry is present. (A per-path array, rather than the inverse profile→paths mapping, so a path's selection facts — `mutates`, `tenant`, `profiles` — all sit together on the path they govern.) |
|
|
88
88
|
|
|
@@ -123,11 +123,11 @@ Read-only paths always run in any profile they declare.
|
|
|
123
123
|
| `prod-preview` | `pre-promotion` | `prod` | `preview` | `read-only` |
|
|
124
124
|
| `prod-active` | `post-promotion` | `prod` | `active` | `read-only` |
|
|
125
125
|
|
|
126
|
-
`
|
|
126
|
+
`rush-release` consumes `prod-preview` before shifting the candidate from 0% traffic and `prod-active` after the shift. Each smoke **path** declares the profiles it is required in (`"profiles": ["pr-local", "prod-preview"]`); a path selected by zero eligible paths under a profile yields the existing **EMPTY** verdict — a gate that proves nothing must not look like a pass.
|
|
127
127
|
|
|
128
128
|
**Recommended, not mandatory (for now).** A manifest with no `profiles` registry still `validate`s (legacy manifests don't break) but `validate` warns, and `plan`/`verdict --profile` cannot run against it. The moment a registry is present it is fully checked, and every path must declare its profiles.
|
|
129
129
|
|
|
130
|
-
**Reserved names are pinned to their gate.** The five shipped names are consumed by `
|
|
130
|
+
**Reserved names are pinned to their gate.** The five shipped names are consumed by `rush-release` (and the other lanes) by **hardcoded string** — staged promotion drives `--profile prod-preview` before exposure and `prod-active` after — so the name is itself a safety key, not just a label. `validate` therefore **pins each reserved name to its gate**: `pr-local`→`pull-request`, `queue-local`→`merge-queue`, `dev-active`→`trunk-integration`, `prod-preview`→`pre-promotion`, `prod-active`→`post-promotion`. From the gate, the `(environment, endpoint)` table and the prod-read-only rule force the rest — so an entry *named* `prod-preview` but *bound* to a writable dev gate **fails closed**, instead of letting `plan --profile prod-preview` inherit a `synthetic-tenant` write policy against the production candidate.
|
|
131
131
|
|
|
132
132
|
| Hazard | Failure mode | What to prove |
|
|
133
133
|
|---|---|---|
|