@intentius/chant 0.116.0 → 0.118.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/build.d.ts +1 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/handlers/gate-resume.d.ts +44 -0
- package/dist/cli/handlers/gate-resume.d.ts.map +1 -0
- package/dist/cli/handlers/operator.d.ts +1 -0
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/run-wave.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +8 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/discovery/files.d.ts +8 -0
- package/dist/discovery/files.d.ts.map +1 -1
- package/dist/lexicon.d.ts +10 -1
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +18 -0
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/op/gate-resume.d.ts +104 -0
- package/dist/op/gate-resume.d.ts.map +1 -0
- package/dist/op/gate-review.d.ts +113 -0
- package/dist/op/gate-review.d.ts.map +1 -0
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/op-waves-run.d.ts +47 -1
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves.d.ts +39 -0
- package/dist/op/op-waves.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/build.ts +13 -2
- package/src/cli/commands/build.ts +2 -1
- package/src/cli/handlers/gate-resume.test.ts +51 -0
- package/src/cli/handlers/gate-resume.ts +166 -0
- package/src/cli/handlers/operator.ts +8 -2
- package/src/cli/handlers/run-wave.ts +13 -1
- package/src/cli/main.ts +21 -3
- package/src/cli/plugins.ts +1 -1
- package/src/cli/registry.ts +8 -0
- package/src/discovery/files.ts +18 -0
- package/src/lexicon.ts +10 -1
- package/src/lifecycle/gate-ledger.ts +14 -0
- package/src/op/gate-resume.e2e.test.ts +135 -0
- package/src/op/gate-resume.test.ts +177 -0
- package/src/op/gate-resume.ts +267 -0
- package/src/op/gate-review.test.ts +225 -0
- package/src/op/gate-review.ts +339 -0
- package/src/op/gate.ts +4 -0
- package/src/op/op-waves-run.ts +191 -13
- package/src/op/op-waves.ts +51 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Approval modes for a gate (#3684): which approvals of a wave's digest
|
|
3
|
+
* count.
|
|
4
|
+
*
|
|
5
|
+
* - `ledger` (the default): any resolution on `chant/lifecycle` that names
|
|
6
|
+
* the digest, as `chant approve` writes it. Anyone who can push to the
|
|
7
|
+
* branch can approve, in anyone's name.
|
|
8
|
+
* - `sealed`: a resolution counts only when its seal (`chant approve --sign`)
|
|
9
|
+
* verifies against the signers file read at the base commit, for the
|
|
10
|
+
* approver it names. A key that the change being applied adds to the
|
|
11
|
+
* signers file is not at base, so it cannot approve that change.
|
|
12
|
+
* - `pr-review`: a resolution counts as under `ledger`, and so does the
|
|
13
|
+
* forge review of the pull request that merged the applied commit: an
|
|
14
|
+
* approving review of the pull request's head, by a writer other than its
|
|
15
|
+
* author. It approves the digest that head planned, which the pull
|
|
16
|
+
* request's pipeline records on `chant/lifecycle` with `chant run wave
|
|
17
|
+
* --record-plans` ({@link HeadPlans}). A plan that moved after the review
|
|
18
|
+
* waits for a `chant approve` of the new digest.
|
|
19
|
+
*
|
|
20
|
+
* What counts as an approving review, per forge:
|
|
21
|
+
*
|
|
22
|
+
* | Forge | Counts | Writer |
|
|
23
|
+
* |---|---|---|
|
|
24
|
+
* | GitHub | the reviewer's newest deciding review is `APPROVED` and names the head (`commit_id`) | `GET /repos/{repo}/collaborators/{login}/permission` is `admin`, `maintain` or `write` |
|
|
25
|
+
* | Forgejo | the same, not dismissed and not stale | the review is `official`, which Forgejo sets for a reviewer with write access |
|
|
26
|
+
* | GitLab | the merge request's approvals, when the project removes approvals on a new push (`reset_approvals_on_push`), since a GitLab approval names no commit | the approver's access level is Developer (30) or higher |
|
|
27
|
+
*
|
|
28
|
+
* A standing review that requests changes, from a writer, holds the review
|
|
29
|
+
* back. The author's own review never counts.
|
|
30
|
+
*/
|
|
31
|
+
import { type TrustPolicy } from "../workspace/trust/policy.js";
|
|
32
|
+
import { type ForgeFetch, type ForgeKind } from "../pr-forge.js";
|
|
33
|
+
import type { GateApprovalRule } from "./gate.js";
|
|
34
|
+
export { GATE_APPROVAL_SOURCES, type GateApprovalSource } from "./op-waves.js";
|
|
35
|
+
/** The signers policy at `base`, read from the repository at `cwd`. */
|
|
36
|
+
export declare function trustPolicyAt(cwd: string, base: string): TrustPolicy;
|
|
37
|
+
/**
|
|
38
|
+
* The rule a `sealed` gate judges approvals by: a seal that verifies for the
|
|
39
|
+
* approver against `policy`, the signers at base. With no signers file at
|
|
40
|
+
* base nothing verifies, so nothing passes the gate.
|
|
41
|
+
*/
|
|
42
|
+
export declare function sealedApprovalRule(gate: string, policy: TrustPolicy): GateApprovalRule;
|
|
43
|
+
/** Both rules at once: an approval counts only when neither refuses it. */
|
|
44
|
+
export declare function bothRules(a: GateApprovalRule | null, b: GateApprovalRule | null): GateApprovalRule | null;
|
|
45
|
+
/** The digests each wave planned at one commit, recorded for a review of it to approve. */
|
|
46
|
+
export interface HeadPlans {
|
|
47
|
+
version: 1;
|
|
48
|
+
/** The Op waves spec's name. */
|
|
49
|
+
name: string;
|
|
50
|
+
/** The commit the waves were planned at: a pull request's head. */
|
|
51
|
+
head: string;
|
|
52
|
+
timestamp: string;
|
|
53
|
+
waves: Array<{
|
|
54
|
+
wave: number;
|
|
55
|
+
name: string;
|
|
56
|
+
digest: string;
|
|
57
|
+
}>;
|
|
58
|
+
}
|
|
59
|
+
/** Where {@link HeadPlans} for `head` live on `chant/lifecycle`: `_wave-plans/<name>/<head>.json`. */
|
|
60
|
+
export declare const HEAD_PLANS_DIR = "_wave-plans";
|
|
61
|
+
export declare function headPlansPath(name: string, head: string): {
|
|
62
|
+
dir: string;
|
|
63
|
+
file: string;
|
|
64
|
+
};
|
|
65
|
+
/** The merged pull request's review, as a `pr-review` gate reads it. */
|
|
66
|
+
export interface MergedReview {
|
|
67
|
+
pr: number;
|
|
68
|
+
url?: string;
|
|
69
|
+
/** The author, as a principal (`github:<login>`). */
|
|
70
|
+
author: string;
|
|
71
|
+
/** The pull request's head commit. */
|
|
72
|
+
head: string;
|
|
73
|
+
/** Writers other than the author whose approval of the head stands, as principals. */
|
|
74
|
+
approvers: string[];
|
|
75
|
+
/** Why a review did not count, one line each. */
|
|
76
|
+
refused: string[];
|
|
77
|
+
}
|
|
78
|
+
/** What a `pr-review` gate asks of the forge. */
|
|
79
|
+
export interface ReviewSource {
|
|
80
|
+
kind: ForgeKind;
|
|
81
|
+
/** The merged pull request whose merge produced `sha`, with its review, or null when none did. */
|
|
82
|
+
mergedReview(sha: string): Promise<MergedReview | null>;
|
|
83
|
+
}
|
|
84
|
+
export interface ReviewSourceOptions {
|
|
85
|
+
apiBase: string;
|
|
86
|
+
/** `owner/name` on GitHub and Forgejo, the project id on GitLab. */
|
|
87
|
+
repo: string;
|
|
88
|
+
token: string;
|
|
89
|
+
/** The forge's host, for naming people. */
|
|
90
|
+
host: string;
|
|
91
|
+
fetch?: ForgeFetch;
|
|
92
|
+
}
|
|
93
|
+
/** A GitHub reader of pull request reviews. The token needs `pull-requests: read`. */
|
|
94
|
+
export declare function githubReviews(opts: ReviewSourceOptions): ReviewSource;
|
|
95
|
+
/** A Forgejo (or Gitea) reader of pull request reviews. */
|
|
96
|
+
export declare function forgejoReviews(opts: ReviewSourceOptions): ReviewSource;
|
|
97
|
+
/** A GitLab reader of merge request approvals. The token needs `read_api`. */
|
|
98
|
+
export declare function gitlabReviews(opts: ReviewSourceOptions): ReviewSource;
|
|
99
|
+
/**
|
|
100
|
+
* The review source for the CI job this runs in: GitLab CI, Forgejo Actions
|
|
101
|
+
* or GitHub Actions, told apart as `./gate-resume.ts` does. The token is
|
|
102
|
+
* `CHANT_FORGE_TOKEN`, else `GITHUB_TOKEN`/`GH_TOKEN` or `GITLAB_TOKEN`.
|
|
103
|
+
* Throws naming what is missing.
|
|
104
|
+
*/
|
|
105
|
+
export declare function reviewSourceFromEnv(env?: Record<string, string | undefined>, fetchImpl?: ForgeFetch): ReviewSource;
|
|
106
|
+
/**
|
|
107
|
+
* The head of the pull request a CI job runs for, from its environment: the
|
|
108
|
+
* event payload's `pull_request.head.sha` on GitHub and Forgejo (whose
|
|
109
|
+
* checkout is a merge preview, not the head), and on GitLab the merge
|
|
110
|
+
* request's source commit. Undefined outside a pull request's pipeline.
|
|
111
|
+
*/
|
|
112
|
+
export declare function pullRequestHeadFromEnv(env?: Record<string, string | undefined>): string | undefined;
|
|
113
|
+
//# sourceMappingURL=gate-review.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gate-review.d.ts","sourceRoot":"","sources":["../../src/op/gate-review.ts"],"names":[],"mappings":"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"}
|
package/dist/op/gate.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../../src/op/gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,EASL,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACvB,MAAM,0BAA0B,CAAC;AAIlC,OAAO,KAAK,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../../src/op/gate.ts"],"names":[],"mappings":"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"}
|
|
@@ -11,13 +11,20 @@
|
|
|
11
11
|
* needs an approval waits on `<gate>-wave-<k>` bound to
|
|
12
12
|
* {@link waveSetDigest} over its members, and exits 3 when nobody approved
|
|
13
13
|
* that digest.
|
|
14
|
+
* - Which approvals count is the wave's `approval`, read at base like its
|
|
15
|
+
* policy (#3684): `ledger`, `sealed` or `pr-review` (`./gate-review.ts`).
|
|
16
|
+
* Under `pr-review` the pull request's pipeline runs `--record-plans`
|
|
17
|
+
* ({@link recordOpWaveHeadPlans}), which plans every wave at the head and
|
|
18
|
+
* records the digests a review of that head approves.
|
|
14
19
|
* - Apply: each run's `apply` command. A share job (`--share i`) re-plans its
|
|
15
20
|
* slice first and refuses, without applying it, a run whose plan digest is
|
|
16
21
|
* not the one the deciding job recorded (exit 4).
|
|
17
22
|
*/
|
|
18
23
|
import { type WaveMember } from "../gated-waves.js";
|
|
19
24
|
import { type GateLedgerPort } from "./gate.js";
|
|
20
|
-
import {
|
|
25
|
+
import type { HeadPlans, ReviewSource } from "./gate-review.js";
|
|
26
|
+
import type { TrustPolicy } from "../workspace/trust/policy.js";
|
|
27
|
+
import { type GateApprovalSource, type OpWaveGatePolicy, type OpWavesSpec } from "./op-waves.js";
|
|
21
28
|
/** What a run's plan command writes to `{plan}`. */
|
|
22
29
|
export interface OpWavePlanFile {
|
|
23
30
|
planDigest: string;
|
|
@@ -42,6 +49,8 @@ export interface OpWaveDecision {
|
|
|
42
49
|
op: string;
|
|
43
50
|
gate: string;
|
|
44
51
|
policy: OpWaveGatePolicy;
|
|
52
|
+
/** Which approvals count, read at base with the policy (#3684). */
|
|
53
|
+
approval: GateApprovalSource;
|
|
45
54
|
policySource: OpWavePolicySource;
|
|
46
55
|
/** The base commit the policy was read at. */
|
|
47
56
|
base: string;
|
|
@@ -50,6 +59,16 @@ export interface OpWaveDecision {
|
|
|
50
59
|
/** `approved`: an approval of this digest stands. `not-required`: the policy lets it through. `waiting`: neither. */
|
|
51
60
|
status: "approved" | "not-required" | "waiting";
|
|
52
61
|
approvedBy?: string;
|
|
62
|
+
/** How the approval arrived: a resolution on the ledger, or the merged pull request's review (#3684). */
|
|
63
|
+
via?: "ledger" | "pr-review";
|
|
64
|
+
/** The review that approved the wave, or why the review did not, under `pr-review`. */
|
|
65
|
+
review?: {
|
|
66
|
+
pr?: number;
|
|
67
|
+
head?: string;
|
|
68
|
+
approvers?: string[];
|
|
69
|
+
url?: string;
|
|
70
|
+
reason?: string;
|
|
71
|
+
};
|
|
53
72
|
/** The command that approves this digest, when waiting. */
|
|
54
73
|
approve?: string;
|
|
55
74
|
}
|
|
@@ -69,6 +88,7 @@ export declare function parseOpWavesSpec(text: string, where: string): OpWavesSp
|
|
|
69
88
|
*/
|
|
70
89
|
export declare function readOpWavePolicy(specFile: string, name: string, base: string, cwd: string, show?: OpWaveShowAtBase): {
|
|
71
90
|
policy: OpWaveGatePolicy;
|
|
91
|
+
approval: GateApprovalSource;
|
|
72
92
|
source: OpWavePolicySource;
|
|
73
93
|
base: string;
|
|
74
94
|
};
|
|
@@ -98,6 +118,14 @@ export interface RunOpWaveOptions {
|
|
|
98
118
|
show?: OpWaveShowAtBase;
|
|
99
119
|
now?: string;
|
|
100
120
|
log?: (line: string) => void;
|
|
121
|
+
/** The applied commit, which a `pr-review` gate finds the merged pull request by. Default `git rev-parse HEAD`. */
|
|
122
|
+
head?: string;
|
|
123
|
+
/** The forge's reviews, for a `pr-review` gate. Default: the CI job's forge (`reviewSourceFromEnv`). */
|
|
124
|
+
reviews?: ReviewSource;
|
|
125
|
+
/** The plans recorded for a pull request's head. Default: read from `chant/lifecycle`. */
|
|
126
|
+
headPlans?: (name: string, head: string) => Promise<HeadPlans | null>;
|
|
127
|
+
/** The signers policy at base, for a `sealed` gate. Default: read from the base commit. */
|
|
128
|
+
trust?: (base: string) => TrustPolicy;
|
|
101
129
|
}
|
|
102
130
|
export interface RunOpWaveResult {
|
|
103
131
|
/** 0 applied (or decided), 1 a run failed, 3 waiting for an approval, 4 a plan moved since the decision. */
|
|
@@ -112,4 +140,22 @@ export interface RunOpWaveResult {
|
|
|
112
140
|
export declare function opWaveNeedsApproval(policy: OpWaveGatePolicy, members: readonly OpWaveMember[]): boolean;
|
|
113
141
|
/** Plan, decide and apply one wave, or one part of a wide wave. */
|
|
114
142
|
export declare function runOpWave(options: RunOpWaveOptions): Promise<RunOpWaveResult>;
|
|
143
|
+
export interface RecordHeadPlansOptions {
|
|
144
|
+
spec: OpWavesSpec;
|
|
145
|
+
cwd?: string;
|
|
146
|
+
exec?: OpWaveExec;
|
|
147
|
+
/** The commit the plans are for. Default: the pull request's head from the CI job's environment, else `git rev-parse HEAD`. */
|
|
148
|
+
head?: string;
|
|
149
|
+
now?: string;
|
|
150
|
+
/** Write the record. Default: append to `chant/lifecycle` and push. */
|
|
151
|
+
write?: (plans: HeadPlans) => Promise<void>;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* `chant run wave --spec <file> --record-plans` (#3684): plan every wave at a
|
|
155
|
+
* pull request's head and record each wave's digest on `chant/lifecycle`,
|
|
156
|
+
* `_wave-plans/<name>/<head>.json`. A `pr-review` wave reads it after the
|
|
157
|
+
* merge: the review of that head approves the digests it planned, and only
|
|
158
|
+
* those.
|
|
159
|
+
*/
|
|
160
|
+
export declare function recordOpWaveHeadPlans(options: RecordHeadPlansOptions): Promise<HeadPlans>;
|
|
115
161
|
//# sourceMappingURL=op-waves-run.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"op-waves-run.d.ts","sourceRoot":"","sources":["../../src/op/op-waves-run.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"op-waves-run.d.ts","sourceRoot":"","sources":["../../src/op/op-waves-run.ts"],"names":[],"mappings":"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"}
|
package/dist/op/op-waves.d.ts
CHANGED
|
@@ -27,6 +27,12 @@
|
|
|
27
27
|
* the same digest a component fan-out's wave binds). Whether it waits at all
|
|
28
28
|
* is the wave's gate policy, read from the base commit's copy of the spec
|
|
29
29
|
* file, so a change cannot loosen the rule that gates its own merge.
|
|
30
|
+
*
|
|
31
|
+
* So is which approvals count (#3684), the wave's `approval`: `ledger` (any
|
|
32
|
+
* `chant approve` of the digest), `sealed` (only one whose seal verifies
|
|
33
|
+
* against the signers file at base) or `pr-review` (also the merged pull
|
|
34
|
+
* request's review of its head, by a writer other than its author, when the
|
|
35
|
+
* head planned the same digest). See `./gate-review.ts`.
|
|
30
36
|
*/
|
|
31
37
|
import type { OpEnvironment, OpSetupStep } from "../lexicon.js";
|
|
32
38
|
/** When a wave waits for an approval. */
|
|
@@ -38,6 +44,15 @@ export type OpWaveGatePolicy =
|
|
|
38
44
|
/** Never: only review and branch protection stand in front of the wave. */
|
|
39
45
|
| "never";
|
|
40
46
|
export declare const OP_WAVE_GATE_POLICIES: readonly OpWaveGatePolicy[];
|
|
47
|
+
/** Which approvals of a gate's digest count (#3684). See `./gate-review.ts`. */
|
|
48
|
+
export type GateApprovalSource =
|
|
49
|
+
/** Any resolution on `chant/lifecycle` that names the digest. */
|
|
50
|
+
"ledger"
|
|
51
|
+
/** As `ledger`, and the merged pull request's review of its head, by a writer other than its author. */
|
|
52
|
+
| "pr-review"
|
|
53
|
+
/** Only a resolution sealed by a key the signers file at base lists for its approver. */
|
|
54
|
+
| "sealed";
|
|
55
|
+
export declare const GATE_APPROVAL_SOURCES: readonly GateApprovalSource[];
|
|
41
56
|
/** One Op run in a wave: an Op applied to one target (an environment, a database, a tenant). */
|
|
42
57
|
export interface OpWaveRun {
|
|
43
58
|
/** What the run applies to. Unique within its wave; it names the run in the set digest. */
|
|
@@ -55,6 +70,11 @@ export interface OpWave {
|
|
|
55
70
|
* spec file at the base commit, never from the change being applied.
|
|
56
71
|
*/
|
|
57
72
|
gate?: OpWaveGatePolicy;
|
|
73
|
+
/**
|
|
74
|
+
* Which approvals of the wave's digest count (#3684). Default `"ledger"`.
|
|
75
|
+
* Like `gate`, the runner reads it from the spec file at the base commit.
|
|
76
|
+
*/
|
|
77
|
+
approval?: GateApprovalSource;
|
|
58
78
|
/**
|
|
59
79
|
* Split the wave into a deciding job and this many share jobs, each
|
|
60
80
|
* applying a slice of the runs. Default 1: one job plans, decides and
|
|
@@ -97,7 +117,26 @@ export interface OpWavesSpec {
|
|
|
97
117
|
/** Apply one run whose plan is at `{plan}`. Default `chant run {op} --env {target}`. */
|
|
98
118
|
apply?: string[];
|
|
99
119
|
waves: OpWave[];
|
|
120
|
+
/**
|
|
121
|
+
* A scheduled job that starts again a wave job whose approval has arrived
|
|
122
|
+
* (#3683): it runs `chant run resume --op <name>`, which re-runs the
|
|
123
|
+
* GitHub run, retries the GitLab job or dispatches the Forgejo workflow.
|
|
124
|
+
* It approves nothing. Absent, no such job is rendered and an approval is
|
|
125
|
+
* followed by `chant approve --resume` or a re-run by hand.
|
|
126
|
+
*/
|
|
127
|
+
resume?: OpWavesResume;
|
|
128
|
+
}
|
|
129
|
+
/** The scheduled resume job of an Op waves pipeline (#3683). */
|
|
130
|
+
export interface OpWavesResume {
|
|
131
|
+
/**
|
|
132
|
+
* Five-field cron, such as `"*\/10 * * * *"`. GitHub and Forgejo put it
|
|
133
|
+
* in the workflow; GitLab runs the job in a pipeline schedule you create,
|
|
134
|
+
* which sets the cron.
|
|
135
|
+
*/
|
|
136
|
+
schedule: string;
|
|
100
137
|
}
|
|
138
|
+
/** The command a resume job runs. */
|
|
139
|
+
export declare function opWavesResumeCommand(spec: Pick<OpWavesSpec, "name">): string[];
|
|
101
140
|
/** The default apply command. */
|
|
102
141
|
export declare const OP_WAVE_DEFAULT_APPLY: string[];
|
|
103
142
|
/** Where the runner keeps a wave's decision; a wide wave's deciding job hands it to its share jobs. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"op-waves.d.ts","sourceRoot":"","sources":["../../src/op/op-waves.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"op-waves.d.ts","sourceRoot":"","sources":["../../src/op/op-waves.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/build.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { discover, type DiscoveryResult, type FoldDecision } from "./discovery/i
|
|
|
19
19
|
import { decodeEntitySet, type DiscoveredEntitiesJson } from "./discovery/entity-wire";
|
|
20
20
|
import { buildDependencyGraph } from "./discovery/graph";
|
|
21
21
|
import { topologicalSort } from "./sort";
|
|
22
|
+
import { statSync } from "node:fs";
|
|
22
23
|
import { resolve } from "node:path";
|
|
23
24
|
|
|
24
25
|
/**
|
|
@@ -641,6 +642,15 @@ function generateManifest(
|
|
|
641
642
|
};
|
|
642
643
|
}
|
|
643
644
|
|
|
645
|
+
/** The directory a build discovered from, for build-root contributors; undefined when the build's path is not one (an entity-set JSON's label). */
|
|
646
|
+
function sourceDirOf(path: string): string | undefined {
|
|
647
|
+
try {
|
|
648
|
+
return statSync(path).isDirectory() ? resolve(path) : undefined;
|
|
649
|
+
} catch {
|
|
650
|
+
return undefined;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
644
654
|
/** What merging build-root contributions produced: non-fatal render notes and
|
|
645
655
|
* fatal messages (a failed contributor, a name collision). */
|
|
646
656
|
export interface BuildRootMergeResult {
|
|
@@ -667,6 +677,7 @@ export interface BuildRootMergeResult {
|
|
|
667
677
|
export async function mergeBuildRootEntities(
|
|
668
678
|
entities: Map<string, Declarable>,
|
|
669
679
|
contributors: ReadonlyArray<BuildRootContributor>,
|
|
680
|
+
sourceDir?: string,
|
|
670
681
|
): Promise<BuildRootMergeResult> {
|
|
671
682
|
const warnings: string[] = [];
|
|
672
683
|
const errors: string[] = [];
|
|
@@ -677,7 +688,7 @@ export async function mergeBuildRootEntities(
|
|
|
677
688
|
// ciphertext file to resolve). Contributors run in order, so this also
|
|
678
689
|
// carries what earlier contributors added; the merge below is still the
|
|
679
690
|
// only writer, which is what keeps the collision refusal meaningful.
|
|
680
|
-
const contribution = await contribute({ entities });
|
|
691
|
+
const contribution = await contribute({ entities, ...(sourceDir !== undefined ? { sourceDir } : {}) });
|
|
681
692
|
warnings.push(...(contribution.warnings ?? []));
|
|
682
693
|
for (const [name, entity] of contribution.entities) {
|
|
683
694
|
if (entities.has(name)) {
|
|
@@ -809,7 +820,7 @@ async function buildFromDiscoveryResult(
|
|
|
809
820
|
// object, and re-running the contributors there would duplicate every
|
|
810
821
|
// contributed entity once per child.
|
|
811
822
|
if (!parentBuildStack && options?.buildRoots) {
|
|
812
|
-
const merged = await mergeBuildRootEntities(discoveryResult.entities, options.buildRoots);
|
|
823
|
+
const merged = await mergeBuildRootEntities(discoveryResult.entities, options.buildRoots, sourceDirOf(resolvedPathForChildStack));
|
|
813
824
|
warnings.push(...merged.warnings);
|
|
814
825
|
for (const message of merged.errors) {
|
|
815
826
|
errors.push(new BuildErrorClass("", message));
|
|
@@ -922,7 +922,8 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
|
|
|
922
922
|
const resourceCount = result.entities.size;
|
|
923
923
|
const fileCount = result.sourceFileCount;
|
|
924
924
|
|
|
925
|
-
|
|
925
|
+
// A build root (a lexicon's own source files, a rendered overlay) can declare everything without one .ts file.
|
|
926
|
+
if (fileCount === 0 && resourceCount === 0 && errors.length === 0) {
|
|
926
927
|
console.error(formatInfo("No source files found — create .ts files in the target directory"));
|
|
927
928
|
}
|
|
928
929
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import type { GateResolutionRecord, PendingGateRecord } from "../../lifecycle/gate-ledger";
|
|
3
|
+
import type { ForgeFetch } from "../../pr-forge";
|
|
4
|
+
import { parseArgs } from "../main";
|
|
5
|
+
import { resumeStanding } from "./gate-resume";
|
|
6
|
+
|
|
7
|
+
const DIGEST = `sha256:${"a".repeat(64)}`;
|
|
8
|
+
const standing: PendingGateRecord = {
|
|
9
|
+
version: 1,
|
|
10
|
+
kind: "pending",
|
|
11
|
+
op: "migrations",
|
|
12
|
+
gate: "migrations-wave-2",
|
|
13
|
+
timestamp: "2026-10-01T00:00:00Z",
|
|
14
|
+
expiresAt: "2026-10-03T00:00:00Z",
|
|
15
|
+
planDigest: DIGEST,
|
|
16
|
+
resume: { forge: "github", api: "https://api.github.com", repo: "acme/db", run: "42", attempt: 1 },
|
|
17
|
+
};
|
|
18
|
+
const approval: GateResolutionRecord = { version: 1, op: "migrations", gate: "migrations-wave-2", resolvedBy: "alice", timestamp: "2026-10-01T01:00:00Z", planDigest: DIGEST };
|
|
19
|
+
|
|
20
|
+
describe("resumeStanding (#3683)", () => {
|
|
21
|
+
test("no approval, no call", async () => {
|
|
22
|
+
const fetch: ForgeFetch = async () => {
|
|
23
|
+
throw new Error("no call expected");
|
|
24
|
+
};
|
|
25
|
+
expect(await resumeStanding(standing, [], { env: { GITHUB_TOKEN: "t" }, fetch })).toMatchObject({ status: "not-approved" });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("approved: re-runs the run the pending fact names", async () => {
|
|
29
|
+
const calls: string[] = [];
|
|
30
|
+
const fetch: ForgeFetch = async (url, init) => {
|
|
31
|
+
calls.push(`${init.method} ${new URL(url).pathname}`);
|
|
32
|
+
const body = init.method === "GET" ? { status: "completed", conclusion: "failure", run_attempt: 1 } : null;
|
|
33
|
+
return { ok: true, status: 200, text: async () => (body ? JSON.stringify(body) : "") };
|
|
34
|
+
};
|
|
35
|
+
const report = await resumeStanding(standing, [approval], { env: { GITHUB_TOKEN: "t" }, fetch });
|
|
36
|
+
expect(report).toMatchObject({ status: "resumed", approvedBy: "alice" });
|
|
37
|
+
expect(calls).toEqual(["GET /repos/acme/db/actions/runs/42", "POST /repos/acme/db/actions/runs/42/rerun-failed-jobs"]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("a fact recorded outside CI, or no token, resumes nothing and says why", async () => {
|
|
41
|
+
const { resume: _none, ...local } = standing;
|
|
42
|
+
expect(await resumeStanding(local, [approval], { env: { GITHUB_TOKEN: "t" } })).toMatchObject({ status: "no-job" });
|
|
43
|
+
expect(await resumeStanding(standing, [approval], { env: {} })).toMatchObject({ status: "no-token" });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("`--resume` takes no value on approve, and keeps its file on the applies", () => {
|
|
47
|
+
expect(parseArgs(["approve", "migrations", "migrations-wave-2", "--resume"]).resumeRun).toBe(true);
|
|
48
|
+
expect(parseArgs(["approve", "migrations", "g", "--resume", "--sign"])).toMatchObject({ resumeRun: true, sign: true });
|
|
49
|
+
expect(parseArgs(["components", "pr-apply", "--resume", "attempt.json"]).resume).toBe("attempt.json");
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant approve <op> <gate> --resume` and `chant run resume` (#3683): start
|
|
3
|
+
* the CI job that waits at a gate again once its approval has arrived. See
|
|
4
|
+
* `../../op/gate-resume.ts` for what each forge is asked and the token it
|
|
5
|
+
* needs. Neither approves anything.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
GATES_DIR,
|
|
10
|
+
isPendingGateExpired,
|
|
11
|
+
readGateLedger,
|
|
12
|
+
type GateResolutionRecord,
|
|
13
|
+
type PendingGateRecord,
|
|
14
|
+
} from "../../lifecycle/gate-ledger";
|
|
15
|
+
import { listFilesInDir, requireLifecycleLedger } from "../../lifecycle/git";
|
|
16
|
+
import {
|
|
17
|
+
isGateRunLocator,
|
|
18
|
+
resolutionAnswering,
|
|
19
|
+
resumeGateRun,
|
|
20
|
+
resumeTokenFromEnv,
|
|
21
|
+
type GateResumeOutcome,
|
|
22
|
+
} from "../../op/gate-resume";
|
|
23
|
+
import type { ForgeFetch } from "../../pr-forge";
|
|
24
|
+
import { formatError, formatInfo, formatSuccess, formatWarning } from "../format";
|
|
25
|
+
import type { CommandContext } from "../registry";
|
|
26
|
+
|
|
27
|
+
/** One waiting gate and what resuming it did. */
|
|
28
|
+
export interface GateResumeReport {
|
|
29
|
+
op: string;
|
|
30
|
+
gate: string;
|
|
31
|
+
environment?: string;
|
|
32
|
+
/** `resumed` and `skipped` come from the forge; the rest are decided before any call. */
|
|
33
|
+
status: GateResumeOutcome["status"] | "not-approved" | "no-job" | "no-token" | "failed";
|
|
34
|
+
message: string;
|
|
35
|
+
approvedBy?: string;
|
|
36
|
+
url?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The newest pending fact per gate and environment: the facts a run still waits on. */
|
|
40
|
+
function standingFacts(pending: readonly PendingGateRecord[], gate?: string): PendingGateRecord[] {
|
|
41
|
+
const newest = new Map<string, PendingGateRecord>();
|
|
42
|
+
for (const p of pending) {
|
|
43
|
+
if (gate !== undefined && p.gate !== gate) continue;
|
|
44
|
+
const key = `${p.gate}\u0000${p.environment ?? ""}`;
|
|
45
|
+
const prior = newest.get(key);
|
|
46
|
+
if (!prior || new Date(p.timestamp).getTime() >= new Date(prior.timestamp).getTime()) newest.set(key, p);
|
|
47
|
+
}
|
|
48
|
+
return [...newest.values()];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ResumeGateOptions {
|
|
52
|
+
env?: Record<string, string | undefined>;
|
|
53
|
+
fetch?: ForgeFetch;
|
|
54
|
+
now?: string;
|
|
55
|
+
dryRun?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Resume the job behind one standing pending fact, when an approval answers it. */
|
|
59
|
+
export async function resumeStanding(
|
|
60
|
+
standing: PendingGateRecord,
|
|
61
|
+
resolutions: readonly GateResolutionRecord[],
|
|
62
|
+
opts: ResumeGateOptions = {},
|
|
63
|
+
): Promise<GateResumeReport> {
|
|
64
|
+
const base = {
|
|
65
|
+
op: standing.op,
|
|
66
|
+
gate: standing.gate,
|
|
67
|
+
...(standing.environment !== undefined ? { environment: standing.environment } : {}),
|
|
68
|
+
...(standing.resume?.url ? { url: standing.resume.url } : {}),
|
|
69
|
+
};
|
|
70
|
+
const answer = resolutionAnswering(resolutions, standing);
|
|
71
|
+
if (!answer) return { ...base, status: "not-approved", message: "no approval answers it yet" };
|
|
72
|
+
const approvedBy = { approvedBy: answer.resolvedBy };
|
|
73
|
+
if (!isGateRunLocator(standing.resume)) {
|
|
74
|
+
return { ...base, ...approvedBy, status: "no-job", message: "its pending fact names no CI job (it was recorded outside CI); run it again by hand" };
|
|
75
|
+
}
|
|
76
|
+
const loc = standing.resume;
|
|
77
|
+
if (opts.dryRun) return { ...base, ...approvedBy, status: "skipped", message: `would resume ${loc.forge} run ${loc.run} (--dry-run)` };
|
|
78
|
+
const token = resumeTokenFromEnv(loc.forge, opts.env ?? process.env);
|
|
79
|
+
if (!token) {
|
|
80
|
+
return {
|
|
81
|
+
...base,
|
|
82
|
+
...approvedBy,
|
|
83
|
+
status: "no-token",
|
|
84
|
+
message: `no token to call ${loc.forge} with: set CHANT_FORGE_TOKEN (or ${loc.forge === "gitlab" ? "GITLAB_TOKEN" : "GITHUB_TOKEN"})`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const outcome = await resumeGateRun(loc, { token, since: answer.timestamp, ...(opts.fetch ? { fetch: opts.fetch } : {}) });
|
|
89
|
+
return { ...base, ...approvedBy, status: outcome.status, message: outcome.status === "resumed" ? outcome.how : outcome.reason };
|
|
90
|
+
} catch (err) {
|
|
91
|
+
return { ...base, ...approvedBy, status: "failed", message: err instanceof Error ? err.message : String(err) };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Resume every job waiting on `op`'s gates (or one gate) whose approval arrived. */
|
|
96
|
+
export async function resumeOpGates(op: string, gate: string | undefined, opts: ResumeGateOptions = {}): Promise<GateResumeReport[]> {
|
|
97
|
+
const ledger = await readGateLedger(op);
|
|
98
|
+
const now = opts.now ?? new Date().toISOString();
|
|
99
|
+
const reports: GateResumeReport[] = [];
|
|
100
|
+
for (const standing of standingFacts(ledger.pending, gate)) {
|
|
101
|
+
if (isPendingGateExpired(standing, now)) continue;
|
|
102
|
+
reports.push(await resumeStanding(standing, ledger.resolutions, opts));
|
|
103
|
+
}
|
|
104
|
+
return reports;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function printReport(r: GateResumeReport): void {
|
|
108
|
+
const where = `${r.op} / ${r.gate}${r.environment !== undefined ? ` (${r.environment})` : ""}`;
|
|
109
|
+
const by = r.approvedBy ? `, approved by ${r.approvedBy}` : "";
|
|
110
|
+
const link = r.url ? ` ${r.url}` : "";
|
|
111
|
+
if (r.status === "resumed") console.error(formatSuccess(`${where}${by}: ${r.message}${link}`));
|
|
112
|
+
else if (r.status === "failed" || r.status === "no-token") {
|
|
113
|
+
console.error(formatError({
|
|
114
|
+
message: `${where}${by}: not resumed: ${r.message}`,
|
|
115
|
+
hint: "GitHub needs actions: write; GitLab a token with the api scope and the Developer role; Forgejo a token with write:repository.",
|
|
116
|
+
}));
|
|
117
|
+
} else console.error(formatInfo(`${where}${by}: not resumed: ${r.message}${link}`));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* What `chant approve --resume` runs once the approval is recorded and
|
|
122
|
+
* pushed. Returns the exit code: 0 when the job was resumed or had nothing to
|
|
123
|
+
* resume, 1 when a forge call failed or no token was found.
|
|
124
|
+
*/
|
|
125
|
+
export async function resumeAfterApproval(op: string, gate: string, environment: string | undefined, pushed: boolean): Promise<number> {
|
|
126
|
+
if (!pushed) {
|
|
127
|
+
console.error(formatWarning({
|
|
128
|
+
message: "Not resuming: the approval did not reach the remote, so the resumed job would not see it.",
|
|
129
|
+
hint: "Push chant/lifecycle, then run `chant run resume --op " + op + "`.",
|
|
130
|
+
}));
|
|
131
|
+
return 1;
|
|
132
|
+
}
|
|
133
|
+
const reports = (await resumeOpGates(op, gate)).filter((r) => environment === undefined || r.environment === environment);
|
|
134
|
+
if (reports.length === 0) {
|
|
135
|
+
console.error(formatInfo(`No pending fact for ${op} / ${gate} is waiting, so there is nothing to resume.`));
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
138
|
+
for (const r of reports) printReport(r);
|
|
139
|
+
return reports.some((r) => r.status === "failed" || r.status === "no-token") ? 1 : 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* `chant run resume [--op <op>] [--dry-run] [--json]`: the scheduled job.
|
|
144
|
+
* Reads every gate ledger on `chant/lifecycle` (or `--op`'s), and starts
|
|
145
|
+
* again each CI job that waits at a gate an approval now answers.
|
|
146
|
+
*/
|
|
147
|
+
export async function runResumeCommand(ctx: CommandContext): Promise<number> {
|
|
148
|
+
try {
|
|
149
|
+
await requireLifecycleLedger();
|
|
150
|
+
} catch (err) {
|
|
151
|
+
console.error(formatError({ message: `Cannot read the gate ledgers: ${err instanceof Error ? err.message : String(err)}` }));
|
|
152
|
+
return 1;
|
|
153
|
+
}
|
|
154
|
+
const ops = ctx.args.op
|
|
155
|
+
? [ctx.args.op]
|
|
156
|
+
: (await listFilesInDir(GATES_DIR)).filter((f) => f.endsWith(".jsonl")).map((f) => f.slice(0, -".jsonl".length));
|
|
157
|
+
const reports: GateResumeReport[] = [];
|
|
158
|
+
for (const op of ops) reports.push(...(await resumeOpGates(op, undefined, { dryRun: ctx.args.dryRun === true })));
|
|
159
|
+
if (ctx.args.json) console.log(JSON.stringify({ reports }, null, 2));
|
|
160
|
+
else {
|
|
161
|
+
for (const r of reports.filter((r) => r.status !== "not-approved")) printReport(r);
|
|
162
|
+
const waiting = reports.filter((r) => r.status === "not-approved").length;
|
|
163
|
+
console.error(formatInfo(`${reports.filter((r) => r.status === "resumed").length} resumed, ${waiting} still waiting for an approval.`));
|
|
164
|
+
}
|
|
165
|
+
return reports.some((r) => r.status === "failed" || r.status === "no-token") ? 1 : 0;
|
|
166
|
+
}
|
|
@@ -911,6 +911,12 @@ export async function runApprove(ctx: CommandContext): Promise<number> {
|
|
|
911
911
|
});
|
|
912
912
|
if (!outcome.ok) return 1;
|
|
913
913
|
|
|
914
|
+
// #3683: start the CI job that waits at this gate again.
|
|
915
|
+
if (ctx.args.resumeRun || ctx.args.resume !== undefined) {
|
|
916
|
+
const { resumeAfterApproval } = await import("./gate-resume");
|
|
917
|
+
return resumeAfterApproval(opName, gate, outcome.record.environment, outcome.pushed);
|
|
918
|
+
}
|
|
919
|
+
|
|
914
920
|
console.error(formatInfo(
|
|
915
921
|
`This records the resolution as a fact; it does not itself re-run anything. ` +
|
|
916
922
|
(opName === FAN_OUT_GATE_OP
|
|
@@ -988,7 +994,7 @@ export interface GateApprovalOptions {
|
|
|
988
994
|
}
|
|
989
995
|
|
|
990
996
|
export type GateApprovalOutcome =
|
|
991
|
-
| { ok: true; record: GateResolutionRecord }
|
|
997
|
+
| { ok: true; record: GateResolutionRecord; pushed: boolean }
|
|
992
998
|
| { ok: false };
|
|
993
999
|
|
|
994
1000
|
/**
|
|
@@ -1313,7 +1319,7 @@ export async function recordGateApproval(
|
|
|
1313
1319
|
console.error(formatInfo(line));
|
|
1314
1320
|
}
|
|
1315
1321
|
}
|
|
1316
|
-
return { ok: true, record };
|
|
1322
|
+
return { ok: true, record, pushed };
|
|
1317
1323
|
}
|
|
1318
1324
|
|
|
1319
1325
|
/**
|