@starci/skills 1.1.0
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/INDEX.md +74 -0
- package/INDEX.vi.md +75 -0
- package/README.md +44 -0
- package/README.vi.md +43 -0
- package/SKILL.md +135 -0
- package/SKILL.vi.md +128 -0
- package/alias/INDEX.md +104 -0
- package/alias/INDEX.vi.md +104 -0
- package/alias/alias.json +297 -0
- package/bin/starci-skills.mjs +219 -0
- package/knowledge/INDEX.md +22 -0
- package/knowledge/INDEX.vi.md +22 -0
- package/knowledge/grammars/starci/DNA.md +146 -0
- package/knowledge/grammars/starci/DNA.vi.md +146 -0
- package/knowledge/grammars/starci/INDEX.md +25 -0
- package/knowledge/grammars/starci/INDEX.vi.md +25 -0
- package/knowledge/grammars/starci/family.md +50 -0
- package/knowledge/grammars/starci/family.vi.md +50 -0
- package/knowledge/grammars/starci/idioms.md +120 -0
- package/knowledge/grammars/starci/idioms.vi.md +121 -0
- package/knowledge/grammars/starci/playbook.md +36 -0
- package/knowledge/grammars/starci/playbook.vi.md +35 -0
- package/knowledge/patterns/be/INDEX.md +29 -0
- package/knowledge/patterns/be/INDEX.vi.md +29 -0
- package/knowledge/patterns/be/comment.md +80 -0
- package/knowledge/patterns/be/comment.vi.md +80 -0
- package/knowledge/patterns/be/error.md +86 -0
- package/knowledge/patterns/be/error.vi.md +86 -0
- package/knowledge/patterns/be/folder.md +87 -0
- package/knowledge/patterns/be/folder.vi.md +86 -0
- package/knowledge/patterns/be/function.md +80 -0
- package/knowledge/patterns/be/function.vi.md +80 -0
- package/knowledge/patterns/be/imports.md +79 -0
- package/knowledge/patterns/be/imports.vi.md +79 -0
- package/knowledge/patterns/be/naming.md +87 -0
- package/knowledge/patterns/be/naming.vi.md +87 -0
- package/knowledge/patterns/be/test.md +79 -0
- package/knowledge/patterns/be/test.vi.md +79 -0
- package/knowledge/patterns/be/typing.md +73 -0
- package/knowledge/patterns/be/typing.vi.md +73 -0
- package/knowledge/patterns/fe/INDEX.md +29 -0
- package/knowledge/patterns/fe/INDEX.vi.md +29 -0
- package/knowledge/patterns/fe/comment.md +70 -0
- package/knowledge/patterns/fe/comment.vi.md +70 -0
- package/knowledge/patterns/fe/error.md +61 -0
- package/knowledge/patterns/fe/error.vi.md +61 -0
- package/knowledge/patterns/fe/folder.md +98 -0
- package/knowledge/patterns/fe/folder.vi.md +95 -0
- package/knowledge/patterns/fe/function.md +70 -0
- package/knowledge/patterns/fe/function.vi.md +70 -0
- package/knowledge/patterns/fe/imports.md +87 -0
- package/knowledge/patterns/fe/imports.vi.md +87 -0
- package/knowledge/patterns/fe/naming.md +77 -0
- package/knowledge/patterns/fe/naming.vi.md +77 -0
- package/knowledge/patterns/fe/test.md +73 -0
- package/knowledge/patterns/fe/test.vi.md +73 -0
- package/knowledge/patterns/fe/typing.md +67 -0
- package/knowledge/patterns/fe/typing.vi.md +67 -0
- package/knowledge/ui/INDEX.md +101 -0
- package/knowledge/ui/INDEX.vi.md +101 -0
- package/knowledge/ui/composition/INDEX.md +60 -0
- package/knowledge/ui/composition/INDEX.vi.md +63 -0
- package/knowledge/ui/composition/accent.md +73 -0
- package/knowledge/ui/composition/accent.vi.md +74 -0
- package/knowledge/ui/composition/action.md +55 -0
- package/knowledge/ui/composition/action.vi.md +56 -0
- package/knowledge/ui/composition/coverage.md +37 -0
- package/knowledge/ui/composition/coverage.vi.md +37 -0
- package/knowledge/ui/composition/cta.md +79 -0
- package/knowledge/ui/composition/cta.vi.md +79 -0
- package/knowledge/ui/composition/feedback.md +55 -0
- package/knowledge/ui/composition/feedback.vi.md +56 -0
- package/knowledge/ui/composition/hierarchy.md +83 -0
- package/knowledge/ui/composition/hierarchy.vi.md +82 -0
- package/knowledge/ui/composition/layout.md +91 -0
- package/knowledge/ui/composition/layout.vi.md +93 -0
- package/knowledge/ui/composition/responsive.md +67 -0
- package/knowledge/ui/composition/responsive.vi.md +67 -0
- package/knowledge/ui/composition/state.md +105 -0
- package/knowledge/ui/composition/state.vi.md +106 -0
- package/knowledge/ui/presentation/INDEX.md +94 -0
- package/knowledge/ui/presentation/INDEX.vi.md +69 -0
- package/knowledge/ui/presentation/boundary.md +170 -0
- package/knowledge/ui/presentation/boundary.vi.md +169 -0
- package/knowledge/ui/presentation/font.md +155 -0
- package/knowledge/ui/presentation/font.vi.md +156 -0
- package/knowledge/ui/presentation/gap.md +181 -0
- package/knowledge/ui/presentation/gap.vi.md +181 -0
- package/knowledge/ui/presentation/margin.md +168 -0
- package/knowledge/ui/presentation/margin.vi.md +171 -0
- package/knowledge/ui/presentation/measure.md +174 -0
- package/knowledge/ui/presentation/measure.vi.md +178 -0
- package/knowledge/ui/presentation/overflow.md +142 -0
- package/knowledge/ui/presentation/overflow.vi.md +142 -0
- package/knowledge/ui/presentation/padding.md +293 -0
- package/knowledge/ui/presentation/padding.vi.md +292 -0
- package/knowledge/ui/presentation/surface.md +166 -0
- package/knowledge/ui/presentation/surface.vi.md +166 -0
- package/knowledge/ui/presentation/text-flow.md +123 -0
- package/knowledge/ui/presentation/text-flow.vi.md +123 -0
- package/knowledge/ui/presentation/tone.md +114 -0
- package/knowledge/ui/presentation/tone.vi.md +113 -0
- package/knowledge/ui/proof/INDEX.md +55 -0
- package/knowledge/ui/proof/INDEX.vi.md +55 -0
- package/knowledge/ui/proof/accessibility.md +67 -0
- package/knowledge/ui/proof/accessibility.vi.md +68 -0
- package/knowledge/ui/proof/contrast.md +58 -0
- package/knowledge/ui/proof/contrast.vi.md +57 -0
- package/knowledge/ui/proof/focus.md +76 -0
- package/knowledge/ui/proof/focus.vi.md +76 -0
- package/knowledge/ui/proof/motion.md +63 -0
- package/knowledge/ui/proof/motion.vi.md +64 -0
- package/knowledge/ui/proof/render-truth.md +63 -0
- package/knowledge/ui/proof/render-truth.vi.md +63 -0
- package/operators/INDEX.md +199 -0
- package/operators/INDEX.vi.md +199 -0
- package/operators/architecture-decide/errors.json +115 -0
- package/operators/architecture-decide/operator.json +20 -0
- package/operators/architecture-decide/operator.md +133 -0
- package/operators/architecture-decide/operator.vi.md +130 -0
- package/operators/architecture-decide/self-test.mjs +221 -0
- package/operators/architecture-decide/validate.mjs +112 -0
- package/operators/backend-source-apply/errors.json +66 -0
- package/operators/backend-source-apply/operator.json +20 -0
- package/operators/backend-source-apply/operator.md +189 -0
- package/operators/backend-source-apply/operator.vi.md +185 -0
- package/operators/backend-source-apply/self-test.mjs +257 -0
- package/operators/backend-source-apply/validate.mjs +240 -0
- package/operators/business-decide/errors.json +78 -0
- package/operators/business-decide/operator.json +20 -0
- package/operators/business-decide/operator.md +176 -0
- package/operators/business-decide/operator.vi.md +174 -0
- package/operators/business-decide/self-test.mjs +225 -0
- package/operators/business-decide/validate.mjs +277 -0
- package/operators/content-generate/errors.json +106 -0
- package/operators/content-generate/operator.json +21 -0
- package/operators/content-generate/operator.md +155 -0
- package/operators/content-generate/operator.vi.md +155 -0
- package/operators/content-generate/self-test.mjs +288 -0
- package/operators/content-generate/validate.mjs +196 -0
- package/operators/errors.json +178 -0
- package/operators/frontend-direction-decide/errors.json +135 -0
- package/operators/frontend-direction-decide/operator.json +21 -0
- package/operators/frontend-direction-decide/operator.md +167 -0
- package/operators/frontend-direction-decide/operator.vi.md +167 -0
- package/operators/frontend-direction-decide/self-test.mjs +226 -0
- package/operators/frontend-direction-decide/validate.mjs +146 -0
- package/operators/frontend-presentation-resolve/errors.json +42 -0
- package/operators/frontend-presentation-resolve/operator.json +19 -0
- package/operators/frontend-presentation-resolve/operator.md +142 -0
- package/operators/frontend-presentation-resolve/operator.vi.md +140 -0
- package/operators/frontend-presentation-resolve/self-test.mjs +163 -0
- package/operators/frontend-presentation-resolve/validate.mjs +143 -0
- package/operators/frontend-source-apply/errors.json +30 -0
- package/operators/frontend-source-apply/operator.json +20 -0
- package/operators/frontend-source-apply/operator.md +127 -0
- package/operators/frontend-source-apply/operator.vi.md +126 -0
- package/operators/frontend-source-apply/self-test.mjs +214 -0
- package/operators/frontend-source-apply/validate.mjs +133 -0
- package/operators/frontend-surface-audit/errors.json +5 -0
- package/operators/frontend-surface-audit/operator.json +22 -0
- package/operators/frontend-surface-audit/operator.md +121 -0
- package/operators/frontend-surface-audit/operator.vi.md +121 -0
- package/operators/frontend-surface-audit/self-test.mjs +185 -0
- package/operators/frontend-surface-audit/validate.mjs +127 -0
- package/operators/git-publish/errors.json +54 -0
- package/operators/git-publish/operator.json +20 -0
- package/operators/git-publish/operator.md +180 -0
- package/operators/git-publish/operator.vi.md +178 -0
- package/operators/git-publish/self-test.mjs +177 -0
- package/operators/git-publish/validate.mjs +106 -0
- package/operators/platform-operate/errors.json +90 -0
- package/operators/platform-operate/operator.json +22 -0
- package/operators/platform-operate/operator.md +162 -0
- package/operators/platform-operate/operator.vi.md +160 -0
- package/operators/platform-operate/self-test.mjs +202 -0
- package/operators/platform-operate/validate.mjs +193 -0
- package/operators/quality-verify/errors.json +54 -0
- package/operators/quality-verify/operator.json +20 -0
- package/operators/quality-verify/operator.md +185 -0
- package/operators/quality-verify/operator.vi.md +177 -0
- package/operators/quality-verify/self-test.mjs +210 -0
- package/operators/quality-verify/validate.mjs +197 -0
- package/operators/release-deploy/errors.json +158 -0
- package/operators/release-deploy/operator.json +23 -0
- package/operators/release-deploy/operator.md +183 -0
- package/operators/release-deploy/operator.vi.md +181 -0
- package/operators/release-deploy/self-test.mjs +235 -0
- package/operators/release-deploy/validate.mjs +129 -0
- package/operators/uat-verify/errors.json +78 -0
- package/operators/uat-verify/operator.json +25 -0
- package/operators/uat-verify/operator.md +162 -0
- package/operators/uat-verify/operator.vi.md +161 -0
- package/operators/uat-verify/self-test.mjs +270 -0
- package/operators/uat-verify/validate.mjs +202 -0
- package/operators/workspace-bind/errors.json +90 -0
- package/operators/workspace-bind/operator.json +21 -0
- package/operators/workspace-bind/operator.md +148 -0
- package/operators/workspace-bind/operator.vi.md +148 -0
- package/operators/workspace-bind/self-test.mjs +205 -0
- package/operators/workspace-bind/validate.mjs +152 -0
- package/package.json +50 -0
- package/readiness/initialization/workspaces/commit-policy.json +91 -0
- package/readiness/initialization/workspaces/config.schema.json +35 -0
- package/readiness/initialization/workspaces/device-state.schema.json +54 -0
- package/readiness/initialization/workspaces/local-route.schema.json +206 -0
- package/readiness/initialization/workspaces/portable-route.schema.json +200 -0
- package/resources/INDEX.md +96 -0
- package/resources/INDEX.vi.md +99 -0
- package/resources/agents/profiles/claude.json +131 -0
- package/resources/agents/profiles/openai.json +131 -0
- package/resources/orchestrator.json +71 -0
- package/resources/tools.json +85 -0
- package/routing.json +272 -0
- package/scripts/alias-registry.mjs +31 -0
- package/scripts/device-state.mjs +497 -0
- package/scripts/device-state.spec.mjs +18 -0
- package/scripts/errors-registry.mjs +68 -0
- package/scripts/generate-alias-doc.mjs +66 -0
- package/scripts/generate-grammar-dna.mjs +387 -0
- package/scripts/generate-operators-index.mjs +95 -0
- package/scripts/generate-presentation-owned.mjs +681 -0
- package/scripts/install-cli.spec.mjs +74 -0
- package/scripts/json-schema.mjs +94 -0
- package/scripts/operator-md.mjs +96 -0
- package/scripts/run-operator-self-tests.mjs +36 -0
- package/scripts/validate-alias.mjs +165 -0
- package/scripts/validate-defaults.mjs +72 -0
- package/scripts/validate-knowledge-citations.mjs +90 -0
- package/scripts/validate-operator.mjs +125 -0
- package/scripts/validate-request.mjs +80 -0
- package/scripts/validate-resources.mjs +117 -0
- package/scripts/validate-response.mjs +141 -0
- package/scripts/validate-routing.mjs +91 -0
- package/scripts/validate-step.mjs +50 -0
- package/scripts/validate-templates.mjs +226 -0
- package/scripts/validate-templates.spec.mjs +144 -0
- package/scripts/validate-workflows.mjs +106 -0
- package/scripts/workspace-portable.mjs +389 -0
- package/scripts/workspace-portable.spec.mjs +246 -0
- package/templates/README.md +47 -0
- package/templates/README.vi.md +45 -0
- package/templates/changes.example.md +27 -0
- package/templates/grammars.template.md +34 -0
- package/templates/kinds/architecture-decision.contract.json +14 -0
- package/templates/kinds/architecture-decision.skeleton.md +52 -0
- package/templates/kinds/backend-source-application.contract.json +10 -0
- package/templates/kinds/backend-source-application.skeleton.md +34 -0
- package/templates/kinds/business-promise-authority.contract.json +13 -0
- package/templates/kinds/business-promise-authority.skeleton.md +67 -0
- package/templates/kinds/capture.schema.json +33 -0
- package/templates/kinds/changes.contract.json +10 -0
- package/templates/kinds/changes.skeleton.md +26 -0
- package/templates/kinds/checks.schema.json +44 -0
- package/templates/kinds/claims.schema.json +165 -0
- package/templates/kinds/conformance.schema.json +15 -0
- package/templates/kinds/content-brief.contract.json +12 -0
- package/templates/kinds/content-brief.skeleton.md +45 -0
- package/templates/kinds/content-generation-receipt.contract.json +11 -0
- package/templates/kinds/content-generation-receipt.skeleton.md +40 -0
- package/templates/kinds/content-review.contract.json +11 -0
- package/templates/kinds/content-review.skeleton.md +45 -0
- package/templates/kinds/contract.schema.json +28 -0
- package/templates/kinds/coverage-matrix.schema.json +86 -0
- package/templates/kinds/coverage.schema.json +62 -0
- package/templates/kinds/current-state.schema.json +40 -0
- package/templates/kinds/delta.schema.json +95 -0
- package/templates/kinds/e2e.schema.json +63 -0
- package/templates/kinds/frontend-direction-decision.contract.json +93 -0
- package/templates/kinds/frontend-direction-decision.skeleton.md +68 -0
- package/templates/kinds/frontend-presentation-resolution.contract.json +11 -0
- package/templates/kinds/frontend-presentation-resolution.skeleton.md +32 -0
- package/templates/kinds/frontend-source-application.contract.json +10 -0
- package/templates/kinds/frontend-source-application.skeleton.md +30 -0
- package/templates/kinds/frontend-surface-audit.contract.json +52 -0
- package/templates/kinds/frontend-surface-audit.skeleton.md +32 -0
- package/templates/kinds/gate-result.schema.json +64 -0
- package/templates/kinds/git-publication.contract.json +82 -0
- package/templates/kinds/git-publication.skeleton.md +61 -0
- package/templates/kinds/independent-critique.contract.json +9 -0
- package/templates/kinds/independent-critique.skeleton.md +28 -0
- package/templates/kinds/inventory.schema.json +35 -0
- package/templates/kinds/model.schema.json +99 -0
- package/templates/kinds/mutations.schema.json +65 -0
- package/templates/kinds/platform-operation-receipt.contract.json +76 -0
- package/templates/kinds/platform-operation-receipt.skeleton.md +54 -0
- package/templates/kinds/probes.schema.json +130 -0
- package/templates/kinds/proof.schema.json +17 -0
- package/templates/kinds/quality-verification.contract.json +87 -0
- package/templates/kinds/quality-verification.skeleton.md +58 -0
- package/templates/kinds/release-deployment.contract.json +85 -0
- package/templates/kinds/release-deployment.skeleton.md +67 -0
- package/templates/kinds/route.schema.json +293 -0
- package/templates/kinds/stack-model.schema.json +90 -0
- package/templates/kinds/uat-capture.schema.json +35 -0
- package/templates/kinds/uat-flow-verification.contract.json +11 -0
- package/templates/kinds/uat-flow-verification.skeleton.md +47 -0
- package/templates/kinds/uat-snapshot.schema.json +96 -0
- package/templates/kinds/uat-verdicts.schema.json +41 -0
- package/templates/kinds/ui-coverage.schema.json +77 -0
- package/templates/kinds/verdicts.schema.json +39 -0
- package/templates/kinds/workspace-route-binding.contract.json +12 -0
- package/templates/kinds/workspace-route-binding.skeleton.md +60 -0
- package/templates/kinds/writes.schema.json +37 -0
- package/templates/operator.template.md +73 -0
- package/templates/patterns.template.md +31 -0
- package/templates/step/request.schema.json +42 -0
- package/templates/step/response.schema.json +144 -0
- package/templates/ui-composition.template.md +36 -0
- package/templates/ui-presentation.template.md +57 -0
- package/templates/ui-proof.template.md +34 -0
- package/workflows/README.md +37 -0
- package/workflows/README.vi.md +37 -0
- package/workflows/backend-feature.json +59 -0
- package/workflows/content-unit.json +19 -0
- package/workflows/frontend-new-surface.json +81 -0
- package/workflows/frontend-reconstruct.json +67 -0
- package/workflows/frontend-refine.json +67 -0
- package/workflows/frontend-with-uat.json +78 -0
- package/workflows/full-feature.json +104 -0
- package/workflows/release.json +29 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# release.deploy
|
|
2
|
+
|
|
3
|
+
## Job
|
|
4
|
+
|
|
5
|
+
Deploy one immutable release to one declared target under its declared authorization and prove the
|
|
6
|
+
steady state it reached, taking the recovery or rollback branch inside the same pass rather than
|
|
7
|
+
assuming the rollout succeeded.
|
|
8
|
+
|
|
9
|
+
## Recovery and rollback are branches of this job
|
|
10
|
+
|
|
11
|
+
A rollout that does not stabilize is still this operator's problem. The run ends on one of three
|
|
12
|
+
terminals and never in the middle: the release is deployed, the previous release is restored, or the
|
|
13
|
+
work is blocked with an exact reason. A restored release is its own terminal outcome; it must never
|
|
14
|
+
be read as successful delivery of the release it rejected.
|
|
15
|
+
|
|
16
|
+
## The release is immutable and exact
|
|
17
|
+
|
|
18
|
+
A release is identified by its `sha256:` digest, not by a tag, a branch, or a build number. The
|
|
19
|
+
artifact is never rebuilt, retagged, or substituted inside this pass; if the digest cannot be
|
|
20
|
+
resolved the run blocks rather than building a replacement and calling it the same release. The
|
|
21
|
+
manifest must have been validated against exactly this release, because a manifest pinned to another
|
|
22
|
+
release is how an unreviewed image reaches a reviewed target.
|
|
23
|
+
|
|
24
|
+
## Authorization is declared, never implied
|
|
25
|
+
|
|
26
|
+
Deployment requires its own declared grant covering this project, this environment, this target and
|
|
27
|
+
the `deploy` action, still valid at the moment the target was observed. No ordinary task, no
|
|
28
|
+
precedent from a sibling project and no urgency implies it, and an unauthorized deployment does not
|
|
29
|
+
become authorized by being useful. Destructive loss, a credential rotation, or a new host, domain,
|
|
30
|
+
tenant or project leaves this operator's authority entirely and returns `APPROVAL_REQUIRED`.
|
|
31
|
+
|
|
32
|
+
## Credentials are names, never values
|
|
33
|
+
|
|
34
|
+
Handles are resolved through existing custody at the moment they are needed. A resolved value never
|
|
35
|
+
enters the plan, the manifest, the receipt, a log line, a command argument, or a message. The receipt
|
|
36
|
+
records which handles were resolved and nothing more, and no field in the contract can hold a value
|
|
37
|
+
even if someone tried: a token written where a `secret-ref://` handle belongs is rejected as
|
|
38
|
+
malformed rather than quietly carried into an argument list.
|
|
39
|
+
|
|
40
|
+
## Every effect is a compare-and-set
|
|
41
|
+
|
|
42
|
+
A step either mutates a boundary or it does not. The mutating steps are `host-prepare`,
|
|
43
|
+
`artifact-publish`, `migrate`, `domain-reconcile`, `rollout`, `recover` and `rollback`, and each
|
|
44
|
+
records the observed revision of its own boundary before and after. A desired state that already
|
|
45
|
+
matches is a proved idempotent no-op and is recorded as one; claiming an application without moving
|
|
46
|
+
a revision is refused, and a reading step that reports a revision has invented a fact about a
|
|
47
|
+
boundary it never touched. The execution root is ignored and rebuildable.
|
|
48
|
+
|
|
49
|
+
## Monitoring distinguishes progressing from failing
|
|
50
|
+
|
|
51
|
+
On this project a push to `main` triggers the workflow and boot takes roughly eight to nine minutes,
|
|
52
|
+
so `progressing` is the expected condition for most of the window and is never treated as a failure,
|
|
53
|
+
and a monitoring deadline shorter than the window it must contain produces a guaranteed false
|
|
54
|
+
failure. One transient probe never becomes recovery: a failing condition has to persist across at
|
|
55
|
+
least two observations. A release that is neither this release nor the one it replaces stops the run
|
|
56
|
+
as `CONCURRENT_DRIFT` and forces a replan; it is never recovered or rolled back as though it
|
|
57
|
+
belonged here. Recovery repeats only approved reversible actions, numbers its attempts contiguously
|
|
58
|
+
from one, preserves the same release identity, and cannot end in a deployment once exhausted.
|
|
59
|
+
Rollback is valid only when the exact safe release still exists, the current data and schema state
|
|
60
|
+
remain compatible with it, and the revision actually moved.
|
|
61
|
+
|
|
62
|
+
## Steady state is proved, not assumed
|
|
63
|
+
|
|
64
|
+
A rollout that returned without an error is not a deployment. Steady means the immutable digest is
|
|
65
|
+
active, every declared target is available, no superseded target remains active unless the strategy
|
|
66
|
+
permits it, the window elapsed in full, and every declared probe passed across the whole of it. That
|
|
67
|
+
is what turns three silent failures into detectable ones: the workflow finished while the old digest
|
|
68
|
+
is still serving traffic; one of two targets never came back and the other absorbed the load; the
|
|
69
|
+
readiness probe passed once, at the one moment it happened to be asked. At least one declared probe
|
|
70
|
+
is public, because a run observing only container health proves nothing a user could see; here the
|
|
71
|
+
GraphQL typename probe returning `200` is the readiness signal.
|
|
72
|
+
|
|
73
|
+
## The two fallbacks are ordered, and the rest terminate
|
|
74
|
+
|
|
75
|
+
A failed rollout is not the end of the run, it is the entry to the first fallback: `ROLLOUT_FAILED`
|
|
76
|
+
takes the recovery branch, which repeats only approved reversible actions against the same release
|
|
77
|
+
identity. When those run out, `RECOVERY_EXHAUSTED` takes the second fallback: rollback to
|
|
78
|
+
`rollbackIdentity` by its exact digest. Both are recorded under `## Fallbacks taken`, because a
|
|
79
|
+
branch taken silently is a branch nobody can audit. After that there is nothing left to try, so
|
|
80
|
+
`ROLLBACK_IDENTITY_MISSING`, `STEADY_STATE_UNPROVEN` and `CONCURRENT_DRIFT` terminate: a rollback
|
|
81
|
+
without its safe release, a window that never closed, and a foreign release that appeared mid-run are
|
|
82
|
+
each a state this operator must not act further on.
|
|
83
|
+
|
|
84
|
+
## Deadlines and probes have defaults, approval does not
|
|
85
|
+
|
|
86
|
+
`steadyDeadline` defaults to 600 seconds because that is the boot time this project actually shows,
|
|
87
|
+
and `probes` default to the set the validated manifest declares, so a person who names neither is
|
|
88
|
+
still measured against something real. `approval` has no default at all: changing what production
|
|
89
|
+
serves is always something a person said yes to.
|
|
90
|
+
|
|
91
|
+
## Boundary
|
|
92
|
+
|
|
93
|
+
Context is read-only apart from the declared mutations. The operator applies only the declared host,
|
|
94
|
+
migration, domain and rollout mutations against the frozen release identity, restores the exact
|
|
95
|
+
declared rollback release when that branch is taken, and writes only `response/` of its own branch:
|
|
96
|
+
`data/probes.json`, `response.md` and `response.json`. It does not log, persist, echo, or return a
|
|
97
|
+
resolved credential value; does not deploy a release the declared authorization does not cover; does
|
|
98
|
+
not rebuild, retag, or otherwise alter the immutable artifact identified by the frozen digest; does
|
|
99
|
+
not edit the intent or revalidate the manifest into something else; does not recover or roll back a
|
|
100
|
+
release that appeared during execution and does not belong to this run; does not report a rolled-back
|
|
101
|
+
run as successful delivery of the rejected release; and does not declare steady state from a single
|
|
102
|
+
probe observation or from an assumed rollout.
|
|
103
|
+
|
|
104
|
+
## Context
|
|
105
|
+
|
|
106
|
+
| Alias | Bind | Required |
|
|
107
|
+
| --- | --- | --- |
|
|
108
|
+
| `@remote/ghcr/<image>` | the immutable image by digest; a tag is never a binding | yes |
|
|
109
|
+
| `@workspaces/device-state` | credential handles by name and their custody; values never appear | yes |
|
|
110
|
+
| `@remote/github-actions/<runId>` | CI evidence of the build and the rollout, read only | no |
|
|
111
|
+
|
|
112
|
+
## Inputs
|
|
113
|
+
|
|
114
|
+
| Kind | From | Required |
|
|
115
|
+
| --- | --- | --- |
|
|
116
|
+
| `quality-verification` | `quality.verify`; verification precedes an immutable build, and it is the authorization this run stands on | yes |
|
|
117
|
+
|
|
118
|
+
## Requirements
|
|
119
|
+
|
|
120
|
+
| Field | Type | Default | Ask |
|
|
121
|
+
| --- | --- | --- | --- |
|
|
122
|
+
| `release` | id | — | The immutable release identity being deployed, with the `sha256:` digest that identifies it |
|
|
123
|
+
| `target` | id | — | The one target this deployment may change, and the environment it sits in |
|
|
124
|
+
| `approval` | id | — | The declared deploy grant covering this project, environment and target; changing what production serves always needs a person |
|
|
125
|
+
| `probes` | list of `{probeId, kind, endpointRef, expectStatus}` | the probes the validated manifest declares | What steady state is measured by; at least one probe is public |
|
|
126
|
+
| `steadyDeadline` | number | 600 | The bounded monitoring deadline in seconds, measured against the boot time this project shows |
|
|
127
|
+
| `rollbackIdentity` | `{releaseId, artifactRef, digest, dataCompatible}` | — | The exact safe release the rollback fallback restores, by digest |
|
|
128
|
+
| `resume` | token | null | The blocked branch's token when re-entering after a stop |
|
|
129
|
+
|
|
130
|
+
## Steps
|
|
131
|
+
|
|
132
|
+
| # | Step | Params | Reads | Writes | Stops with |
|
|
133
|
+
| --- | --- | --- | --- | --- | --- |
|
|
134
|
+
| 1 | Validate the gate, the authorization the input carries, and the resume | `resume` | `request/request.json`, input `quality-verification` as the authorization this run stands on | — | `INVALID_INPUT`, `AUTHORIZATION_MISSING`, `NO_PROGRESS` |
|
|
135
|
+
| 2 | Bind the release and compile the plan | `release`, `target`, `approval` | @remote/ghcr/<image> at the frozen digest, @remote/github-actions/<runId> for the observed state, @tools/git, @tools/ci | — | `MANIFEST_INVALID`, `APPROVAL_REQUIRED` |
|
|
136
|
+
| 3 | Initialize the execution root and resolve the credentials by name | — | @workspaces/device-state for the declared handles and their custody, @tools/secrets | — | `CREDENTIAL_UNAVAILABLE` |
|
|
137
|
+
| 4 | Prepare the host, publish the artifact by digest, migrate and reconcile the domain | — | @remote/ghcr/<image> for the artifact by digest, @remote/github-actions/<runId> for each boundary's revision before and after | @tools/shell | `HOST_UNAVAILABLE`, `ARTIFACT_MISSING`, `MIGRATION_BLOCKED`, `DOMAIN_UNRECONCILED` |
|
|
138
|
+
| 5 | Roll out | — | @remote/ghcr/<image> for the target revision before and after | @tools/container | `ROLLOUT_FAILED` |
|
|
139
|
+
| 6 | Monitor within the deadline, with backoff | `steadyDeadline`, `probes` | @remote/github-actions/<runId> for the probe observations across the window, @tools/http | `response/data/probes.json` | — |
|
|
140
|
+
| 7 | Detect concurrent drift before acting | — | `response/data/probes.json`, @remote/ghcr/<image> for the active release by digest | — | `CONCURRENT_DRIFT` |
|
|
141
|
+
| 8 | Take the recovery branch when the failure persists | — | `response/data/probes.json`, @remote/ghcr/<image> at the same release identity | @tools/container | `RECOVERY_EXHAUSTED` |
|
|
142
|
+
| 9 | Take the rollback branch when recovery cannot hold | `rollbackIdentity` | @remote/ghcr/<image> at the exact safe digest | @tools/container | `ROLLBACK_IDENTITY_MISSING` |
|
|
143
|
+
| 10 | Prove the steady state, write the receipt and emit | — | everything above | `response/response.md`, `response/response.json` | `STEADY_STATE_UNPROVEN` |
|
|
144
|
+
|
|
145
|
+
Steps 8 and 9 are the two fallbacks in order, not a sequence every run walks: a run enters step 8
|
|
146
|
+
only under `ROLLOUT_FAILED` and step 9 only under `RECOVERY_EXHAUSTED`, and a run that took neither
|
|
147
|
+
records the branch as `none`. A resume begins again at validation, reuses only unchanged fingerprinted
|
|
148
|
+
observations, and keeps the same release identity, because a different release is a different
|
|
149
|
+
deployment; a resume that adds no authorization, manifest, credential or observation change is
|
|
150
|
+
`NO_PROGRESS`.
|
|
151
|
+
|
|
152
|
+
## Outputs
|
|
153
|
+
|
|
154
|
+
| Kind | File | Type | Required |
|
|
155
|
+
| --- | --- | --- | --- |
|
|
156
|
+
| `release-deployment` | `response/response.md` | md | yes |
|
|
157
|
+
| `probes` | `response/data/probes.json` | data | yes |
|
|
158
|
+
|
|
159
|
+
## Stops
|
|
160
|
+
|
|
161
|
+
| Code | Disposition |
|
|
162
|
+
| --- | --- |
|
|
163
|
+
| `INVALID_INPUT` | terminate |
|
|
164
|
+
| `NO_PROGRESS` | terminate |
|
|
165
|
+
| `AUTHORIZATION_MISSING` | terminate |
|
|
166
|
+
| `MANIFEST_INVALID` | terminate |
|
|
167
|
+
| `APPROVAL_REQUIRED` | terminate |
|
|
168
|
+
| `CREDENTIAL_UNAVAILABLE` | terminate |
|
|
169
|
+
| `HOST_UNAVAILABLE` | terminate |
|
|
170
|
+
| `ARTIFACT_MISSING` | terminate |
|
|
171
|
+
| `MIGRATION_BLOCKED` | terminate |
|
|
172
|
+
| `DOMAIN_UNRECONCILED` | terminate |
|
|
173
|
+
| `ROLLOUT_FAILED` | fallback |
|
|
174
|
+
| `RECOVERY_EXHAUSTED` | fallback |
|
|
175
|
+
| `CONCURRENT_DRIFT` | terminate |
|
|
176
|
+
| `ROLLBACK_IDENTITY_MISSING` | terminate |
|
|
177
|
+
| `STEADY_STATE_UNPROVEN` | terminate |
|
|
178
|
+
|
|
179
|
+
## Next
|
|
180
|
+
|
|
181
|
+
| When | Operator |
|
|
182
|
+
| --- | --- |
|
|
183
|
+
| the host, the artifact registry, the credential custody or the safe release needs a shared runtime change | `platform.operate` |
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# release.deploy
|
|
2
|
+
|
|
3
|
+
## Việc
|
|
4
|
+
|
|
5
|
+
Deploy một release bất biến lên một target đã khai, dưới đúng thẩm quyền đã khai, rồi chứng minh
|
|
6
|
+
trạng thái ổn định mà nó đạt tới, đi nhánh recovery hay rollback ngay trong cùng lượt chạy thay vì
|
|
7
|
+
giả định rollout đã thành công.
|
|
8
|
+
|
|
9
|
+
## Recovery và rollback là nhánh của chính việc này
|
|
10
|
+
|
|
11
|
+
Một rollout không ổn định lại vẫn là vấn đề của operator này. Lượt chạy kết thúc ở đúng một trong ba
|
|
12
|
+
điểm cuối và không bao giờ dừng giữa chừng: release được deploy, release trước được khôi phục, hoặc
|
|
13
|
+
công việc bị chặn với một lý do chính xác. Một release được khôi phục là điểm cuối của riêng nó; nó
|
|
14
|
+
không bao giờ được đọc thành việc giao thành công cái release mà nó vừa từ chối.
|
|
15
|
+
|
|
16
|
+
## Release là bất biến và chính xác
|
|
17
|
+
|
|
18
|
+
Một release được nhận diện bằng digest `sha256:` của nó, không phải bằng tag, nhánh hay số build.
|
|
19
|
+
Artifact không bao giờ được build lại, gắn tag lại hay thay thế bên trong lượt này; nếu digest không
|
|
20
|
+
phân giải được thì lượt chạy dừng, chứ không build một cái thay thế rồi gọi nó là cùng một release.
|
|
21
|
+
Manifest phải đã được kiểm đúng với chính release này, vì manifest ghim vào một release khác chính là
|
|
22
|
+
cách một image chưa duyệt tới được một target đã duyệt.
|
|
23
|
+
|
|
24
|
+
## Thẩm quyền là được khai, không bao giờ được ngầm hiểu
|
|
25
|
+
|
|
26
|
+
Deploy đòi hỏi một grant khai riêng, phủ đúng project này, môi trường này, target này và hành động
|
|
27
|
+
`deploy`, và còn hiệu lực tại đúng lúc target được quan sát. Không nhiệm vụ thường nào, không tiền lệ
|
|
28
|
+
từ một project anh em nào và không sự gấp gáp nào ngầm cấp nó, và một lần deploy không có thẩm quyền
|
|
29
|
+
không trở thành có thẩm quyền chỉ vì nó hữu ích. Mất mát phá huỷ, xoay vòng credential, hay một host,
|
|
30
|
+
domain, tenant hoặc project mới đều nằm hẳn ngoài thẩm quyền của operator này và trả `APPROVAL_REQUIRED`.
|
|
31
|
+
|
|
32
|
+
## Credential là tên, không bao giờ là giá trị
|
|
33
|
+
|
|
34
|
+
Handle được phân giải qua custody đang có, đúng lúc cần. Một giá trị đã phân giải không bao giờ đi
|
|
35
|
+
vào plan, manifest, biên bản, một dòng log, một tham số lệnh hay một tin nhắn. Biên bản ghi lại
|
|
36
|
+
những handle nào đã được phân giải và không gì hơn, và không trường nào trong hợp đồng chứa nổi một
|
|
37
|
+
giá trị dù có ai cố: một token viết vào chỗ đáng lẽ là handle `secret-ref://` bị từ chối như đầu vào
|
|
38
|
+
sai dạng, thay vì lặng lẽ trôi vào một danh sách tham số.
|
|
39
|
+
|
|
40
|
+
## Mọi tác động đều là compare-and-set
|
|
41
|
+
|
|
42
|
+
Một bước hoặc có làm đổi một boundary, hoặc không. Các bước có làm đổi là `host-prepare`,
|
|
43
|
+
`artifact-publish`, `migrate`, `domain-reconcile`, `rollout`, `recover` và `rollback`, và mỗi bước
|
|
44
|
+
ghi lại revision quan sát được của chính boundary mình trước và sau. Trạng thái mong muốn mà đã trùng
|
|
45
|
+
sẵn là một no-op idempotent đã được chứng minh và được ghi đúng như vậy; tuyên bố đã áp dụng mà
|
|
46
|
+
revision không dịch chuyển thì bị từ chối, còn một bước chỉ đọc mà báo revision là bịa ra một sự thật
|
|
47
|
+
về boundary nó chưa hề chạm. Execution root là thứ bỏ đi được và dựng lại được.
|
|
48
|
+
|
|
49
|
+
## Giám sát phân biệt đang tiến và đang hỏng
|
|
50
|
+
|
|
51
|
+
Ở dự án này, push lên `main` kích hoạt workflow và boot mất khoảng tám tới chín phút, nên
|
|
52
|
+
`progressing` là điều kiện bình thường trong phần lớn cửa sổ và không bao giờ bị coi là lỗi, và một
|
|
53
|
+
deadline giám sát ngắn hơn cửa sổ mà nó phải chứa sẽ sinh ra một lỗi giả chắc chắn. Một lần probe
|
|
54
|
+
chớp nhoáng không bao giờ thành recovery: điều kiện hỏng phải kéo dài qua ít nhất hai lần quan sát.
|
|
55
|
+
Một release không phải release này và cũng không phải release nó thay thế sẽ dừng lượt chạy bằng
|
|
56
|
+
`CONCURRENT_DRIFT` và buộc lập kế hoạch lại; nó không bao giờ được recover hay rollback như thể nó
|
|
57
|
+
thuộc về đây. Recovery chỉ lặp lại những hành động đảo ngược được đã duyệt, đánh số các lần thử liền
|
|
58
|
+
mạch từ một, giữ nguyên danh tính release, và một khi đã cạn thì không thể kết thúc bằng một lần
|
|
59
|
+
deploy. Rollback chỉ hợp lệ khi release an toàn chính xác vẫn còn, trạng thái dữ liệu và schema hiện
|
|
60
|
+
tại vẫn tương thích với nó, và revision thực sự đã dịch chuyển.
|
|
61
|
+
|
|
62
|
+
## Trạng thái ổn định là được chứng minh, không phải được giả định
|
|
63
|
+
|
|
64
|
+
Một rollout trả về không lỗi chưa phải một lần deploy. Ổn định nghĩa là digest bất biến đang active,
|
|
65
|
+
mọi target đã khai đều sẵn sàng, không target bị thay thế nào còn active trừ khi chiến lược cho phép,
|
|
66
|
+
cửa sổ đã trôi qua trọn vẹn, và mọi probe đã khai đều pass suốt cửa sổ đó. Đó là thứ biến ba lỗi
|
|
67
|
+
im lặng thành ba lỗi phát hiện được: workflow chạy xong trong khi digest cũ vẫn đang phục vụ traffic;
|
|
68
|
+
một trong hai target không bao giờ quay lại còn cái kia gánh hết tải; probe readiness pass đúng một
|
|
69
|
+
lần, đúng khoảnh khắc nó tình cờ được hỏi. Ít nhất một probe đã khai phải là probe công khai, vì một
|
|
70
|
+
lượt chạy chỉ quan sát sức khoẻ container thì chẳng chứng minh được gì mà người dùng thấy được; ở đây
|
|
71
|
+
probe GraphQL typename trả `200` là tín hiệu readiness.
|
|
72
|
+
|
|
73
|
+
## Hai fallback có thứ tự, phần còn lại thì terminate
|
|
74
|
+
|
|
75
|
+
Một rollout hỏng không phải là hết lượt, nó là cửa vào fallback thứ nhất: `ROLLOUT_FAILED` đi nhánh
|
|
76
|
+
phục hồi, nhánh chỉ lặp lại những hành động đảo ngược được đã duyệt trên cùng danh tính release. Khi
|
|
77
|
+
chúng cạn, `RECOVERY_EXHAUSTED` đi fallback thứ hai: rollback về `rollbackIdentity` theo đúng digest
|
|
78
|
+
của nó. Cả hai đều được ghi dưới `## Fallbacks taken`, vì một nhánh đi âm thầm là một nhánh không ai
|
|
79
|
+
soát lại được. Sau đó không còn gì để thử nữa, nên `ROLLBACK_IDENTITY_MISSING`, `STEADY_STATE_UNPROVEN`
|
|
80
|
+
và `CONCURRENT_DRIFT` terminate: một lần rollback không còn release an toàn, một cửa sổ không bao giờ
|
|
81
|
+
khép, và một release lạ xuất hiện giữa lượt, mỗi thứ đều là trạng thái operator này không được hành
|
|
82
|
+
động tiếp lên.
|
|
83
|
+
|
|
84
|
+
## Deadline và probe có mặc định, phê duyệt thì không
|
|
85
|
+
|
|
86
|
+
`steadyDeadline` mặc định 600 giây vì đó là thời gian boot mà dự án này thực sự cho thấy, còn `probes`
|
|
87
|
+
mặc định lấy bộ mà manifest đã kiểm khai ra, nên người không nêu cái nào vẫn được đo theo một thứ có
|
|
88
|
+
thật. `approval` thì hoàn toàn không có mặc định: đổi thứ mà production đang phục vụ luôn là việc có
|
|
89
|
+
người nói đồng ý.
|
|
90
|
+
|
|
91
|
+
## Ranh giới ghi
|
|
92
|
+
|
|
93
|
+
Context chỉ đọc, trừ các mutation đã khai. Operator chỉ áp các mutation host, migration, domain và
|
|
94
|
+
rollout đã khai lên đúng danh tính release đã đóng băng, khôi phục đúng release rollback đã khai khi
|
|
95
|
+
đi nhánh đó, và chỉ ghi `response/` của nhánh mình: `data/probes.json`, `response.md` và
|
|
96
|
+
`response.json`. Nó không log, lưu, in ra hay trả về một giá trị credential đã phân giải; không deploy
|
|
97
|
+
một release mà thẩm quyền đã khai không phủ; không build lại, gắn tag lại hay thay đổi artifact bất
|
|
98
|
+
biến mà digest đóng băng chỉ tới; không sửa intent hay kiểm lại manifest thành một thứ khác; không
|
|
99
|
+
recover hay rollback một release xuất hiện giữa lượt và không thuộc về lượt này; không báo một lượt đã
|
|
100
|
+
rollback là đã giao thành công cái release bị từ chối; và không tuyên bố trạng thái ổn định từ một lần
|
|
101
|
+
quan sát probe duy nhất hay từ một rollout được giả định.
|
|
102
|
+
|
|
103
|
+
## Context
|
|
104
|
+
|
|
105
|
+
| Alias | Bind | Bắt buộc |
|
|
106
|
+
| --- | --- | --- |
|
|
107
|
+
| `@remote/ghcr/<image>` | image bất biến theo digest; một tag không bao giờ là ràng buộc | có |
|
|
108
|
+
| `@workspaces/device-state` | handle credential theo tên và custody của chúng; giá trị không bao giờ xuất hiện | có |
|
|
109
|
+
| `@remote/github-actions/<runId>` | bằng chứng CI của build và rollout, chỉ đọc | không |
|
|
110
|
+
|
|
111
|
+
## Đầu vào
|
|
112
|
+
|
|
113
|
+
| Kind | Từ đâu | Bắt buộc |
|
|
114
|
+
| --- | --- | --- |
|
|
115
|
+
| `quality-verification` | `quality.verify`; kiểm định đi trước một bản build bất biến, và nó là thẩm quyền mà lượt chạy này đứng lên | có |
|
|
116
|
+
|
|
117
|
+
## Yêu cầu
|
|
118
|
+
|
|
119
|
+
| Field | Kiểu | Mặc định | Hỏi |
|
|
120
|
+
| --- | --- | --- | --- |
|
|
121
|
+
| `release` | id | — | Danh tính release bất biến đang được deploy, kèm digest `sha256:` nhận diện nó |
|
|
122
|
+
| `target` | id | — | Đúng một target mà lần deploy này được đổi, cùng môi trường nó nằm trong |
|
|
123
|
+
| `approval` | id | — | Grant deploy đã khai phủ project, môi trường và target này; đổi thứ production đang phục vụ luôn cần một con người |
|
|
124
|
+
| `probes` | list of `{probeId, kind, endpointRef, expectStatus}` | the probes the validated manifest declares | Trạng thái ổn định được đo bằng gì; ít nhất một probe là công khai |
|
|
125
|
+
| `steadyDeadline` | number | 600 | Deadline giám sát có chặn trên, tính bằng giây, đo theo thời gian boot mà dự án này cho thấy |
|
|
126
|
+
| `rollbackIdentity` | `{releaseId, artifactRef, digest, dataCompatible}` | — | Đúng release an toàn mà fallback rollback khôi phục, theo digest |
|
|
127
|
+
| `resume` | token | null | Token của nhánh bị chặn khi vào lại sau một mã dừng |
|
|
128
|
+
|
|
129
|
+
## Các bước
|
|
130
|
+
|
|
131
|
+
| # | Bước | Tham số | Đọc | Ghi | Dừng với |
|
|
132
|
+
| --- | --- | --- | --- | --- | --- |
|
|
133
|
+
| 1 | Kiểm cổng vào, thẩm quyền mà Đầu vào mang, và resume | `resume` | `request/request.json`, Đầu vào `quality-verification` là thẩm quyền lượt chạy này đứng lên | — | `INVALID_INPUT`, `AUTHORIZATION_MISSING`, `NO_PROGRESS` |
|
|
134
|
+
| 2 | Ràng release và biên dịch kế hoạch | `release`, `target`, `approval` | @remote/ghcr/<image> tại digest đã đóng băng, @remote/github-actions/<runId> cho trạng thái quan sát được, @tools/git, @tools/ci | — | `MANIFEST_INVALID`, `APPROVAL_REQUIRED` |
|
|
135
|
+
| 3 | Khởi tạo execution root và phân giải credential theo tên | — | @workspaces/device-state cho các handle đã khai và custody của chúng, @tools/secrets | — | `CREDENTIAL_UNAVAILABLE` |
|
|
136
|
+
| 4 | Chuẩn bị host, publish artifact theo digest, migrate và đối chiếu domain | — | @remote/ghcr/<image> cho artifact theo digest, @remote/github-actions/<runId> cho revision trước và sau của từng boundary | @tools/shell | `HOST_UNAVAILABLE`, `ARTIFACT_MISSING`, `MIGRATION_BLOCKED`, `DOMAIN_UNRECONCILED` |
|
|
137
|
+
| 5 | Roll out | — | @remote/ghcr/<image> cho revision của target trước và sau | @tools/container | `ROLLOUT_FAILED` |
|
|
138
|
+
| 6 | Theo dõi trong deadline, có backoff | `steadyDeadline`, `probes` | @remote/github-actions/<runId> cho các quan sát probe suốt cửa sổ, @tools/http | `response/data/probes.json` | — |
|
|
139
|
+
| 7 | Phát hiện drift đồng thời trước khi hành động | — | `response/data/probes.json`, @remote/ghcr/<image> cho release đang active theo digest | — | `CONCURRENT_DRIFT` |
|
|
140
|
+
| 8 | Đi nhánh phục hồi khi lỗi còn dai dẳng | — | `response/data/probes.json`, @remote/ghcr/<image> tại cùng danh tính release | @tools/container | `RECOVERY_EXHAUSTED` |
|
|
141
|
+
| 9 | Đi nhánh rollback khi phục hồi không giữ được | `rollbackIdentity` | @remote/ghcr/<image> tại đúng digest an toàn | @tools/container | `ROLLBACK_IDENTITY_MISSING` |
|
|
142
|
+
| 10 | Chứng minh trạng thái ổn định, viết biên bản và phát | — | mọi thứ ở trên | `response/response.md`, `response/response.json` | `STEADY_STATE_UNPROVEN` |
|
|
143
|
+
|
|
144
|
+
Bước 8 và 9 là hai fallback theo thứ tự, không phải một chuỗi mà lượt chạy nào cũng đi qua: một lượt
|
|
145
|
+
chỉ vào bước 8 dưới `ROLLOUT_FAILED` và chỉ vào bước 9 dưới `RECOVERY_EXHAUSTED`, còn lượt không đi
|
|
146
|
+
cái nào thì ghi nhánh là `none`. Một lần resume bắt đầu lại từ cổng vào, chỉ dùng lại quan sát có
|
|
147
|
+
fingerprint không đổi, và giữ nguyên danh tính release, vì một release khác là một lần deploy khác;
|
|
148
|
+
một lần resume không thêm thẩm quyền, manifest, credential hay quan sát nào là `NO_PROGRESS`.
|
|
149
|
+
|
|
150
|
+
## Đầu ra
|
|
151
|
+
|
|
152
|
+
| Kind | File | Kiểu | Bắt buộc |
|
|
153
|
+
| --- | --- | --- | --- |
|
|
154
|
+
| `release-deployment` | `response/response.md` | md | có |
|
|
155
|
+
| `probes` | `response/data/probes.json` | data | có |
|
|
156
|
+
|
|
157
|
+
## Dừng
|
|
158
|
+
|
|
159
|
+
| Code | Xử lý |
|
|
160
|
+
| --- | --- |
|
|
161
|
+
| `INVALID_INPUT` | terminate |
|
|
162
|
+
| `NO_PROGRESS` | terminate |
|
|
163
|
+
| `AUTHORIZATION_MISSING` | terminate |
|
|
164
|
+
| `MANIFEST_INVALID` | terminate |
|
|
165
|
+
| `APPROVAL_REQUIRED` | terminate |
|
|
166
|
+
| `CREDENTIAL_UNAVAILABLE` | terminate |
|
|
167
|
+
| `HOST_UNAVAILABLE` | terminate |
|
|
168
|
+
| `ARTIFACT_MISSING` | terminate |
|
|
169
|
+
| `MIGRATION_BLOCKED` | terminate |
|
|
170
|
+
| `DOMAIN_UNRECONCILED` | terminate |
|
|
171
|
+
| `ROLLOUT_FAILED` | fallback |
|
|
172
|
+
| `RECOVERY_EXHAUSTED` | fallback |
|
|
173
|
+
| `CONCURRENT_DRIFT` | terminate |
|
|
174
|
+
| `ROLLBACK_IDENTITY_MISSING` | terminate |
|
|
175
|
+
| `STEADY_STATE_UNPROVEN` | terminate |
|
|
176
|
+
|
|
177
|
+
## Kế tiếp
|
|
178
|
+
|
|
179
|
+
| Khi | Operator |
|
|
180
|
+
| --- | --- |
|
|
181
|
+
| host, registry artifact, custody credential hay release an toàn cần một thay đổi runtime dùng chung | `platform.operate` |
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// Proves validate.mjs on a synthetic session branch: one steady deployment, one that took the
|
|
2
|
+
// recovery fallback, one that took both fallbacks and ended rolled back, one blocked on an unproven
|
|
3
|
+
// steady state, and one mutation per law, each of which must fail with a line that names the defect.
|
|
4
|
+
import assert from 'node:assert/strict';
|
|
5
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { validateReleaseStep } from './validate.mjs';
|
|
9
|
+
|
|
10
|
+
const RELEASE = 'release:2026.01.10-1';
|
|
11
|
+
const PREVIOUS = 'release:2026.01.03-2';
|
|
12
|
+
const DIGEST = `sha256:${'1'.repeat(64)}`;
|
|
13
|
+
const SAFE_DIGEST = `sha256:${'2'.repeat(64)}`;
|
|
14
|
+
const TARGET = 'production/api';
|
|
15
|
+
const APPROVAL = '@worktrees/businesses/features/release/model.json#deploy-grant';
|
|
16
|
+
const MANIFEST = '.stacks/production/api.manifest.json';
|
|
17
|
+
const ARTIFACT = '@remote/ghcr/starci/academy-api';
|
|
18
|
+
const DEADLINE = 600;
|
|
19
|
+
const PROBES = [
|
|
20
|
+
{ probeId: 'public-graphql-typename', kind: 'public', endpointRef: 'https://api.example/graphql', expectStatus: 200 },
|
|
21
|
+
];
|
|
22
|
+
const ROLLBACK = { releaseId: PREVIOUS, artifactRef: ARTIFACT, digest: SAFE_DIGEST, dataCompatible: true };
|
|
23
|
+
|
|
24
|
+
const observation = (at, condition, status = 'pass') => ({
|
|
25
|
+
observedAt: at, condition, activeReleaseIds: [RELEASE], activeDigest: DIGEST, availableTargets: 1,
|
|
26
|
+
probeResults: [{ probeId: 'public-graphql-typename', status, observedStatus: status === 'pass' ? 200 : 502, observedAt: at }],
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const probesJson = ({ observations = [observation('2026-01-10T00:01:00.000Z', 'progressing'), observation('2026-01-10T00:07:00.000Z', 'steady')], finalCondition = 'steady', deadlineSeconds = DEADLINE, elapsedSeconds = 420, backoffSeconds = 30 } = {}) => ({
|
|
30
|
+
deadlineSeconds, elapsedSeconds, backoffSeconds, observations, finalCondition,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const NL = String.fromCharCode(10);
|
|
34
|
+
const STEPS = [
|
|
35
|
+
['`authorize`', 'applied', '—', '—', 'the declared grant covers this project, environment and target'],
|
|
36
|
+
['`rollout`', 'applied', '4', '5', 'the target moved to the immutable digest'],
|
|
37
|
+
['`monitor`', 'applied', '—', '—', 'the window was observed to its end'],
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
function responseMd({
|
|
41
|
+
release = RELEASE, digest = DIGEST, target = TARGET, approval = APPROVAL, deadline = DEADLINE,
|
|
42
|
+
rollbackId = PREVIOUS, outcome = 'deployed', branch = 'none', steps = STEPS,
|
|
43
|
+
monitoring = { Deadline: DEADLINE, Elapsed: 420, Backoff: 30, 'Final condition': 'steady' },
|
|
44
|
+
steady = { 'Active digest': DIGEST, 'Available targets': '1 of 1', 'Superseded active': '0', 'Window elapsed': '300' },
|
|
45
|
+
fallbacksTaken = [],
|
|
46
|
+
} = {}) {
|
|
47
|
+
return `# release-deployment — ${release}
|
|
48
|
+
|
|
49
|
+
The immutable release reached its declared target under the declared grant, and the steady state
|
|
50
|
+
rests on the observed digest, the available targets and every declared probe across the window.
|
|
51
|
+
|
|
52
|
+
## Binding
|
|
53
|
+
|
|
54
|
+
| Field | Value |
|
|
55
|
+
| --- | --- |
|
|
56
|
+
| Operator | \`release.deploy\` |
|
|
57
|
+
| Step | \`step-1/parallel-1\` |
|
|
58
|
+
| Project | \`starci-academy\` |
|
|
59
|
+
| Release | ${release} |
|
|
60
|
+
| Artifact | \`${ARTIFACT}\` |
|
|
61
|
+
| Digest | \`${digest}\` |
|
|
62
|
+
| Target | ${target} |
|
|
63
|
+
| Environment | production |
|
|
64
|
+
| Replaced release | \`${PREVIOUS}\` |
|
|
65
|
+
| Approval | ${approval} |
|
|
66
|
+
| Manifest | \`${MANIFEST}\` |
|
|
67
|
+
| Steady deadline | ${deadline} |
|
|
68
|
+
| Rollback identity | ${rollbackId} |
|
|
69
|
+
|
|
70
|
+
## Outcome
|
|
71
|
+
|
|
72
|
+
| Field | Value |
|
|
73
|
+
| --- | --- |
|
|
74
|
+
| Outcome | ${outcome} |
|
|
75
|
+
| Branch | ${branch} |
|
|
76
|
+
|
|
77
|
+
## Steps
|
|
78
|
+
|
|
79
|
+
| Step | State | Revision before | Revision after | Statement |
|
|
80
|
+
| --- | --- | --- | --- | --- |
|
|
81
|
+
${steps.map((s) => `| ${s[0]} | ${s[1]} | ${s[2]} | ${s[3]} | ${s[4]} |`).join('\n')}
|
|
82
|
+
|
|
83
|
+
## Monitoring
|
|
84
|
+
|
|
85
|
+
| Field | Value |
|
|
86
|
+
| --- | --- |
|
|
87
|
+
| Deadline | ${monitoring.Deadline} |
|
|
88
|
+
| Elapsed | ${monitoring.Elapsed} |
|
|
89
|
+
| Backoff | ${monitoring.Backoff} |
|
|
90
|
+
| Final condition | ${monitoring['Final condition']} |
|
|
91
|
+
|
|
92
|
+
## Steady state
|
|
93
|
+
|
|
94
|
+
| Metric | Value |
|
|
95
|
+
| --- | --- |
|
|
96
|
+
${Object.entries(steady).map(([k, v]) => `| ${k} | ${/^sha256:/.test(String(v)) ? `\`${v}\`` : v} |`).join('\n')}
|
|
97
|
+
|
|
98
|
+
## Findings
|
|
99
|
+
|
|
100
|
+
| Code | Step | Statement |
|
|
101
|
+
| --- | --- | --- |
|
|
102
|
+
| \`IDEMPOTENT_NO_OP\` | \`host-prepare\` | the host already matched the declaration |
|
|
103
|
+
|
|
104
|
+
## Fallbacks taken
|
|
105
|
+
|
|
106
|
+
| Code | Action |
|
|
107
|
+
| --- | --- |
|
|
108
|
+
${fallbacksTaken.map((c) => `| \`${c}\` | the branch was taken and recorded |`).join(NL)}
|
|
109
|
+
`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const requestJson = ({ release = RELEASE, target = TARGET, approval = APPROVAL, probes = PROBES, deadline = DEADLINE, rollbackIdentity = ROLLBACK, extra = {} } = {}) => ({
|
|
113
|
+
schemaVersion: 9, operatorId: 'release.deploy', step: 1, parallel: 1, sessionId: 's-test',
|
|
114
|
+
contexts: [{ alias: '@remote/ghcr/starci/academy-api', head: null }],
|
|
115
|
+
requirements: { release, target, approval, probes, steadyDeadline: deadline, rollbackIdentity, resume: null, ...extra },
|
|
116
|
+
inputs: { 'quality-verification': 'step-1/parallel-1/response/quality.md' }, resume: null,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const responseJson = ({ status = 'done', stop, fallbacks = [], next = ['platform.operate'] } = {}) => ({
|
|
120
|
+
schemaVersion: 9, operatorId: 'release.deploy', step: 1, parallel: 1, status, ...(stop ? { stop } : {}),
|
|
121
|
+
fallbacks,
|
|
122
|
+
fields: status === 'blocked' ? {} : { 'release-deployment': 'response/response.md', probes: 'response/data/probes.json' },
|
|
123
|
+
commits: [], next,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
function writeBranch(files) {
|
|
127
|
+
const session = mkdtempSync(path.join(tmpdir(), 'release-session-'));
|
|
128
|
+
const branch = path.join(session, 'step-1', 'parallel-1');
|
|
129
|
+
for (const d of ['request', 'response/data', 'response/artifacts']) mkdirSync(path.join(branch, d), { recursive: true });
|
|
130
|
+
writeFileSync(path.join(session, 'state.json'), JSON.stringify({ id: 's-test', chain: [['1/1']], steps: { '1/1': 'release.deploy' }, current: '1/1', status: 'running' }));
|
|
131
|
+
writeFileSync(path.join(branch, 'response', 'quality.md'), '# quality\n');
|
|
132
|
+
for (const [name, content] of Object.entries(files)) {
|
|
133
|
+
if (content === null) continue;
|
|
134
|
+
writeFileSync(path.join(branch, name), typeof content === 'string' ? content : JSON.stringify(content, null, 2));
|
|
135
|
+
}
|
|
136
|
+
return { branch, session };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const baseline = (over = {}) => ({
|
|
140
|
+
'request/request.json': requestJson(),
|
|
141
|
+
'response/response.json': responseJson(),
|
|
142
|
+
'response/response.md': responseMd(),
|
|
143
|
+
'response/data/probes.json': probesJson(),
|
|
144
|
+
...over,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
// The recovery fallback: two failing observations, then steady again on the same release.
|
|
149
|
+
const recovered = () => baseline({
|
|
150
|
+
'response/response.json': responseJson({ fallbacks: ['ROLLOUT_FAILED'] }),
|
|
151
|
+
'response/data/probes.json': probesJson({
|
|
152
|
+
observations: [observation('2026-01-10T00:01:00.000Z', 'failing', 'fail'), observation('2026-01-10T00:03:00.000Z', 'failing', 'fail'), observation('2026-01-10T00:07:00.000Z', 'steady')],
|
|
153
|
+
}),
|
|
154
|
+
'response/response.md': responseMd({
|
|
155
|
+
branch: 'recover',
|
|
156
|
+
steps: [...STEPS, ['`recover`', 'applied', '5', '6', 'the approved reversible action restored the target']],
|
|
157
|
+
fallbacksTaken: ['ROLLOUT_FAILED'],
|
|
158
|
+
}),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Both fallbacks in order: the recovery ran out and the safe release was restored by digest.
|
|
162
|
+
const rolledBack = () => baseline({
|
|
163
|
+
'response/response.json': responseJson({ fallbacks: ['ROLLOUT_FAILED', 'RECOVERY_EXHAUSTED'] }),
|
|
164
|
+
'response/data/probes.json': probesJson({
|
|
165
|
+
observations: [observation('2026-01-10T00:01:00.000Z', 'failing', 'fail'), observation('2026-01-10T00:03:00.000Z', 'failing', 'fail'), observation('2026-01-10T00:09:00.000Z', 'steady')],
|
|
166
|
+
}),
|
|
167
|
+
'response/response.md': responseMd({
|
|
168
|
+
outcome: 'rolled-back', branch: 'rollback',
|
|
169
|
+
steps: [...STEPS, ['`recover`', 'applied', '5', '6', 'the approved actions ran out'], ['`rollback`', 'applied', '6', '7', 'the safe release was restored by digest']],
|
|
170
|
+
steady: { 'Active digest': SAFE_DIGEST, 'Available targets': '1 of 1', 'Superseded active': '0', 'Window elapsed': '300' },
|
|
171
|
+
fallbacksTaken: ['ROLLOUT_FAILED', 'RECOVERY_EXHAUSTED'],
|
|
172
|
+
}),
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
async function expectValid(files, label) {
|
|
176
|
+
const { branch, session } = writeBranch(files);
|
|
177
|
+
const { errors } = await validateReleaseStep(branch);
|
|
178
|
+
rmSync(session, { recursive: true, force: true });
|
|
179
|
+
assert.deepEqual(errors, [], `${label} should be valid`);
|
|
180
|
+
}
|
|
181
|
+
async function expectError(files, needle, label) {
|
|
182
|
+
const { branch, session } = writeBranch(files);
|
|
183
|
+
const { errors } = await validateReleaseStep(branch);
|
|
184
|
+
rmSync(session, { recursive: true, force: true });
|
|
185
|
+
assert.ok(errors.some((e) => e.includes(needle)), `${label}: expected an error containing "${needle}", got:\n${errors.join('\n') || '(none)'}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
await expectValid(baseline(), 'a steady deployment of the immutable release');
|
|
189
|
+
await expectValid(recovered(), 'the recovery fallback taken after a persistent failure');
|
|
190
|
+
await expectValid(rolledBack(), 'both fallbacks in order, ending in a restored release');
|
|
191
|
+
await expectValid({
|
|
192
|
+
'request/request.json': requestJson(),
|
|
193
|
+
'response/response.json': responseJson({ status: 'blocked', stop: 'STEADY_STATE_UNPROVEN', next: [] }),
|
|
194
|
+
'response/response.md': null, 'response/data/probes.json': null,
|
|
195
|
+
}, 'blocked because the steady window never closed');
|
|
196
|
+
|
|
197
|
+
await expectError(baseline({ 'response/response.json': { ...responseJson(), stop: 'ROLLOUT_FAILED' } }), 'only a blocked response carries a stop', 'done with a stop');
|
|
198
|
+
await expectError(baseline({ 'response/response.json': responseJson({ status: 'blocked', stop: 'ROLLOUT_FAILED', next: [] }) }), 'has disposition fallback under these requirements', 'a fallback code used to block the branch');
|
|
199
|
+
await expectError(baseline({ 'response/response.json': responseJson({ fallbacks: ['STEADY_STATE_UNPROVEN'] }) }), 'cannot be taken as a fallback', 'a terminate code taken as a fallback');
|
|
200
|
+
await expectError(baseline({
|
|
201
|
+
'response/response.json': responseJson({ fallbacks: ['RECOVERY_EXHAUSTED'] }),
|
|
202
|
+
'response/response.md': responseMd({ outcome: 'rolled-back', branch: 'rollback', steps: [...STEPS, ['`rollback`', 'applied', '6', '7', 'restored']], steady: { 'Active digest': SAFE_DIGEST, 'Available targets': '1 of 1', 'Superseded active': '0', 'Window elapsed': '300' }, fallbacksTaken: ['RECOVERY_EXHAUSTED'] }),
|
|
203
|
+
}), 'reached only through an exhausted recovery', 'a rollback that skipped the recovery branch');
|
|
204
|
+
await expectError(baseline({ 'request/request.json': requestJson({ extra: { strategy: 'rolling' } }) }), 'requirements.strategy is not a field', 'a field the operator no longer declares');
|
|
205
|
+
await expectError(baseline({ 'request/request.json': requestJson({ approval: null }) }), 'required field approval has no value', 'a production deploy nobody approved');
|
|
206
|
+
await expectError(baseline({ 'request/request.json': requestJson({ probes: [{ probeId: 'container-health', kind: 'internal', endpointRef: 'unix:///health', expectStatus: 200 }] }), 'response/data/probes.json': probesJson({ observations: [{ ...observation('2026-01-10T00:01:00.000Z', 'progressing'), probeResults: [{ probeId: 'container-health', status: 'pass', observedStatus: 200, observedAt: '2026-01-10T00:01:00.000Z' }] }, { ...observation('2026-01-10T00:07:00.000Z', 'steady'), probeResults: [{ probeId: 'container-health', status: 'pass', observedStatus: 200, observedAt: '2026-01-10T00:07:00.000Z' }] }] }) }), 'at least one declared probe is public', 'a run proving only container health');
|
|
207
|
+
await expectError(baseline({ 'request/request.json': requestJson({ rollbackIdentity: { ...ROLLBACK, digest: 'v1.4.0' } }) }), 'a rollback by tag restores whatever the tag now points at', 'a rollback identity named by tag');
|
|
208
|
+
await expectError(baseline({ 'request/request.json': requestJson({ extra: { release: RELEASE }, target: TARGET, approval: 'ghp_abcdefghijklmnopqrstuvwxyz' }) }), 'looks like a resolved credential value', 'a token where a handle belongs');
|
|
209
|
+
await expectError(baseline({ 'request/request.json': requestJson({ deadline: 120 }) }), 'but the request pinned 120s', 'monitoring measured against another deadline');
|
|
210
|
+
await expectError(baseline({ 'response/data/probes.json': probesJson({ elapsedSeconds: 900 }) }), 'ran past its own bounded deadline', 'monitoring that outran its deadline');
|
|
211
|
+
await expectError(baseline({ 'response/data/probes.json': probesJson({ backoffSeconds: 900 }) }), 'backoff cannot exceed the deadline', 'a backoff wider than the window');
|
|
212
|
+
await expectError({
|
|
213
|
+
...recovered(),
|
|
214
|
+
'response/data/probes.json': probesJson({ observations: [observation('2026-01-10T00:01:00.000Z', 'failing', 'fail'), observation('2026-01-10T00:07:00.000Z', 'steady')] }),
|
|
215
|
+
}, 'one transient probe is not a persistent failure', 'recovery entered on a single failing probe');
|
|
216
|
+
await expectError(baseline({ 'response/data/probes.json': probesJson({ finalCondition: 'deadline-exceeded' }) }), 'which is STEADY_STATE_UNPROVEN, not a deployment', 'a deployment over an exceeded deadline');
|
|
217
|
+
await expectError(baseline({ 'response/response.md': responseMd({ release: 'release:someone-elses' }) }), 'but the request bound', 'a receipt for another release');
|
|
218
|
+
await expectError(baseline({ 'response/response.md': responseMd({ digest: 'latest' }) }), 'identified by its digest, never by a tag', 'a release named by tag');
|
|
219
|
+
await expectError(baseline({ 'response/response.md': responseMd({ approval: '`someone-elses-grant`' }) }), 'names an approval the request did not bind', 'a grant borrowed from another deployment');
|
|
220
|
+
await expectError(baseline({ 'response/response.md': responseMd({ branch: 'recover' }) }), 'but the fallbacks taken say none', 'a branch the fallbacks never recorded');
|
|
221
|
+
await expectError({
|
|
222
|
+
...rolledBack(),
|
|
223
|
+
'response/response.md': responseMd({ outcome: 'deployed', branch: 'rollback', steps: [...STEPS, ['`recover`', 'applied', '5', '6', 'x'], ['`rollback`', 'applied', '6', '7', 'y']], fallbacksTaken: ['ROLLOUT_FAILED', 'RECOVERY_EXHAUSTED'] }),
|
|
224
|
+
}, 'a restored release is its own terminal', 'a rollback reported as delivery');
|
|
225
|
+
await expectError(baseline({ 'response/response.md': responseMd({ steps: [['`authorize`', 'applied', '—', '—', 'x'], ['`rollout`', 'applied', '4', '4', 'y'], ['`monitor`', 'applied', '—', '—', 'z']] }) }), 'claims it applied without moving a revision', 'an application that moved nothing');
|
|
226
|
+
await expectError(baseline({ 'response/response.md': responseMd({ steps: [['`authorize`', 'applied', '2', '3', 'x'], ['`rollout`', 'applied', '4', '5', 'y'], ['`monitor`', 'applied', '—', '—', 'z']] }) }), 'reports a revision for a boundary it never touched', 'a reading step inventing a revision');
|
|
227
|
+
await expectError(baseline({ 'response/response.md': responseMd({ steady: { 'Active digest': SAFE_DIGEST, 'Available targets': '1 of 1', 'Superseded active': '0', 'Window elapsed': '300' } }) }), 'while another digest is active', 'a deployment over the old digest');
|
|
228
|
+
await expectError(baseline({ 'response/response.md': responseMd({ steady: { 'Active digest': DIGEST, 'Available targets': '1 of 2', 'Superseded active': '0', 'Window elapsed': '300' } }) }), 'a partly available target set is not steady state', 'one target absorbing the load for two');
|
|
229
|
+
await expectError(baseline({ 'response/response.md': responseMd({ steady: { 'Active digest': DIGEST, 'Available targets': '1 of 1', 'Superseded active': '1', 'Window elapsed': '300' } }) }), 'still serving traffic', 'a superseded target left active');
|
|
230
|
+
await expectError(baseline({ 'response/data/probes.json': probesJson({ observations: [observation('2026-01-10T00:07:00.000Z', 'steady')] }) }), 'claimed from a single observation', 'steady state read off one probe');
|
|
231
|
+
await expectError(baseline({ 'response/response.md': responseMd({ monitoring: { Deadline: DEADLINE, Elapsed: 420, Backoff: 30, 'Final condition': 'progressing' } }) }), 'but the series ended steady', 'a receipt disagreeing with its own series');
|
|
232
|
+
await expectError(baseline({ 'response/data/probes.json': { ...probesJson(), finalCondition: 'nope' } }), 'finalCondition', 'probes schema');
|
|
233
|
+
await expectError(baseline({ 'response/response.md': responseMd().replace('## Steady state', '## Stable state') }), 'missing section ^## Steady state$', 'receipt section renamed');
|
|
234
|
+
|
|
235
|
+
process.stdout.write('release.deploy self-test: 4 valid branches, 26 rejected mutations\n');
|