@intentius/chant 0.91.0 → 0.93.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/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts +6 -0
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/workspace-tools.d.ts +2 -1
- package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +12 -2
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/pinned-upgrade.d.ts.map +1 -1
- package/dist/config.d.ts +22 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/lexicon-module.d.ts +22 -0
- package/dist/lexicon-module.d.ts.map +1 -1
- package/dist/lifecycle/converge-ledger.d.ts +19 -0
- package/dist/lifecycle/converge-ledger.d.ts.map +1 -1
- package/dist/lifecycle/run-ledger.d.ts +2 -0
- package/dist/lifecycle/run-ledger.d.ts.map +1 -1
- package/dist/lifecycle/symptoms.d.ts +51 -0
- package/dist/lifecycle/symptoms.d.ts.map +1 -1
- package/dist/lint/rules/index.d.ts +4 -2
- package/dist/lint/rules/index.d.ts.map +1 -1
- package/dist/lint/rules/op/index.d.ts +14 -3
- package/dist/lint/rules/op/index.d.ts.map +1 -1
- package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts.map +1 -1
- package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts +29 -0
- package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts.map +1 -0
- package/dist/lint/rules/sys001-no-literal-key.d.ts +3 -0
- package/dist/lint/rules/sys001-no-literal-key.d.ts.map +1 -0
- package/dist/op/__fixtures__/decide-stub-backend.d.ts +53 -0
- package/dist/op/__fixtures__/decide-stub-backend.d.ts.map +1 -0
- package/dist/op/activities/activity-contracts.d.ts +201 -0
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/apply.d.ts.map +1 -1
- package/dist/op/activities/change-coverage.d.ts +44 -0
- package/dist/op/activities/change-coverage.d.ts.map +1 -0
- package/dist/op/activities/converge.d.ts +24 -1
- package/dist/op/activities/converge.d.ts.map +1 -1
- package/dist/op/activities/decide.d.ts +107 -0
- package/dist/op/activities/decide.d.ts.map +1 -0
- package/dist/op/activities/index.d.ts +10 -0
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/shell.d.ts +32 -0
- package/dist/op/activities/shell.d.ts.map +1 -1
- package/dist/op/activities/source-release.d.ts +86 -0
- package/dist/op/activities/source-release.d.ts.map +1 -0
- package/dist/op/activities/source-rollback.d.ts +100 -0
- package/dist/op/activities/source-rollback.d.ts.map +1 -0
- package/dist/op/activities/work-evidence.d.ts +42 -0
- package/dist/op/activities/work-evidence.d.ts.map +1 -0
- package/dist/op/activity-contract.d.ts +1 -0
- package/dist/op/activity-contract.d.ts.map +1 -1
- package/dist/op/activity-profiles.d.ts +16 -0
- package/dist/op/activity-profiles.d.ts.map +1 -1
- package/dist/op/builders.d.ts +84 -2
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/composites/converge-op.d.ts +31 -4
- package/dist/op/composites/converge-op.d.ts.map +1 -1
- package/dist/op/decide-backend.d.ts +116 -0
- package/dist/op/decide-backend.d.ts.map +1 -0
- package/dist/op/decide-config.d.ts +120 -0
- package/dist/op/decide-config.d.ts.map +1 -0
- package/dist/op/decide-read-inputs.d.ts +40 -0
- package/dist/op/decide-read-inputs.d.ts.map +1 -0
- package/dist/op/gate-wait.d.ts +43 -0
- package/dist/op/gate-wait.d.ts.map +1 -0
- package/dist/op/gate.d.ts +5 -2
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/index.d.ts +11 -4
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts +29 -4
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-output.d.ts +2 -0
- package/dist/op/local-output.d.ts.map +1 -1
- package/dist/op/op-ir.d.ts +2 -0
- package/dist/op/op-ir.d.ts.map +1 -1
- package/dist/op/op-verb-class.d.ts.map +1 -1
- package/dist/op/operator.d.ts +97 -1
- package/dist/op/operator.d.ts.map +1 -1
- package/dist/op/runtime.d.ts +37 -5
- package/dist/op/runtime.d.ts.map +1 -1
- package/dist/op/runtimes/local.d.ts.map +1 -1
- package/dist/op/source-archive.d.ts +63 -0
- package/dist/op/source-archive.d.ts.map +1 -0
- package/dist/op/steward-points.d.ts +136 -0
- package/dist/op/steward-points.d.ts.map +1 -0
- package/dist/op/steward-turn.d.ts +53 -0
- package/dist/op/steward-turn.d.ts.map +1 -0
- package/dist/op/steward.d.ts +22 -1
- package/dist/op/steward.d.ts.map +1 -1
- package/dist/op/types.d.ts +7 -0
- package/dist/op/types.d.ts.map +1 -1
- package/dist/op/work-lease-run.d.ts +1 -1
- package/dist/op/work-lease-run.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/decide-workspace.d.ts +100 -0
- package/dist/workspace/__fixtures__/decide-workspace.d.ts.map +1 -0
- package/dist/workspace/answers-ledger.d.ts +76 -0
- package/dist/workspace/answers-ledger.d.ts.map +1 -0
- package/dist/workspace/changes-cli.d.ts +16 -0
- package/dist/workspace/changes-cli.d.ts.map +1 -0
- package/dist/workspace/changes.d.ts +200 -0
- package/dist/workspace/changes.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts +59 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts +51 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts +26 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts +59 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts +22 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts +71 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts.map +1 -0
- package/dist/workspace/chant-migrations.d.ts +99 -0
- package/dist/workspace/chant-migrations.d.ts.map +1 -0
- package/dist/workspace/checks/boxes.d.ts +34 -1
- package/dist/workspace/checks/boxes.d.ts.map +1 -1
- package/dist/workspace/checks/diagrams.d.ts +31 -0
- package/dist/workspace/checks/diagrams.d.ts.map +1 -0
- package/dist/workspace/checks/records.d.ts +7 -0
- package/dist/workspace/checks/records.d.ts.map +1 -1
- package/dist/workspace/checks.d.ts +3 -0
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/compose-graph.d.ts +2 -0
- package/dist/workspace/compose-graph.d.ts.map +1 -1
- package/dist/workspace/conformance/index.d.ts.map +1 -1
- package/dist/workspace/decide.d.ts +49 -4
- package/dist/workspace/decide.d.ts.map +1 -1
- package/dist/workspace/declaration.d.ts +52 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.schema.json +114 -1
- package/dist/workspace/declared-kinds.d.ts +20 -0
- package/dist/workspace/declared-kinds.d.ts.map +1 -1
- package/dist/workspace/generated-files.d.ts +7 -0
- package/dist/workspace/generated-files.d.ts.map +1 -1
- package/dist/workspace/intent.d.ts.map +1 -1
- package/dist/workspace/lineage-check.d.ts +3 -0
- package/dist/workspace/lineage-check.d.ts.map +1 -1
- package/dist/workspace/lineage-upgrade.d.ts +6 -1
- package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
- package/dist/workspace/ls.d.ts +39 -1
- package/dist/workspace/ls.d.ts.map +1 -1
- package/dist/workspace/point-answer.schema.json +95 -0
- package/dist/workspace/points-cli.d.ts.map +1 -1
- package/dist/workspace/points.d.ts +1 -1
- package/dist/workspace/reason-codes.d.ts +14 -2
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/records-cli.d.ts +11 -1
- package/dist/workspace/records-cli.d.ts.map +1 -1
- package/dist/workspace/records-close.d.ts +1 -1
- package/dist/workspace/records-close.d.ts.map +1 -1
- package/dist/workspace/records-write.d.ts +11 -5
- package/dist/workspace/records-write.d.ts.map +1 -1
- package/dist/workspace/records.d.ts +34 -4
- package/dist/workspace/records.d.ts.map +1 -1
- package/dist/workspace/status-gates.d.ts +7 -2
- package/dist/workspace/status-gates.d.ts.map +1 -1
- package/dist/workspace/status-stewards.d.ts +59 -1
- package/dist/workspace/status-stewards.d.ts.map +1 -1
- package/dist/workspace/status.d.ts +16 -0
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/work-cli.d.ts +113 -2
- package/dist/workspace/work-cli.d.ts.map +1 -1
- package/dist/workspace/work-evidence.d.ts +74 -0
- package/dist/workspace/work-evidence.d.ts.map +1 -0
- package/dist/workspace/work.d.ts +43 -1
- package/dist/workspace/work.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/audit.test.ts +9 -1
- package/src/cli/commands/build.ts +3 -2
- package/src/cli/commands/lint.ts +7 -1
- package/src/cli/handlers/graph.test.ts +20 -0
- package/src/cli/handlers/graph.ts +16 -2
- package/src/cli/handlers/operator.ts +34 -2
- package/src/cli/handlers/run.test.ts +154 -2
- package/src/cli/handlers/run.ts +121 -2
- package/src/cli/main.test.ts +10 -0
- package/src/cli/main.ts +19 -0
- package/src/cli/mcp/workspace-tools.test.ts +35 -19
- package/src/cli/mcp/workspace-tools.ts +62 -4
- package/src/cli/plugins.ts +11 -3
- package/src/cli/registry.ts +12 -2
- package/src/codegen/pinned-upgrade.ts +2 -1
- package/src/codegen/release-wiring.test.ts +49 -2
- package/src/components/capability-plugin-loader.ts +3 -3
- package/src/components/cli-support.ts +2 -2
- package/src/config.ts +11 -0
- package/src/lexicon-module.ts +56 -1
- package/src/lexicon-package.test.ts +96 -0
- package/src/lifecycle/converge-ledger.ts +20 -0
- package/src/lifecycle/run-ledger.ts +6 -0
- package/src/lifecycle/symptoms.ts +95 -0
- package/src/lint/rules/index.ts +6 -2
- package/src/lint/rules/op/index.ts +17 -4
- package/src/lint/rules/op/ops014-converge-rule-refusals.test.ts +23 -1
- package/src/lint/rules/op/ops014-converge-rule-refusals.ts +10 -6
- package/src/lint/rules/op/sys010-key-over-plain-http.test.ts +52 -0
- package/src/lint/rules/op/sys010-key-over-plain-http.ts +100 -0
- package/src/lint/rules/sys001-no-literal-key.test.ts +31 -0
- package/src/lint/rules/sys001-no-literal-key.ts +67 -0
- package/src/op/__fixtures__/decide-stub-backend.ts +143 -0
- package/src/op/activities/activity-contracts.ts +190 -1
- package/src/op/activities/apply.ts +7 -6
- package/src/op/activities/change-coverage.ts +60 -0
- package/src/op/activities/converge.test.ts +55 -1
- package/src/op/activities/converge.ts +208 -29
- package/src/op/activities/decide.live.test.ts +78 -0
- package/src/op/activities/decide.test.ts +300 -0
- package/src/op/activities/decide.ts +267 -0
- package/src/op/activities/index.ts +21 -0
- package/src/op/activities/shell.test.ts +91 -1
- package/src/op/activities/shell.ts +91 -2
- package/src/op/activities/source-release.test.ts +118 -0
- package/src/op/activities/source-release.ts +168 -0
- package/src/op/activities/source-rollback.test.ts +131 -0
- package/src/op/activities/source-rollback.ts +239 -0
- package/src/op/activities/work-evidence.ts +62 -0
- package/src/op/activity-contract-registry.test.ts +2 -0
- package/src/op/activity-contract-registry.ts +2 -2
- package/src/op/activity-contract.test.ts +12 -0
- package/src/op/activity-contract.ts +6 -1
- package/src/op/activity-profiles.ts +29 -0
- package/src/op/activity-registry.test.ts +1 -1
- package/src/op/activity-registry.ts +2 -2
- package/src/op/builders.ts +119 -5
- package/src/op/command-gate.test.ts +192 -0
- package/src/op/composites/converge-op.test.ts +27 -1
- package/src/op/composites/converge-op.ts +69 -15
- package/src/op/decide-backend.test.ts +44 -0
- package/src/op/decide-backend.ts +212 -0
- package/src/op/decide-config.test.ts +52 -0
- package/src/op/decide-config.ts +80 -0
- package/src/op/decide-read-inputs.ts +117 -0
- package/src/op/gate-approval.ts +2 -2
- package/src/op/gate-wait.ts +57 -0
- package/src/op/gate.ts +8 -3
- package/src/op/generate-pipeline.ts +2 -2
- package/src/op/index.ts +18 -3
- package/src/op/local-executor.ts +151 -15
- package/src/op/local-output.test.ts +33 -0
- package/src/op/local-output.ts +20 -2
- package/src/op/op-ir.test.ts +10 -0
- package/src/op/op-ir.ts +4 -0
- package/src/op/op-verb-class.ts +1 -0
- package/src/op/operator.ts +290 -11
- package/src/op/runtime.ts +31 -8
- package/src/op/runtimes/local.ts +3 -1
- package/src/op/source-archive.ts +161 -0
- package/src/op/steward-points.test.ts +313 -0
- package/src/op/steward-points.ts +194 -0
- package/src/op/steward-turn.ts +79 -0
- package/src/op/steward.test.ts +87 -3
- package/src/op/steward.ts +25 -1
- package/src/op/types.ts +7 -0
- package/src/op/work-lease-run.test.ts +20 -1
- package/src/op/work-lease-run.ts +2 -2
- package/src/workspace/__fixtures__/decide-workspace.ts +129 -0
- package/src/workspace/__fixtures__/studio-template-84c8b21.json +67 -0
- package/src/workspace/answers-ledger.ts +118 -0
- package/src/workspace/changes-cli.ts +56 -0
- package/src/workspace/changes.schema.json +472 -0
- package/src/workspace/changes.test.ts +315 -0
- package/src/workspace/changes.ts +512 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit-fly-site.ts +234 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit-live-names.ts +164 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit-rollback.ts +223 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.ts +195 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit-ship-policy.ts +108 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit.ts +882 -0
- package/src/workspace/chant-migrations.e2e.test.ts +1089 -0
- package/src/workspace/chant-migrations.ts +164 -0
- package/src/workspace/check-contract.test.ts +4 -2
- package/src/workspace/check.schema.json +15 -5
- package/src/workspace/checks/boxes.test.ts +80 -1
- package/src/workspace/checks/boxes.ts +136 -1
- package/src/workspace/checks/diagrams.test.ts +134 -0
- package/src/workspace/checks/diagrams.ts +108 -0
- package/src/workspace/checks/records.ts +47 -0
- package/src/workspace/checks.ts +8 -1
- package/src/workspace/compose-graph.ts +2 -0
- package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +10 -0
- package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +26 -0
- package/src/workspace/conformance/index.ts +5 -0
- package/src/workspace/decide.test.ts +1 -1
- package/src/workspace/decide.ts +156 -23
- package/src/workspace/declaration.schema.json +114 -1
- package/src/workspace/declaration.test.ts +58 -0
- package/src/workspace/declaration.ts +104 -0
- package/src/workspace/declared-kinds.test.ts +5 -5
- package/src/workspace/declared-kinds.ts +46 -1
- package/src/workspace/generated-files.ts +14 -0
- package/src/workspace/graph-cli.ts +1 -1
- package/src/workspace/graph.schema.json +9 -1
- package/src/workspace/intent.schema.json +8 -2
- package/src/workspace/intent.ts +3 -10
- package/src/workspace/lineage-check.ts +16 -1
- package/src/workspace/lineage-upgrade-cli.ts +1 -1
- package/src/workspace/lineage-upgrade.test.ts +70 -0
- package/src/workspace/lineage-upgrade.ts +81 -10
- package/src/workspace/ls-contract.test.ts +57 -0
- package/src/workspace/ls.schema.json +46 -1
- package/src/workspace/ls.ts +62 -3
- package/src/workspace/points-cli.ts +14 -3
- package/src/workspace/points-write.schema.json +11 -1
- package/src/workspace/points.schema.json +33 -0
- package/src/workspace/points.test.ts +1 -1
- package/src/workspace/points.ts +1 -1
- package/src/workspace/read-contract.test.ts +1 -1
- package/src/workspace/reason-codes.test.ts +11 -1
- package/src/workspace/reason-codes.ts +18 -2
- package/src/workspace/records-amend.schema.json +2 -0
- package/src/workspace/records-cli.ts +13 -3
- package/src/workspace/records-close.schema.json +2 -0
- package/src/workspace/records-new.schema.json +2 -0
- package/src/workspace/records-review.schema.json +2 -0
- package/src/workspace/records-sessions-write.e2e.test.ts +77 -0
- package/src/workspace/records-sessions-write.test.ts +2 -34
- package/src/workspace/records-write.e2e.test.ts +108 -0
- package/src/workspace/records-write.test.ts +32 -41
- package/src/workspace/records-write.ts +33 -15
- package/src/workspace/records.schema.json +36 -2
- package/src/workspace/records.test.ts +48 -3
- package/src/workspace/records.ts +109 -3
- package/src/workspace/status-contract.test.ts +13 -8
- package/src/workspace/status-gates.ts +9 -4
- package/src/workspace/status-stewards.ts +108 -4
- package/src/workspace/status.schema.json +111 -7
- package/src/workspace/status.ts +36 -1
- package/src/workspace/work-acceptance.test.ts +274 -0
- package/src/workspace/{work-cli.test.ts → work-cli.e2e.test.ts} +1 -1
- package/src/workspace/work-cli.ts +193 -10
- package/src/workspace/work-evidence.schema.json +212 -0
- package/src/workspace/work-evidence.ts +169 -0
- package/src/workspace/work-history.schema.json +349 -0
- package/src/workspace/work-history.test.ts +156 -0
- package/src/workspace/work.test.ts +3 -2
- package/src/workspace/work.ts +100 -1
- /package/src/cli/{discovery-skip.test.ts → discovery-skip.e2e.test.ts} +0 -0
- /package/src/workspace/conformance/{conformance.test.ts → conformance.e2e.test.ts} +0 -0
- /package/src/workspace/{which-chant.test.ts → which-chant.e2e.test.ts} +0 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rollback activities for a release a release Op shipped as a source tree
|
|
3
|
+
* (#2800): plan the rollback from the release ledger, and record it once the
|
|
4
|
+
* site serves the earlier release again.
|
|
5
|
+
*
|
|
6
|
+
* - {@link releaseRollbackPlan}: pick the release to go back to, read its plan
|
|
7
|
+
* from `_plans/` (ws-055), archive the same directory of the same commit
|
|
8
|
+
* again and refuse unless it hashes to the digest that plan recorded, then
|
|
9
|
+
* write a rollback plan named by its own sha256. The rollback gate binds to
|
|
10
|
+
* that digest. The step that restores the Machine reads the archive again,
|
|
11
|
+
* checked against the same digest, before the Machine changes.
|
|
12
|
+
* - {@link releaseRollbackRecord}: append a release record with the restored
|
|
13
|
+
* release's digest and commit, `restores` naming that release, the actor who
|
|
14
|
+
* ran it and the gate's approver, the way `chant components rollback`
|
|
15
|
+
* records one. Nothing is written when the ledger already records it.
|
|
16
|
+
*
|
|
17
|
+
* Which release: by default, the one the site served before the latest
|
|
18
|
+
* release the release Op shipped. A rollback's own record does not count as
|
|
19
|
+
* a release here, so running the rollback again plans the same rollback, its
|
|
20
|
+
* approval still holds, the Machine already serves it, and nothing is
|
|
21
|
+
* recorded twice. `to` names another release by its digest.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { execFileSync } from "node:child_process";
|
|
25
|
+
import { realpathSync } from "node:fs";
|
|
26
|
+
import { join, resolve } from "node:path";
|
|
27
|
+
import type { ReleaseRecord, ReleaseRef } from "../../lifecycle/release-ledger";
|
|
28
|
+
import { archiveSourceTree } from "../source-archive";
|
|
29
|
+
import { readReleasePlan, releasePlan, releasePlanDigest } from "./source-release";
|
|
30
|
+
|
|
31
|
+
// ── releaseRollbackPlan ───────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
export interface ReleaseRollbackPlanArgs {
|
|
34
|
+
/** The component the releases are recorded under. */
|
|
35
|
+
component: string;
|
|
36
|
+
/** The environment whose ledger records them. */
|
|
37
|
+
env: string;
|
|
38
|
+
/** The digest of the release to go back to. Default: the release the site served before the latest one. */
|
|
39
|
+
to?: string;
|
|
40
|
+
/**
|
|
41
|
+
* The directory the release archived, relative to the Op's working
|
|
42
|
+
* directory, for a plan that does not record `artifact.dir`.
|
|
43
|
+
*/
|
|
44
|
+
path?: string;
|
|
45
|
+
/** Where plans are written. Default: `dist/plans`. */
|
|
46
|
+
dir?: string;
|
|
47
|
+
cwd?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ReleaseRollbackPlanResult {
|
|
51
|
+
/** `sha256:` of the rollback plan. The rollback gate binds to it. */
|
|
52
|
+
digest: string;
|
|
53
|
+
/** Where the rollback plan was written. */
|
|
54
|
+
file: string;
|
|
55
|
+
/** The release it goes back to: that release's plan digest, which the ledger and the Machine's metadata name. */
|
|
56
|
+
to: string;
|
|
57
|
+
/** The release it replaces. */
|
|
58
|
+
from: string;
|
|
59
|
+
/** The commit the restored release shipped. */
|
|
60
|
+
gitSha: string;
|
|
61
|
+
/** The restored release's source tree, archived again from that commit. */
|
|
62
|
+
archive: string;
|
|
63
|
+
/** Its sha256, the one the restored release's plan recorded. */
|
|
64
|
+
archiveDigest: string;
|
|
65
|
+
/** The directory the archive holds. */
|
|
66
|
+
dir: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A record's reference, the way `restores` and `promotedFrom` name one. */
|
|
70
|
+
const refTo = (r: ReleaseRecord): ReleaseRef => ({ env: r.env, runId: r.runId, timestamp: r.timestamp });
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The release a rollback goes back to, and the release it replaces. Pure.
|
|
74
|
+
*
|
|
75
|
+
* The replaced release is the component's latest record that is not itself a
|
|
76
|
+
* rollback. By default the target is the latest record before it with another
|
|
77
|
+
* digest: what the site served before that release. With `to`, the target is
|
|
78
|
+
* the latest record with that digest.
|
|
79
|
+
*/
|
|
80
|
+
export function pickRollback(
|
|
81
|
+
records: ReleaseRecord[],
|
|
82
|
+
component: string,
|
|
83
|
+
env: string,
|
|
84
|
+
to?: string,
|
|
85
|
+
): { target: ReleaseRecord; replaced: ReleaseRecord } | { error: string } {
|
|
86
|
+
// Ledger order, oldest first; a later append wins a tied timestamp.
|
|
87
|
+
const own = records
|
|
88
|
+
.map((r, i) => ({ r, i }))
|
|
89
|
+
.filter(({ r }) => r.component === component)
|
|
90
|
+
.sort((a, b) => (a.r.timestamp === b.r.timestamp ? a.i - b.i : a.r.timestamp < b.r.timestamp ? -1 : 1))
|
|
91
|
+
.map(({ r }) => r);
|
|
92
|
+
if (own.length === 0) return { error: `no release of "${component}" is recorded in "${env}"` };
|
|
93
|
+
const releasedAt = own.map((r) => !r.restores).lastIndexOf(true);
|
|
94
|
+
if (releasedAt < 0) return { error: `"${component}" has no release in "${env}" that was not itself a rollback` };
|
|
95
|
+
const replaced = own[releasedAt];
|
|
96
|
+
if (to !== undefined) {
|
|
97
|
+
const target = own.filter((r) => r.digest === to).at(-1);
|
|
98
|
+
if (!target) return { error: `no release of "${component}" in "${env}" has digest ${to}` };
|
|
99
|
+
return { target, replaced };
|
|
100
|
+
}
|
|
101
|
+
const target = own.slice(0, releasedAt).filter((r) => r.digest !== replaced.digest).at(-1);
|
|
102
|
+
if (!target) return { error: `"${component}" has no release in "${env}" before ${replaced.digest} to roll back to` };
|
|
103
|
+
return { target, replaced };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Plan a rollback: the earlier release, its plan, and its source tree archived again and checked against the recorded digest. */
|
|
107
|
+
export async function releaseRollbackPlan(args: ReleaseRollbackPlanArgs): Promise<ReleaseRollbackPlanResult> {
|
|
108
|
+
if (!args.component || !args.env) throw new Error("releaseRollbackPlan: name the component and the environment");
|
|
109
|
+
const cwd = resolve(args.cwd ?? process.cwd());
|
|
110
|
+
const opts = { cwd };
|
|
111
|
+
const { readReleaseLedger } = await import("../../lifecycle/release-ledger");
|
|
112
|
+
const { readReleasePlan: readPersistedPlan } = await import("../../lifecycle/plan-ledger");
|
|
113
|
+
|
|
114
|
+
const picked = pickRollback((await readReleaseLedger(args.env, opts)).records, args.component, args.env, args.to);
|
|
115
|
+
if ("error" in picked) throw new Error(`releaseRollbackPlan: ${picked.error}`);
|
|
116
|
+
const { target, replaced } = picked;
|
|
117
|
+
|
|
118
|
+
// The restored release's plan, as it was approved: its content still hashes to its name.
|
|
119
|
+
const plan = await readPersistedPlan(target.digest, opts);
|
|
120
|
+
if (!plan) throw new Error(`releaseRollbackPlan: no plan is recorded for ${target.digest} (_plans/ on chant/lifecycle), so its source tree is unknown`);
|
|
121
|
+
const planDigest = releasePlanDigest(plan);
|
|
122
|
+
if (planDigest !== target.digest) throw new Error(`releaseRollbackPlan: the plan recorded for ${target.digest} hashes to ${planDigest}`);
|
|
123
|
+
const artifact = plan.artifact as { digest?: unknown; dir?: unknown } | undefined;
|
|
124
|
+
if (typeof artifact?.digest !== "string") throw new Error(`releaseRollbackPlan: the plan for ${target.digest} records no artifact digest`);
|
|
125
|
+
const gitSha = (plan.gitSha as string | undefined) ?? target.gitSha;
|
|
126
|
+
|
|
127
|
+
// The same directory of the same commit, archived again: git archive gives the same bytes.
|
|
128
|
+
let dir: string;
|
|
129
|
+
if (typeof artifact.dir === "string") {
|
|
130
|
+
const root = realpathSync(execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf-8" }).trim());
|
|
131
|
+
dir = join(root, artifact.dir);
|
|
132
|
+
} else if (args.path) {
|
|
133
|
+
dir = args.path;
|
|
134
|
+
} else {
|
|
135
|
+
throw new Error(`releaseRollbackPlan: the plan for ${target.digest} records no artifact.dir; pass path, the directory the release archived`);
|
|
136
|
+
}
|
|
137
|
+
const archive = archiveSourceTree({ dir, ref: gitSha, cwd });
|
|
138
|
+
if (archive.digest !== artifact.digest) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`releaseRollbackPlan: ${archive.dir} at ${gitSha.slice(0, 12)} archives to ${archive.digest}, not the ${artifact.digest} release ${target.digest} recorded; refusing to restore it`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const written = await releasePlan({
|
|
145
|
+
component: args.component,
|
|
146
|
+
env: args.env,
|
|
147
|
+
gitSha,
|
|
148
|
+
content: {
|
|
149
|
+
rollback: { to: target.digest, from: replaced.digest, restores: refTo(target) },
|
|
150
|
+
artifact: { ...(plan.artifact as Record<string, unknown>), digest: archive.digest, dir: archive.dir },
|
|
151
|
+
},
|
|
152
|
+
...(args.dir ? { dir: args.dir } : {}),
|
|
153
|
+
cwd,
|
|
154
|
+
});
|
|
155
|
+
return {
|
|
156
|
+
digest: written.digest,
|
|
157
|
+
file: written.file,
|
|
158
|
+
to: target.digest,
|
|
159
|
+
from: replaced.digest,
|
|
160
|
+
gitSha,
|
|
161
|
+
archive: archive.archive,
|
|
162
|
+
archiveDigest: archive.digest,
|
|
163
|
+
dir: archive.dir,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── releaseRollbackRecord ─────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
export interface ReleaseRollbackRecordArgs {
|
|
170
|
+
/** The rollback plan `releaseRollbackPlan` wrote. */
|
|
171
|
+
plan: string;
|
|
172
|
+
/** The digest the gate approved. The plan must be that one. */
|
|
173
|
+
digest?: string;
|
|
174
|
+
/** Who approved it: the resolution bound to this digest on this Op's gate is looked up. */
|
|
175
|
+
approval?: { op: string; gate: string };
|
|
176
|
+
/** Who ran the rollback. Default: GITHUB_ACTOR, GITLAB_USER_LOGIN, USER. */
|
|
177
|
+
actor?: string;
|
|
178
|
+
runId?: string;
|
|
179
|
+
cwd?: string;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface ReleaseRollbackRecordResult {
|
|
183
|
+
/** False when the ledger already recorded this rollback. */
|
|
184
|
+
recorded: boolean;
|
|
185
|
+
/** The restored release's digest, which the record carries. */
|
|
186
|
+
digest: string;
|
|
187
|
+
env: string;
|
|
188
|
+
component: string;
|
|
189
|
+
approver: string | null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Record a rollback in the release ledger, once. */
|
|
193
|
+
export async function releaseRollbackRecord(args: ReleaseRollbackRecordArgs): Promise<ReleaseRollbackRecordResult> {
|
|
194
|
+
const cwd = args.cwd;
|
|
195
|
+
const opts = cwd ? { cwd } : undefined;
|
|
196
|
+
const plan = readReleasePlan(resolve(cwd ?? process.cwd(), args.plan), args.digest);
|
|
197
|
+
const rollback = plan.rollback as { to?: string; restores?: ReleaseRef } | undefined;
|
|
198
|
+
if (!rollback?.to || !rollback.restores) throw new Error(`releaseRollbackRecord: ${args.plan} is not a rollback plan (no rollback.to)`);
|
|
199
|
+
const component = plan.component as string;
|
|
200
|
+
const env = plan.env as string;
|
|
201
|
+
const gitSha = plan.gitSha as string;
|
|
202
|
+
|
|
203
|
+
const { readReleaseLedger, latestPerComponent, appendReleaseRecord, resolveRunId } = await import("../../lifecycle/release-ledger");
|
|
204
|
+
const { pushLifecycle } = await import("../../lifecycle/git");
|
|
205
|
+
|
|
206
|
+
let approver: string | null = null;
|
|
207
|
+
if (args.approval) {
|
|
208
|
+
const { readGateLedger } = await import("../../lifecycle/gate-ledger");
|
|
209
|
+
const { resolutions } = await readGateLedger(args.approval.op, opts);
|
|
210
|
+
const bound = resolutions.filter((r) => r.gate === args.approval!.gate && r.planDigest === plan.digest);
|
|
211
|
+
approver = bound.length > 0 ? [...new Set(bound.map((r) => r.resolvedBy))].join(", ") : null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const latest = latestPerComponent((await readReleaseLedger(env, opts)).records.filter((r) => r.component === component)).get(component);
|
|
215
|
+
if (latest?.digest === rollback.to && latest.restores?.runId === rollback.restores.runId && latest.restores.timestamp === rollback.restores.timestamp) {
|
|
216
|
+
return { recorded: false, digest: rollback.to, env, component, approver };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const actor = args.actor || process.env.GITHUB_ACTOR || process.env.GITLAB_USER_LOGIN || process.env.USER;
|
|
220
|
+
if (!actor) throw new Error("releaseRollbackRecord: no actor: pass one, or set GITHUB_ACTOR, GITLAB_USER_LOGIN or USER");
|
|
221
|
+
const { runId, runOrigin } = resolveRunId(args.runId);
|
|
222
|
+
await appendReleaseRecord(
|
|
223
|
+
{
|
|
224
|
+
component,
|
|
225
|
+
env,
|
|
226
|
+
digest: rollback.to,
|
|
227
|
+
gitSha,
|
|
228
|
+
runId,
|
|
229
|
+
...(runOrigin ? { runOrigin } : {}),
|
|
230
|
+
timestamp: new Date().toISOString(),
|
|
231
|
+
actor,
|
|
232
|
+
...(approver ? { approver } : {}),
|
|
233
|
+
restores: rollback.restores,
|
|
234
|
+
},
|
|
235
|
+
opts,
|
|
236
|
+
);
|
|
237
|
+
await pushLifecycle(opts);
|
|
238
|
+
return { recorded: true, digest: rollback.to, env, component, approver };
|
|
239
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `workEvidence` (#2772): attach one piece of evidence to an acceptance
|
|
3
|
+
* criterion of the work item the run holds the lease on.
|
|
4
|
+
*
|
|
5
|
+
* Meant for an Op that declares `workLease` (#2748), a steward's among them:
|
|
6
|
+
* the builder passes the run's lease (`workLeaseOutput()`) as `lease`, and the
|
|
7
|
+
* write goes through only while that lease is still the run's, under its
|
|
8
|
+
* token. The rules are `attachWorkEvidence`'s (`../../workspace/work-evidence.ts`):
|
|
9
|
+
* the criterion must be one the record lists, and a `manual` criterion is
|
|
10
|
+
* refused, since the run holding the lease is the item's implementer.
|
|
11
|
+
* A refusal fails the step with its code.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { EvidenceResult, WorkAcceptance } from "../../workspace/work";
|
|
15
|
+
|
|
16
|
+
export interface WorkEvidenceArgs {
|
|
17
|
+
/** The run's work lease: `workLeaseOutput()`, which the builder passes. */
|
|
18
|
+
lease: { item: string; holder: string; token: string };
|
|
19
|
+
/** The criterion the evidence is for, by id. */
|
|
20
|
+
criterion: string;
|
|
21
|
+
result: EvidenceResult;
|
|
22
|
+
title: string;
|
|
23
|
+
/** A public https:// link, or */
|
|
24
|
+
url?: string;
|
|
25
|
+
/** a workspace file, pinned by the hash of its bytes now. */
|
|
26
|
+
path?: string;
|
|
27
|
+
/** The work kind file, when the declaration names more than one with the item. */
|
|
28
|
+
kind?: string;
|
|
29
|
+
/** Where the workspace is found; the run's directory by default. */
|
|
30
|
+
cwd?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface WorkEvidenceResult {
|
|
34
|
+
item: string;
|
|
35
|
+
/** The work record, from the repository root. */
|
|
36
|
+
path: string;
|
|
37
|
+
evidence: Record<string, unknown>;
|
|
38
|
+
acceptance: WorkAcceptance;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function workEvidence(args: WorkEvidenceArgs): Promise<WorkEvidenceResult> {
|
|
42
|
+
const lease = args.lease;
|
|
43
|
+
if (!lease || typeof lease.item !== "string" || typeof lease.holder !== "string" || typeof lease.token !== "string") {
|
|
44
|
+
throw new Error("workEvidence runs under a work lease: pass the run's lease as `lease` (workLeaseOutput()) from an Op that declares workLease");
|
|
45
|
+
}
|
|
46
|
+
const { attachWorkEvidence } = await import("../../workspace/work-evidence");
|
|
47
|
+
const out = await attachWorkEvidence({
|
|
48
|
+
cwd: args.cwd ?? process.cwd(),
|
|
49
|
+
item: lease.item,
|
|
50
|
+
holder: lease.holder,
|
|
51
|
+
token: lease.token,
|
|
52
|
+
criterion: args.criterion,
|
|
53
|
+
result: args.result,
|
|
54
|
+
title: args.title,
|
|
55
|
+
...(args.url !== undefined ? { url: args.url } : {}),
|
|
56
|
+
...(args.path !== undefined ? { path: args.path } : {}),
|
|
57
|
+
...(args.kind !== undefined ? { kind: args.kind } : {}),
|
|
58
|
+
});
|
|
59
|
+
if ("error" in out) throw new Error(`workEvidence refused for ${lease.item} ${args.criterion}: ${out.error.code}: ${out.error.message}`);
|
|
60
|
+
console.log(`workEvidence: ${out.item} ${args.criterion} ${args.result} (${out.acceptance.met}/${out.acceptance.total} met)`);
|
|
61
|
+
return { item: out.item, path: out.path, evidence: out.evidence, acceptance: out.acceptance };
|
|
62
|
+
}
|
|
@@ -12,6 +12,8 @@ describe("loadActivityContracts", () => {
|
|
|
12
12
|
expect(contracts.get("predictBehaviour")?.name).toBe("predictBehaviour");
|
|
13
13
|
expect(contracts.get("behaviourFinding")?.returns).toBeDefined();
|
|
14
14
|
expect(contracts.get("lifecycleDiff")?.name).toBe("lifecycleDiff");
|
|
15
|
+
// The forward coverage check over an Op's own diff (#2773).
|
|
16
|
+
expect(contracts.get("changeCoverage")?.returns).toBeDefined();
|
|
15
17
|
});
|
|
16
18
|
|
|
17
19
|
test("a lexicon's contracts are resolved by the same subpath convention loadActivities uses", async () => {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
|
|
37
37
|
import * as coreContracts from "./activities/activity-contracts";
|
|
38
38
|
import { collectActivityContracts, type ActivityContract } from "./activity-contract";
|
|
39
|
-
import { importLexiconModule } from "../lexicon-module";
|
|
39
|
+
import { importLexiconModule, importLexiconPackage } from "../lexicon-module";
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* The shape {@link loadActivityContracts} reads off a plugin: an optional
|
|
@@ -89,7 +89,7 @@ export async function loadActivityContracts(
|
|
|
89
89
|
);
|
|
90
90
|
} else {
|
|
91
91
|
const spec = `@intentius/chant-lexicon-${name}/op/activity-contracts`;
|
|
92
|
-
collectActivityContracts((await
|
|
92
|
+
collectActivityContracts((await importLexiconPackage(spec)) as Record<string, unknown>, contracts);
|
|
93
93
|
}
|
|
94
94
|
} catch {
|
|
95
95
|
// Lexicon absent or declares no contracts at the conventional subpath.
|
|
@@ -67,6 +67,18 @@ function opWith(config: Partial<OpConfig> & Pick<OpConfig, "phases">): Pick<OpCo
|
|
|
67
67
|
return { name: "test-op", ...config };
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
describe("validateActivitySteps() — a step's own timeout (#2787)", () => {
|
|
71
|
+
it("accepts a duration up to six hours, and flags one that is not a duration, is zero, or is longer", () => {
|
|
72
|
+
const withTimeout = (timeout: string) => opWith({ phases: [phase("Build", [activity("lifecycleDiff", { env: "prod" }, { timeout })])] });
|
|
73
|
+
const contracts = new Map([["lifecycleDiff", lifecycleDiffContract]]);
|
|
74
|
+
for (const ok of ["45m", "1h30m", "6h"]) expect(validateActivitySteps(withTimeout(ok), contracts), ok).toEqual([]);
|
|
75
|
+
for (const [bad, why] of [["forever", /not a duration/], ["45 minutes", /not a duration/], ["0m", /more than zero/], ["7h", /longer than a step may run \(6h\)/]] as const) {
|
|
76
|
+
const issues = validateActivitySteps(withTimeout(bad), contracts);
|
|
77
|
+
expect(issues.map((i) => i.message), bad).toEqual([expect.stringMatching(why)]);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
70
82
|
describe("validateActivitySteps() — passing Ops", () => {
|
|
71
83
|
it("returns no issues for a step matching its contract exactly", () => {
|
|
72
84
|
const config = opWith({ phases: [phase("Diff", [activity("lifecycleDiff", { env: "prod", live: true })])] });
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
import { z } from "zod";
|
|
42
42
|
import { outcomeAttributesOf } from "./types";
|
|
43
43
|
import type { OpConfig, PhaseDefinition, ActivityStep, StepDefinition } from "./types";
|
|
44
|
-
import { ACTIVITY_PROFILE_NAMES } from "./activity-profiles";
|
|
44
|
+
import { ACTIVITY_PROFILE_NAMES, stepTimeoutProblem } from "./activity-profiles";
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
47
|
* Every literal value {@link ActivityStep.profile} may hold, read off
|
|
@@ -237,6 +237,7 @@ export function primitiveKindOf(schema: z.ZodTypeAny): PrimitiveSchemaKind | und
|
|
|
237
237
|
*
|
|
238
238
|
* Catches the four failure classes chant #1288 names:
|
|
239
239
|
* - an unrecognized `profile` (checked against {@link KNOWN_ACTIVITY_PROFILES}, independent of whether `fn` has a contract),
|
|
240
|
+
* - a step's own `timeout` that is not a duration, or is longer than `MAX_STEP_TIMEOUT` (#2787),
|
|
240
241
|
* - an args key the declared schema doesn't recognize,
|
|
241
242
|
* - an args value of the wrong type (including a required key that's missing),
|
|
242
243
|
* - an `outcomeAttribute.from` path that can't exist on the declared return type.
|
|
@@ -258,6 +259,10 @@ export function validateActivitySteps(
|
|
|
258
259
|
message: `unknown profile "${step.profile}" (known: ${KNOWN_ACTIVITY_PROFILES.join(", ")})`,
|
|
259
260
|
});
|
|
260
261
|
}
|
|
262
|
+
if (step.timeout !== undefined) {
|
|
263
|
+
const problem = stepTimeoutProblem(step.timeout);
|
|
264
|
+
if (problem) issues.push({ opName: config.name, phase: phase.name, fn: step.fn, message: problem });
|
|
265
|
+
}
|
|
261
266
|
|
|
262
267
|
const contract = contracts.get(step.fn);
|
|
263
268
|
if (!contract) continue;
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* heartbeats to anything.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import { parseDuration } from "./duration";
|
|
19
|
+
|
|
18
20
|
export interface ActivityProfile {
|
|
19
21
|
/** Maximum time allowed for a single activity execution attempt (e.g. `"20m"`). */
|
|
20
22
|
timeout: string;
|
|
@@ -132,3 +134,30 @@ export type ActivityProfileName = keyof typeof ACTIVITY_PROFILES;
|
|
|
132
134
|
|
|
133
135
|
/** Every profile name, as a runtime list (`KNOWN_ACTIVITY_PROFILES`'s source). */
|
|
134
136
|
export const ACTIVITY_PROFILE_NAMES = Object.keys(ACTIVITY_PROFILES) as ActivityProfileName[];
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The longest timeout one step may declare for itself (#2787): six hours, the
|
|
140
|
+
* ceiling hosted CI puts on a single job. A step's own `timeout` replaces its
|
|
141
|
+
* profile's and keeps the profile's retries, so a builder `shell` step can
|
|
142
|
+
* run 45 minutes on its one attempt. Anything that waits longer is waiting on
|
|
143
|
+
* someone, which is a gate's job (`humanGate`, 48 hours), not a step's.
|
|
144
|
+
*/
|
|
145
|
+
export const MAX_STEP_TIMEOUT = "6h";
|
|
146
|
+
|
|
147
|
+
/** {@link MAX_STEP_TIMEOUT} in milliseconds. */
|
|
148
|
+
export const MAX_STEP_TIMEOUT_MS = 6 * 3_600_000;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Why `timeout` can't be a step's own timeout, or null when it can: it must
|
|
152
|
+
* be a duration such as `45m` or `1h30m`, more than zero, and at most
|
|
153
|
+
* {@link MAX_STEP_TIMEOUT}.
|
|
154
|
+
*/
|
|
155
|
+
export function stepTimeoutProblem(timeout: unknown): string | null {
|
|
156
|
+
if (typeof timeout !== "string" || !/^(\d+(ms|s|m|h|d))+$/.test(timeout)) {
|
|
157
|
+
return `timeout ${JSON.stringify(timeout)} is not a duration such as "45m" or "1h30m"`;
|
|
158
|
+
}
|
|
159
|
+
const ms = parseDuration(timeout);
|
|
160
|
+
if (ms <= 0) return `timeout "${timeout}" must be more than zero`;
|
|
161
|
+
if (ms > MAX_STEP_TIMEOUT_MS) return `timeout "${timeout}" is longer than a step may run (${MAX_STEP_TIMEOUT}); a longer wait is a gate's`;
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
@@ -26,7 +26,7 @@ describe("loadActivities", () => {
|
|
|
26
26
|
"shellCmd", "chantBuild", "waitForStack", "lifecycleSnapshot", "lifecycleDiff",
|
|
27
27
|
"httpCheck", "chantTeardown", "envTeardown", "convergeTick", "reconcilePr",
|
|
28
28
|
"guardValidate", "policyGate", "workflowSupplyChainAudit", "pipelineSupplyChainAudit",
|
|
29
|
-
"lexiconUpgrade", "nativeApply",
|
|
29
|
+
"lexiconUpgrade", "nativeApply", "changeCoverage",
|
|
30
30
|
]) {
|
|
31
31
|
expect(typeof a.get(fn), `${fn} should resolve from the base library`).toBe("function");
|
|
32
32
|
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import * as baseActivities from "./activities";
|
|
14
14
|
import { ACTIVITY_PROFILES, type ActivityProfile } from "./activity-profiles";
|
|
15
|
-
import { importLexiconModule } from "../lexicon-module";
|
|
15
|
+
import { importLexiconModule, importLexiconPackage } from "../lexicon-module";
|
|
16
16
|
|
|
17
17
|
export type { ActivityProfile } from "./activity-profiles";
|
|
18
18
|
|
|
@@ -64,7 +64,7 @@ export async function loadActivities(lexicons: string[] = []): Promise<Map<strin
|
|
|
64
64
|
continue;
|
|
65
65
|
}
|
|
66
66
|
const spec = `@intentius/chant-lexicon-${name}/op/activities`;
|
|
67
|
-
collectActivities((await
|
|
67
|
+
collectActivities((await importLexiconPackage(spec)) as Record<string, unknown>, activities);
|
|
68
68
|
} catch {
|
|
69
69
|
// Lexicon absent or contributes no activities — fine.
|
|
70
70
|
}
|
package/src/op/builders.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { OpResource } from "./resource";
|
|
2
2
|
import { workLeaseProblems } from "./work-lease-decl";
|
|
3
|
-
import type
|
|
3
|
+
import { WORK_LEASE_STEP_ID, type OpConfig, type PhaseDefinition, type StepDefinition, type ActivityStep, type GateStep, type EffectStep } from "./types";
|
|
4
4
|
import { isEffectReceipt, type EffectReceiptDeclaration } from "../effect-receipt";
|
|
5
5
|
import { receiptCheckInput } from "./receipt-store";
|
|
6
6
|
import { gateApprovalProblems } from "./gate-approval";
|
|
7
|
-
import { makeOutProxy, type StepOutputRef, type WithStepRefs } from "./step-output-ref";
|
|
7
|
+
import { makeOutProxy, stepOutput, type StepOutputRef, type WithStepRefs } from "./step-output-ref";
|
|
8
8
|
import type { ChantBuildArgs } from "./activities/build";
|
|
9
9
|
import type { ShellCmdArgs } from "./activities/shell";
|
|
10
10
|
import type { WaitForStackArgs } from "./activities/wait";
|
|
@@ -12,8 +12,12 @@ import type { LifecycleSnapshotArgs } from "./activities/lifecycle";
|
|
|
12
12
|
import type { ChantTeardownArgs } from "./activities/teardown";
|
|
13
13
|
import type { EnvTeardownArgs } from "./activities/env-teardown";
|
|
14
14
|
import type { HttpCheckArgs } from "./activities/http-check";
|
|
15
|
+
import type { SourceArchiveArgs, ReleasePlanArgs, ReleaseRecordArgs } from "./activities/source-release";
|
|
16
|
+
import type { ReleaseRollbackPlanArgs, ReleaseRollbackRecordArgs } from "./activities/source-rollback";
|
|
15
17
|
import type { PolicyGateArgs } from "./activities/policy";
|
|
16
18
|
import type { GuardValidateArgs } from "./activities/guard-validate";
|
|
19
|
+
import type { WorkEvidenceArgs } from "./activities/work-evidence";
|
|
20
|
+
import type { DecideArgs } from "./activities/decide";
|
|
17
21
|
import { isValidCronExpression, cronSyntaxMessage } from "./cron";
|
|
18
22
|
|
|
19
23
|
/** An `activity()` result — the plain `ActivityStep` shape plus the `.out` reference sugar (#1290). */
|
|
@@ -82,16 +86,18 @@ export function phase(
|
|
|
82
86
|
export function activity(
|
|
83
87
|
fn: string,
|
|
84
88
|
args?: Record<string, unknown>,
|
|
85
|
-
opts?: ActivityStep["profile"] | { profile?: ActivityStep["profile"]; id?: string },
|
|
89
|
+
opts?: ActivityStep["profile"] | { profile?: ActivityStep["profile"]; id?: string; timeout?: string },
|
|
86
90
|
): NamedActivityStep {
|
|
87
91
|
const profile = typeof opts === "string" ? opts : opts?.profile;
|
|
88
92
|
const id = typeof opts === "string" ? undefined : opts?.id;
|
|
93
|
+
const timeout = typeof opts === "string" ? undefined : opts?.timeout;
|
|
89
94
|
const step = {
|
|
90
95
|
kind: "activity",
|
|
91
96
|
fn,
|
|
92
97
|
...(args && Object.keys(args).length > 0 ? { args } : {}),
|
|
93
98
|
...(profile ? { profile } : {}),
|
|
94
99
|
...(id ? { id } : {}),
|
|
100
|
+
...(timeout !== undefined ? { timeout } : {}),
|
|
95
101
|
} as NamedActivityStep;
|
|
96
102
|
Object.defineProperty(step, "out", {
|
|
97
103
|
enumerable: false,
|
|
@@ -337,6 +343,20 @@ export const lifecycleSnapshot = (env: string, opts?: { id?: string }): NamedAct
|
|
|
337
343
|
* shell("./smoke.sh", { env: { HOST: host.out.stdout } });
|
|
338
344
|
* ```
|
|
339
345
|
*
|
|
346
|
+
* With `json: true`, stdout is parsed as JSON and published as `json`
|
|
347
|
+
* (#2787), so a list or an object reaches the next step or the Op's work
|
|
348
|
+
* lease as itself. A pick step that prints `["W-3","W-7"]` hands
|
|
349
|
+
* `workLease.item` its candidates:
|
|
350
|
+
*
|
|
351
|
+
* ```ts
|
|
352
|
+
* const pick = shell("node pick.mjs", { id: "pick", json: true });
|
|
353
|
+
* Op({ workLease: { item: pick.out.json }, ... });
|
|
354
|
+
* ```
|
|
355
|
+
*
|
|
356
|
+
* `timeout` sets how long the one attempt may run, in place of the profile's
|
|
357
|
+
* twenty minutes, and keeps the profile's retries: `shell("./build.sh", {
|
|
358
|
+
* timeout: "45m" })`. At most six hours (`MAX_STEP_TIMEOUT`).
|
|
359
|
+
*
|
|
340
360
|
* `cmd` stays a plain `string` and takes no references. A value spliced into
|
|
341
361
|
* a command line is a quoting decision chant would then be making on the
|
|
342
362
|
* author's behalf, and `env` carries the same value into the same command
|
|
@@ -344,10 +364,88 @@ export const lifecycleSnapshot = (env: string, opts?: { id?: string }): NamedAct
|
|
|
344
364
|
*/
|
|
345
365
|
export const shell = (
|
|
346
366
|
cmd: string,
|
|
347
|
-
opts?: WithStepRefs<Omit<ShellCmdArgs, "cmd">> & StepOpts,
|
|
367
|
+
opts?: WithStepRefs<Omit<ShellCmdArgs, "cmd">> & StepOpts & { timeout?: string },
|
|
368
|
+
): NamedActivityStep => {
|
|
369
|
+
const { timeout, ...rest } = (opts ?? {}) as Record<string, unknown> & { timeout?: string };
|
|
370
|
+
const { args, profile, id } = takeProfileAndId(rest);
|
|
371
|
+
return activity("shellCmd", { cmd, ...args }, { profile: profile ?? "atMostOnce", ...(id ? { id } : {}), ...(timeout !== undefined ? { timeout } : {}) });
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Archive one directory of HEAD as a release artifact (#2782): a tar written by
|
|
376
|
+
* `git archive`, named by its sha256, the same bytes for the same commit.
|
|
377
|
+
* Defaults to the `fastIdempotent` profile.
|
|
378
|
+
*
|
|
379
|
+
* ```ts
|
|
380
|
+
* const archive = sourceArchive("../app", { id: "archive" });
|
|
381
|
+
* ```
|
|
382
|
+
*/
|
|
383
|
+
export const sourceArchive = (
|
|
384
|
+
path: string,
|
|
385
|
+
opts?: WithStepRefs<Omit<SourceArchiveArgs, "path">> & StepOpts,
|
|
348
386
|
): NamedActivityStep => {
|
|
349
387
|
const { args, profile, id } = takeProfileAndId(opts as Record<string, unknown> | undefined);
|
|
350
|
-
return activity("
|
|
388
|
+
return activity("sourceArchive", { path, ...args }, { profile: profile ?? "fastIdempotent", ...(id ? { id } : {}) });
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Plan a release (#2782): a JSON object named by the sha256 of its canonical
|
|
393
|
+
* form, which the ship gate binds to (`gate("ship", { plan: plan.out.digest })`)
|
|
394
|
+
* and the release ledger records. Defaults to the `fastIdempotent` profile.
|
|
395
|
+
*/
|
|
396
|
+
export const releasePlan = (args: WithStepRefs<ReleasePlanArgs> & StepOpts): NamedActivityStep => {
|
|
397
|
+
const { args: rest, profile, id } = takeProfileAndId(args as Record<string, unknown>);
|
|
398
|
+
return activity("releasePlan", rest, { profile: profile ?? "fastIdempotent", ...(id ? { id } : {}) });
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Record a release in the release ledger with its plan (#2782, ws-055), once:
|
|
403
|
+
* a retry whose release the ledger already names records nothing. Defaults to
|
|
404
|
+
* the `fastIdempotent` profile.
|
|
405
|
+
*/
|
|
406
|
+
export const releaseRecord = (args: WithStepRefs<ReleaseRecordArgs> & StepOpts): NamedActivityStep => {
|
|
407
|
+
const { args: rest, profile, id } = takeProfileAndId(args as Record<string, unknown>);
|
|
408
|
+
return activity("releaseRecord", rest, { profile: profile ?? "fastIdempotent", ...(id ? { id } : {}) });
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Ask a decision point and record its answer (ws-058, #2740): the `decide`
|
|
413
|
+
* activity, core's since #2828. `opts` is the activity's own
|
|
414
|
+
* {@link DecideArgs}, minus the positional `point`, so the builder and the
|
|
415
|
+
* activity cannot drift. Defaults to the `fastIdempotent` profile: asking
|
|
416
|
+
* again with the same point, declaration and inputs returns the record
|
|
417
|
+
* already written. An open question stops the run `waiting`.
|
|
418
|
+
*
|
|
419
|
+
* ```ts
|
|
420
|
+
* decide("slice-tier", { read: { "work-item": "W-002" }, subject: "W-002" })
|
|
421
|
+
* ```
|
|
422
|
+
*/
|
|
423
|
+
export const decide = (point: string, opts?: WithStepRefs<Omit<DecideArgs, "point">> & StepOpts): NamedActivityStep => {
|
|
424
|
+
const { args, profile, id } = takeProfileAndId(opts as Record<string, unknown> | undefined);
|
|
425
|
+
return activity("decide", { point, ...args }, { profile: profile ?? "fastIdempotent", ...(id ? { id } : {}) });
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Plan a rollback of a source release (#2800): the release the site served
|
|
430
|
+
* before the latest one (or `to`), its plan read back from the ledger, and its
|
|
431
|
+
* source tree archived again from its commit and refused unless it hashes to
|
|
432
|
+
* the digest that plan recorded. Writes a rollback plan named by its own
|
|
433
|
+
* sha256, which the rollback gate binds to (`gate("rollback", { plan:
|
|
434
|
+
* plan.out.digest })`). Defaults to the `fastIdempotent` profile.
|
|
435
|
+
*/
|
|
436
|
+
export const releaseRollbackPlan = (args: WithStepRefs<ReleaseRollbackPlanArgs> & StepOpts): NamedActivityStep => {
|
|
437
|
+
const { args: rest, profile, id } = takeProfileAndId(args as Record<string, unknown>);
|
|
438
|
+
return activity("releaseRollbackPlan", rest, { profile: profile ?? "fastIdempotent", ...(id ? { id } : {}) });
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Record a rollback in the release ledger (#2800): the restored release's
|
|
443
|
+
* digest and commit, `restores` naming that release, the actor and the gate's
|
|
444
|
+
* approver, once. Defaults to the `fastIdempotent` profile.
|
|
445
|
+
*/
|
|
446
|
+
export const releaseRollbackRecord = (args: WithStepRefs<ReleaseRollbackRecordArgs> & StepOpts): NamedActivityStep => {
|
|
447
|
+
const { args: rest, profile, id } = takeProfileAndId(args as Record<string, unknown>);
|
|
448
|
+
return activity("releaseRollbackRecord", rest, { profile: profile ?? "fastIdempotent", ...(id ? { id } : {}) });
|
|
351
449
|
};
|
|
352
450
|
|
|
353
451
|
/**
|
|
@@ -924,6 +1022,22 @@ export const policyGate = (opts?: WithStepRefs<PolicyGateArgs> & { id?: string }
|
|
|
924
1022
|
);
|
|
925
1023
|
};
|
|
926
1024
|
|
|
1025
|
+
/**
|
|
1026
|
+
* Attach evidence to an acceptance criterion of the work item the run holds
|
|
1027
|
+
* the lease on (#2772), for an Op that declares `workLease`. The run's lease
|
|
1028
|
+
* is passed for it, so the write happens only while the lease is still the
|
|
1029
|
+
* run's. `opts` takes the `result` (pass or fail), a `title`, and a `url` or a
|
|
1030
|
+
* workspace `path` to pin by hash. A `manual` criterion is refused: the run
|
|
1031
|
+
* is the item's implementer. The `atMostOnce` profile: each attempt appends.
|
|
1032
|
+
*/
|
|
1033
|
+
export const workEvidence = (
|
|
1034
|
+
criterion: string,
|
|
1035
|
+
opts: WithStepRefs<Omit<WorkEvidenceArgs, "criterion" | "lease">> & { id?: string },
|
|
1036
|
+
): NamedActivityStep => {
|
|
1037
|
+
const { id, ...rest } = opts as Omit<WorkEvidenceArgs, "criterion" | "lease"> & { id?: string };
|
|
1038
|
+
return activity("workEvidence", { criterion, lease: stepOutput(WORK_LEASE_STEP_ID), ...rest }, { profile: "atMostOnce", ...(id ? { id } : {}) });
|
|
1039
|
+
};
|
|
1040
|
+
|
|
927
1041
|
/**
|
|
928
1042
|
* Run an external CloudFormation Guard rules pack against the project's built
|
|
929
1043
|
* CloudFormation template (chant #522). `opts` is {@link GuardValidateArgs}
|