@intentius/chant-lexicon-github 0.124.54 → 0.124.56

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.
Files changed (39) hide show
  1. package/dist/components/agent-jobs.d.ts +67 -0
  2. package/dist/components/agent-jobs.d.ts.map +1 -0
  3. package/dist/components/agent-jobs.js +139 -0
  4. package/dist/components/control-pipeline.d.ts +34 -0
  5. package/dist/components/control-pipeline.d.ts.map +1 -0
  6. package/dist/components/control-pipeline.js +45 -0
  7. package/dist/components/generate-op-pipeline.d.ts +8 -1
  8. package/dist/components/generate-op-pipeline.d.ts.map +1 -1
  9. package/dist/components/generate-op-pipeline.js +144 -16
  10. package/dist/components/generate-op-waves-pipeline.d.ts +27 -1
  11. package/dist/components/generate-op-waves-pipeline.d.ts.map +1 -1
  12. package/dist/components/generate-op-waves-pipeline.js +135 -16
  13. package/dist/components/generate-pipeline.d.ts.map +1 -1
  14. package/dist/components/generate-pipeline.js +160 -15
  15. package/dist/integrity.json +2 -2
  16. package/dist/manifest.json +1 -1
  17. package/package.json +2 -2
  18. package/src/components/__golden__/op-waves-jobs.yml +20 -0
  19. package/src/components/agent-jobs.ts +177 -0
  20. package/src/components/control-pipeline.ts +65 -0
  21. package/src/components/generate-op-pipeline.agent.test.ts +147 -0
  22. package/src/components/generate-op-pipeline.cloud.test.ts +96 -0
  23. package/src/components/generate-op-pipeline.notice.test.ts +2 -1
  24. package/src/components/generate-op-pipeline.ts +165 -20
  25. package/src/components/generate-op-waves-pipeline.approval.test.ts +46 -0
  26. package/src/components/generate-op-waves-pipeline.control.test.ts +64 -0
  27. package/src/components/generate-op-waves-pipeline.guard.test.ts +57 -0
  28. package/src/components/generate-op-waves-pipeline.identity.test.ts +8 -8
  29. package/src/components/generate-op-waves-pipeline.test.ts +17 -0
  30. package/src/components/generate-op-waves-pipeline.ts +157 -15
  31. package/src/components/generate-pipeline.ts +170 -15
  32. package/src/components/generate-pr-pipeline.comment-locks.test.ts +53 -0
  33. package/src/components/generate-pr-pipeline.comment-preview.test.ts +64 -0
  34. package/src/components/generate-pr-pipeline.comment-targets.test.ts +24 -0
  35. package/src/components/generate-pr-pipeline.control.test.ts +94 -0
  36. package/src/components/generate-pr-pipeline.pulumi-identity.test.ts +43 -0
  37. package/src/components/generate-pr-pipeline.respond.test.ts +47 -0
  38. package/src/components/generate-pr-pipeline.runners.test.ts +61 -0
  39. package/src/components/generate-pr-pipeline.stale-notes.test.ts +42 -0
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The agent jobs on GitHub and Forgejo (#4616): `ScheduledOpSpec.agent`.
3
+ * What each job does is @intentius/chant/ci-agent's; this file lays them
4
+ * out so the model never shares a job with a write token:
5
+ *
6
+ * - the agent job holds `contents: read` and `pull-requests: read`. Its
7
+ * checkout script alone sees the job token, for the fetches (the target
8
+ * branch's commit, chant/lifecycle for the run count, and for a comment
9
+ * the pull request's head). `chant agent prepare` reads with the job
10
+ * token; `chant agent run` is the last step and starts without any forge
11
+ * token, with the model's key from its secret, and hands the agent
12
+ * directory on as the job's outputs (`handoverTrap`).
13
+ * - the push job checks out the target branch's commit, takes the agent
14
+ * directory back as data and runs `chant agent land` with the push token:
15
+ * `tokenEnv`'s secret, else the job token. A push made with the job token
16
+ * starts no workflow on GitHub, so the pull request is planned again only
17
+ * with a token of a user or an app.
18
+ *
19
+ * Forgejo's runner ignores `permissions:` and has no author association, so
20
+ * there `chant agent prepare` asks the forge whether the commenter can write,
21
+ * and both jobs drop the token variables before any step that does not need
22
+ * them.
23
+ */
24
+ import { type CiRunner } from "@intentius/chant/ci-jobs";
25
+ import { type AgentSettings } from "@intentius/chant/ci-agent";
26
+ /** The agent step's id, which the agent job's outputs come from. */
27
+ export declare const AGENT_STEP_ID = "chant-agent";
28
+ /** The drift Op's run step id, whose outputs carry the drift and the code. */
29
+ export declare const AGENT_DRIFT_STEP_ID = "chant-run";
30
+ /** The name of the push job beside the agent job. */
31
+ export declare function agentPushJobName(jobName: string): string;
32
+ /** The agent job's name for a drift Op's job. */
33
+ export declare function agentJobName(jobName: string, on: AgentSettings["on"]): string;
34
+ export interface GithubAgentJobsInput {
35
+ forge: "github" | "forgejo";
36
+ /** The agent job's name. */
37
+ jobName: string;
38
+ image: string;
39
+ settings: AgentSettings;
40
+ runner?: CiRunner;
41
+ /** Setup steps, as built by `setupStepDoc`. */
42
+ setup: Array<Record<string, unknown>>;
43
+ beforeScript: readonly string[];
44
+ /** The secret holding the push token; absent: the job token. */
45
+ tokenEnv?: string;
46
+ /** The command in place of `chant`: the product's bin. */
47
+ bin?: string;
48
+ /** For a drift agent: the Op's job, which handed the drift on. */
49
+ after?: string;
50
+ }
51
+ /** The comment condition of the agent job: a pull request comment that starts with the agent's line, from a writer on GitHub. */
52
+ export declare function agentCommentIf(forge: "github" | "forgejo", comment: string): string;
53
+ /**
54
+ * The drift Op's run step (#4616): the run with its output kept as the
55
+ * drift for the agent, `drift=true` among the step's outputs when it exits
56
+ * with chant's `drift` code, and the output handed on. The step still exits
57
+ * with the run's code.
58
+ */
59
+ export declare function driftRunStep(invocation: string, env: Record<string, string>, forge: "github" | "forgejo"): Record<string, unknown>;
60
+ /** The drift Op job's outputs: the handed-on drift and the drift flag. */
61
+ export declare function driftRunOutputs(forge: "github" | "forgejo"): Record<string, string>;
62
+ /** The agent job and the push job. */
63
+ export declare function githubAgentJobs(i: GithubAgentJobsInput): {
64
+ agent: Record<string, unknown>;
65
+ push: Record<string, unknown>;
66
+ };
67
+ //# sourceMappingURL=agent-jobs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-jobs.d.ts","sourceRoot":"","sources":["../../src/components/agent-jobs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,OAAO,EAAwE,KAAK,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAC/H,OAAO,EAUL,KAAK,aAAa,EACnB,MAAM,2BAA2B,CAAC;AAGnC,oEAAoE;AACpE,eAAO,MAAM,aAAa,gBAAgB,CAAC;AAC3C,8EAA8E;AAC9E,eAAO,MAAM,mBAAmB,cAAc,CAAC;AAQ/C,qDAAqD;AACrD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,iDAAiD;AACjD,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,aAAa,CAAC,IAAI,CAAC,GAAG,MAAM,CAE7E;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,QAAQ,GAAG,SAAS,CAAC;IAC5B,4BAA4B;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,aAAa,CAAC;IACxB,MAAM,CAAC,EAAE,QAAQ,CAAC;IAClB,+CAA+C;IAC/C,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACtC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0DAA0D;IAC1D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,kEAAkE;IAClE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,iIAAiI;AACjI,wBAAgB,cAAc,CAAC,KAAK,EAAE,QAAQ,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAOnF;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,KAAK,EAAE,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAclI;AAED,0EAA0E;AAC1E,wBAAgB,eAAe,CAAC,KAAK,EAAE,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAEnF;AAED,sCAAsC;AACtC,wBAAgB,eAAe,CAAC,CAAC,EAAE,oBAAoB,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CA2D1H"}
@@ -0,0 +1,139 @@
1
+ /**
2
+ * The agent jobs on GitHub and Forgejo (#4616): `ScheduledOpSpec.agent`.
3
+ * What each job does is @intentius/chant/ci-agent's; this file lays them
4
+ * out so the model never shares a job with a write token:
5
+ *
6
+ * - the agent job holds `contents: read` and `pull-requests: read`. Its
7
+ * checkout script alone sees the job token, for the fetches (the target
8
+ * branch's commit, chant/lifecycle for the run count, and for a comment
9
+ * the pull request's head). `chant agent prepare` reads with the job
10
+ * token; `chant agent run` is the last step and starts without any forge
11
+ * token, with the model's key from its secret, and hands the agent
12
+ * directory on as the job's outputs (`handoverTrap`).
13
+ * - the push job checks out the target branch's commit, takes the agent
14
+ * directory back as data and runs `chant agent land` with the push token:
15
+ * `tokenEnv`'s secret, else the job token. A push made with the job token
16
+ * starts no workflow on GitHub, so the pull request is planned again only
17
+ * with a token of a user or an app.
18
+ *
19
+ * Forgejo's runner ignores `permissions:` and has no author association, so
20
+ * there `chant agent prepare` asks the forge whether the commenter can write,
21
+ * and both jobs drop the token variables before any step that does not need
22
+ * them.
23
+ */
24
+ import { CHECKOUT_TOKEN_ENV, checkoutScript, dropForgeTokens, handoverOutputs, handoverTrap, takeOverScript } from "@intentius/chant/ci-handover";
25
+ import { githubRunsOn, GITHUB_JOB_TOKEN, githubSecret, githubSecretOrJobToken } from "@intentius/chant/ci-jobs";
26
+ import { AGENT_DECISION_FILE, AGENT_DIR, AGENT_DRIFT_FILE, AGENT_PATCH_FILE, AGENT_RC_FILE, agentCommandLines, agentGitAuthLines, agentHeadFetchLine, agentSettingsEnv, } from "@intentius/chant/ci-agent";
27
+ import { CHANT_EXIT } from "@intentius/chant/exit-codes";
28
+ /** The agent step's id, which the agent job's outputs come from. */
29
+ export const AGENT_STEP_ID = "chant-agent";
30
+ /** The drift Op's run step id, whose outputs carry the drift and the code. */
31
+ export const AGENT_DRIFT_STEP_ID = "chant-run";
32
+ /** The variable the push step holds its token in, for git's header and the forge client. */
33
+ const PUSH_TOKEN_ENV = "CHANT_AGENT_TOKEN";
34
+ /** The files the handover keeps when the agent directory is too large to hand on whole. */
35
+ const HANDED = { label: "chant-agent", noteFiles: [AGENT_DECISION_FILE, AGENT_RC_FILE, AGENT_PATCH_FILE, "prompt.sha256"] };
36
+ const DRIFT_HANDED = { label: "chant-agent", noteFiles: [AGENT_DRIFT_FILE] };
37
+ /** The name of the push job beside the agent job. */
38
+ export function agentPushJobName(jobName) {
39
+ return `${jobName}-push`;
40
+ }
41
+ /** The agent job's name for a drift Op's job. */
42
+ export function agentJobName(jobName, on) {
43
+ return on === "drift" ? `${jobName}-agent` : jobName;
44
+ }
45
+ /** The comment condition of the agent job: a pull request comment that starts with the agent's line, from a writer on GitHub. */
46
+ export function agentCommentIf(forge, comment) {
47
+ return [
48
+ "github.event_name == 'issue_comment'",
49
+ forge === "forgejo" ? "(github.event.issue.pull_request || github.event.is_pull)" : "github.event.issue.pull_request",
50
+ `startsWith(github.event.comment.body, '${comment.replace(/'/g, "''")} ')`,
51
+ ...(forge === "forgejo" ? [] : ["contains(fromJSON('[\"OWNER\",\"MEMBER\",\"COLLABORATOR\"]'), github.event.comment.author_association)"]),
52
+ ].join(" && ");
53
+ }
54
+ /**
55
+ * The drift Op's run step (#4616): the run with its output kept as the
56
+ * drift for the agent, `drift=true` among the step's outputs when it exits
57
+ * with chant's `drift` code, and the output handed on. The step still exits
58
+ * with the run's code.
59
+ */
60
+ export function driftRunStep(invocation, env, forge) {
61
+ return {
62
+ id: AGENT_DRIFT_STEP_ID,
63
+ env,
64
+ run: [
65
+ "set +e",
66
+ `mkdir -p ${AGENT_DIR}`,
67
+ ...handoverTrap(AGENT_DIR, forge, DRIFT_HANDED),
68
+ `{ ${invocation}; echo "$?" >"\${RUNNER_TEMP:-/tmp}/chant-run.code"; } 2>&1 | tee ${AGENT_DIR}/${AGENT_DRIFT_FILE}`,
69
+ 'code=$(cat "${RUNNER_TEMP:-/tmp}/chant-run.code")',
70
+ `if [ "$code" -eq ${CHANT_EXIT.drift} ]; then echo "drift=true" >>"$GITHUB_OUTPUT"; fi`,
71
+ 'exit "$code"',
72
+ ].join("\n"),
73
+ };
74
+ }
75
+ /** The drift Op job's outputs: the handed-on drift and the drift flag. */
76
+ export function driftRunOutputs(forge) {
77
+ return { ...handoverOutputs(AGENT_DRIFT_STEP_ID, forge), drift: `\${{ steps.${AGENT_DRIFT_STEP_ID}.outputs.drift }}` };
78
+ }
79
+ /** The agent job and the push job. */
80
+ export function githubAgentJobs(i) {
81
+ const s = i.settings;
82
+ const lines = agentCommandLines(i.forge, i.bin);
83
+ const drop = dropForgeTokens("sh");
84
+ const comment = s.on === "comment";
85
+ const settingsEnv = agentSettingsEnv(s);
86
+ const checkout = (head) => ({
87
+ name: head ? "Check out the target branch's commit and the pull request's head" : "Check out the commit the drift check ran on",
88
+ env: { [CHECKOUT_TOKEN_ENV]: GITHUB_JOB_TOKEN, ...(head ? { PR_NUMBER: "${{ github.event.issue.number }}" } : {}) },
89
+ // The ledger branch too: prepare counts the agent's runs on it, and land appends to it.
90
+ run: [checkoutScript(true, true), ...(head ? [agentHeadFetchLine(i.forge, "PR_NUMBER")] : [])].join("\n"),
91
+ });
92
+ const before = i.beforeScript.map((line) => ({ run: `${drop}\n${line}` }));
93
+ const timeout = s.timeout + 15;
94
+ const agent = {
95
+ ...(comment ? { if: agentCommentIf(i.forge, s.comment) } : { needs: i.after, if: `always() && needs.${i.after}.outputs.drift == 'true'` }),
96
+ "runs-on": githubRunsOn(i.runner),
97
+ container: i.image,
98
+ "timeout-minutes": timeout,
99
+ permissions: { contents: "read", "pull-requests": "read" },
100
+ env: settingsEnv,
101
+ outputs: handoverOutputs(AGENT_STEP_ID, i.forge),
102
+ steps: [
103
+ checkout(comment),
104
+ ...(comment ? [] : [{ name: "Take the drift from the drift check", run: takeOverScript(i.after, AGENT_DIR, i.forge) }]),
105
+ ...i.setup,
106
+ ...before,
107
+ // Reads only: the comment, the commenter's access, the pull request's head and the ledger.
108
+ { name: "Decide whether the agent runs", env: { GITHUB_TOKEN: GITHUB_JOB_TOKEN }, run: lines.prepare },
109
+ // The job's last step: the model's key and no forge token, and the agent directory handed on however it ends.
110
+ {
111
+ id: AGENT_STEP_ID,
112
+ name: "Run the agent",
113
+ env: { [s.keyEnv]: githubSecret(s.keyEnv) },
114
+ run: [drop, ...handoverTrap(AGENT_DIR, i.forge, HANDED), lines.run].join("\n"),
115
+ },
116
+ ],
117
+ };
118
+ const token = i.tokenEnv ? githubSecretOrJobToken(i.tokenEnv) : GITHUB_JOB_TOKEN;
119
+ const push = {
120
+ needs: i.jobName,
121
+ if: `always() && needs.${i.jobName}.result == 'success'`,
122
+ "runs-on": githubRunsOn(i.runner),
123
+ container: i.image,
124
+ permissions: { contents: "write", "pull-requests": "write", issues: "write" },
125
+ env: settingsEnv,
126
+ steps: [
127
+ checkout(comment),
128
+ { name: "Take the change from the agent job", run: takeOverScript(i.jobName, AGENT_DIR, i.forge) },
129
+ ...i.setup,
130
+ ...before,
131
+ {
132
+ name: comment ? "Push the change to the pull request" : "Open or update the agent's pull request",
133
+ env: { [PUSH_TOKEN_ENV]: token, GITHUB_TOKEN: token, ...(i.forge === "forgejo" ? { CHANT_FORGEJO_TOKEN: token } : {}) },
134
+ run: [...agentGitAuthLines(PUSH_TOKEN_ENV, "x-access-token"), lines.land].join("\n"),
135
+ },
136
+ ],
137
+ };
138
+ return { agent, push };
139
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The control repo's own workflow (#4568) on GitHub, and on Forgejo through
3
+ * its dialect: a member's dispatch starts it, and it runs the member's
4
+ * dependents with `chant workspace run --dependents-of`. The pieces are
5
+ * core's (`controlPipelineSpec` in `@intentius/chant/control/dispatch`);
6
+ * this file writes them as a workflow. The member's side, the
7
+ * `control-dispatch` job, comes from the pull request loop's `control`
8
+ * option (./generate-pipeline.ts).
9
+ */
10
+ import type { CiProductWords, CiProductWordsInput } from "@intentius/chant/ci-product-words";
11
+ import { type GithubWorkflowDoc } from "./ci-jobs.js";
12
+ /** A control repo's pipeline file: where it goes, its text, and what a person still sets up. */
13
+ export interface ControlPipelineFile {
14
+ path: string;
15
+ content: string;
16
+ notes: string[];
17
+ }
18
+ /** What {@link controlGithubDoc} and the generators take. */
19
+ export interface ControlPipelineOptions {
20
+ words?: CiProductWordsInput | CiProductWords;
21
+ /** The image the job runs in. Default chant's CI image. */
22
+ image?: string;
23
+ /** The command the job runs chant with. Default `npx --no-install chant`. */
24
+ command?: string;
25
+ }
26
+ /** The control repo's workflow document for a GitHub-shaped forge, before emission. Forgejo transforms it first. */
27
+ export declare function controlGithubDoc(forge: "github" | "forgejo", options?: ControlPipelineOptions): {
28
+ doc: GithubWorkflowDoc;
29
+ path: string;
30
+ notes: string[];
31
+ };
32
+ /** The control repo's workflow on GitHub: `.github/workflows/<product>-dependents.yml`. */
33
+ export declare function generateControlPipeline(options?: ControlPipelineOptions): ControlPipelineFile;
34
+ //# sourceMappingURL=control-pipeline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"control-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/control-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAC7F,OAAO,EAA2C,KAAK,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAE5F,gGAAgG;AAChG,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,6DAA6D;AAC7D,MAAM,WAAW,sBAAsB;IACrC,KAAK,CAAC,EAAE,mBAAmB,GAAG,cAAc,CAAC;IAC7C,2DAA2D;IAC3D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,oHAAoH;AACpH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,QAAQ,GAAG,SAAS,EAAE,OAAO,GAAE,sBAA2B,GAAG;IAAE,GAAG,EAAE,iBAAiB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,CA0B7J;AAED,2FAA2F;AAC3F,wBAAgB,uBAAuB,CAAC,OAAO,GAAE,sBAA2B,GAAG,mBAAmB,CAGjG"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The control repo's own workflow (#4568) on GitHub, and on Forgejo through
3
+ * its dialect: a member's dispatch starts it, and it runs the member's
4
+ * dependents with `chant workspace run --dependents-of`. The pieces are
5
+ * core's (`controlPipelineSpec` in `@intentius/chant/control/dispatch`);
6
+ * this file writes them as a workflow. The member's side, the
7
+ * `control-dispatch` job, comes from the pull request loop's `control`
8
+ * option (./generate-pipeline.ts).
9
+ */
10
+ import { CI_IMAGE, GITHUB_RUNNER } from "@intentius/chant/ci-jobs";
11
+ import { controlPipelineSpec } from "@intentius/chant/control/dispatch";
12
+ import { CHECKOUT, concurrency, emitWorkflowYAML } from "./ci-jobs.js";
13
+ /** The control repo's workflow document for a GitHub-shaped forge, before emission. Forgejo transforms it first. */
14
+ export function controlGithubDoc(forge, options = {}) {
15
+ const spec = controlPipelineSpec(forge, options.words ?? {}, options.command ? { command: options.command } : {});
16
+ const doc = {
17
+ header: spec.header,
18
+ name: spec.path.split("/").pop().replace(/\.yml$/, ""),
19
+ on: {
20
+ ...(forge === "github" ? { repository_dispatch: { types: [spec.event] } } : {}),
21
+ workflow_dispatch: { inputs: spec.inputs },
22
+ },
23
+ // #4617: contents: write pushes the record of each dispatch that carries an outcome to chant/lifecycle.
24
+ permissions: { contents: "write" },
25
+ // One run per member at a time, and none cancelled: a later apply of the same member waits for the one running.
26
+ concurrency: concurrency(spec.group),
27
+ jobsDoc: {
28
+ dependents: {
29
+ "runs-on": GITHUB_RUNNER,
30
+ container: options.image ?? CI_IMAGE,
31
+ steps: [
32
+ // The full history, so the job reads and appends to chant/lifecycle (#4617).
33
+ { uses: CHECKOUT, with: { "fetch-depth": 0 } },
34
+ { name: "Run the dependents of the member that applied", shell: "bash", env: spec.env, run: spec.script },
35
+ ],
36
+ },
37
+ },
38
+ };
39
+ return { doc, path: spec.path, notes: spec.notes };
40
+ }
41
+ /** The control repo's workflow on GitHub: `.github/workflows/<product>-dependents.yml`. */
42
+ export function generateControlPipeline(options = {}) {
43
+ const { doc, path, notes } = controlGithubDoc("github", options);
44
+ return { path, content: emitWorkflowYAML(doc), notes };
45
+ }
@@ -131,6 +131,13 @@ export interface GithubOpPipelineDoc {
131
131
  * dialect transforms this job on its own terms.
132
132
  */
133
133
  reviewNoteDoc?: Record<string, unknown>;
134
+ /**
135
+ * The agent jobs (#4616), when the spec sets `agent`: the agent job and
136
+ * the push job. Kept out of {@link jobsDoc} so a member's
137
+ * `working-directory` default stays on the Op's own job: these check out
138
+ * at the repository root.
139
+ */
140
+ agentDoc?: Record<string, unknown>;
134
141
  }
135
142
  /** One generated file: a suggested name plus its pipeline document, pre-emission. */
136
143
  export interface GithubOpPipelineFile {
@@ -150,7 +157,7 @@ export interface GithubOpPipelineFile {
150
157
  * assertSetupSteps}), or a `permissions` entry that is not additive ({@link
151
158
  * mergePermissions}).
152
159
  */
153
- export declare function buildGithubOpPipelineDocs(ops: ScheduledOpSpec[], options?: GenerateGithubOpOptions): {
160
+ export declare function buildGithubOpPipelineDocs(ops: ScheduledOpSpec[], options?: GenerateGithubOpOptions, forge?: "github" | "forgejo"): {
154
161
  files: GithubOpPipelineFile[];
155
162
  jobs: OpPipelineJob[];
156
163
  };
@@ -1 +1 @@
1
- {"version":3,"file":"generate-op-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-op-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsEG;AAGH,OAAO,EACL,iBAAiB,EACjB,gBAAgB,EAOhB,gBAAgB,EAIjB,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EACV,wBAAwB,IAAI,uBAAuB,EAEnD,aAAa,EACb,gBAAgB,IAAI,sBAAsB,EAG1C,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAIlC,YAAY,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,CAAC;AAIhE,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,CAAC;AAEjE;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC5B,2DAA2D;IAC3D,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,6DAA6D;IAC7D,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,8DAA8D;IAC9D,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACzC;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACzC;AAED,qFAAqF;AACrF,MAAM,WAAW,oBAAoB;IACnC,4FAA4F;IAC5F,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,mBAAmB,CAAC;CAC1B;AAuOD;;;;;;;;;;;GAWG;AACH,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,eAAe,EAAE,EACtB,OAAO,GAAE,uBAA4B,GACpC;IAAE,KAAK,EAAE,oBAAoB,EAAE,CAAC;IAAC,IAAI,EAAE,aAAa,EAAE,CAAA;CAAE,CAoH1D;AAwCD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,mBAAmB,GAAG,MAAM,CAMnE;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,eAAe,EAAE,EACtB,OAAO,GAAE,uBAA4B,GACpC,sBAAsB,CAMxB"}
1
+ {"version":3,"file":"generate-op-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-op-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsEG;AAGH,OAAO,EACL,iBAAiB,EACjB,gBAAgB,EAOhB,gBAAgB,EAIjB,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EACV,wBAAwB,IAAI,uBAAuB,EAEnD,aAAa,EACb,gBAAgB,IAAI,sBAAsB,EAG1C,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAUlC,YAAY,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,CAAC;AAIhE,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,CAAC;AAEjE;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC5B,2DAA2D;IAC3D,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,6DAA6D;IAC7D,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,8DAA8D;IAC9D,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACzC;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,qFAAqF;AACrF,MAAM,WAAW,oBAAoB;IACnC,4FAA4F;IAC5F,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,mBAAmB,CAAC;CAC1B;AAuOD;;;;;;;;;;;GAWG;AACH,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,eAAe,EAAE,EACtB,OAAO,GAAE,uBAA4B,EACrC,KAAK,GAAE,QAAQ,GAAG,SAAoB,GACrC;IAAE,KAAK,EAAE,oBAAoB,EAAE,CAAC;IAAC,IAAI,EAAE,aAAa,EAAE,CAAA;CAAE,CAuM1D;AAwCD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,mBAAmB,GAAG,MAAM,CAMnE;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,eAAe,EAAE,EACtB,OAAO,GAAE,uBAA4B,GACpC,sBAAsB,CAMxB"}
@@ -70,10 +70,16 @@
70
70
  * that job either, same as `environment:` above does not).
71
71
  */
72
72
  import { readFileSync } from "node:fs";
73
- import { assertEnvironment, assertSetupSteps, CI_IMAGE, ciJobName, environmentDoc, fillCommand, GITHUB_JOB_TOKEN, GITHUB_RUNNER, mergePermissions, nonEmpty, permissionsFor, setupStepDoc, } from "@intentius/chant/ci-jobs";
73
+ import { assertEnvironment, assertSetupSteps, CI_IMAGE, ciJobName, environmentDoc, fillCommand, GITHUB_JOB_TOKEN, githubRunsOn, mergePermissions, nonEmpty, permissionsFor, setupStepDoc, } from "@intentius/chant/ci-jobs";
74
74
  import { memberPathFilter, resolveOpTrigger } from "@intentius/chant/lexicon";
75
75
  import { checkoutStep, concurrency, emitWorkflowYAML, jobTokenEnv, onTrigger, opConcurrencyGroup, runSteps } from "./ci-jobs.js";
76
76
  import { assertReviewSplit, reviewJobs, reviewNoteJobName } from "./review-jobs.js";
77
+ import { resolveAgentSpec } from "@intentius/chant/ci-agent";
78
+ import { activeProductWords } from "@intentius/chant/ci-product-words";
79
+ import { agentJobName, agentPushJobName, driftRunOutputs, driftRunStep, githubAgentJobs } from "./agent-jobs.js";
80
+ import { assertScheduledOpCloud, scheduledOpUpdateJobName, withoutTokenRequestLine } from "@intentius/chant/scheduled-op-cloud";
81
+ import { TRADE_SPEC_ENV, tradeLine } from "@intentius/chant/ci-identity";
82
+ import { CHANT_EXIT } from "@intentius/chant/exit-codes";
77
83
  // The setup, environment and permission checks are shared with the Op waves
78
84
  // generator and the forgejo lexicon (#3765); they stay importable from here.
79
85
  export { assertEnvironment, assertSetupSteps, mergePermissions };
@@ -253,12 +259,12 @@ function gateNoticeScript(version) {
253
259
  * no branch and merges nothing. The Forgejo dialect drops them, as it drops
254
260
  * every scope but `id-token`.
255
261
  */
256
- function gateNoticeJob(applyJobName, image) {
262
+ function gateNoticeJob(applyJobName, image, runner) {
257
263
  const output = (name) => "${{ needs." + applyJobName + ".outputs." + name + ' }}';
258
264
  return {
259
265
  needs: applyJobName,
260
266
  if: `needs.${applyJobName}.outputs.gated == 'true'`,
261
- "runs-on": GITHUB_RUNNER,
267
+ "runs-on": githubRunsOn(runner),
262
268
  container: image,
263
269
  permissions: { contents: "read", issues: "write", "pull-requests": "write" },
264
270
  steps: [
@@ -301,14 +307,50 @@ function assertTriggerSupportsMode(name, mode, trigger) {
301
307
  * assertSetupSteps}), or a `permissions` entry that is not additive ({@link
302
308
  * mergePermissions}).
303
309
  */
304
- export function buildGithubOpPipelineDocs(ops, options = {}) {
310
+ export function buildGithubOpPipelineDocs(ops, options = {}, forge = "github") {
305
311
  const image = options.image ?? CI_IMAGE;
306
312
  const runCommand = options.runCommand ?? ["chant", "run", "{name}"];
307
313
  const beforeScript = options.beforeScript ?? [];
308
314
  const extraScript = options.extraScript ?? [];
309
315
  const files = [];
310
316
  const jobs = [];
317
+ const words = activeProductWords();
311
318
  for (const spec of ops) {
319
+ // #4631: the job's cloud identities and the update on drift, checked before anything renders.
320
+ assertScheduledOpCloud(spec);
321
+ // #4616: the comment agent is a workflow of its own, on pull request comments; the spec's trigger is not read.
322
+ if (spec.agent?.on === "comment") {
323
+ const settings = resolveAgentSpec(spec.name, spec.agent, words.product);
324
+ const jobName = ciJobName(spec.name);
325
+ const setup = spec.setup ?? [];
326
+ assertSetupSteps(spec.name, setup);
327
+ jobs.push({ jobName, op: spec.name, trigger: { kind: "pull_request" }, findingMode: "report" });
328
+ const { agent, push } = githubAgentJobs({
329
+ forge,
330
+ jobName,
331
+ image,
332
+ settings,
333
+ ...(spec.runner !== undefined ? { runner: spec.runner } : {}),
334
+ setup: setup.map(setupStepDoc),
335
+ beforeScript,
336
+ ...(spec.agent.tokenEnv ? { tokenEnv: spec.agent.tokenEnv } : {}),
337
+ ...(words.bin ? { bin: words.bin } : {}),
338
+ });
339
+ files.push({
340
+ name: `${spec.name}.yml`,
341
+ doc: {
342
+ name: spec.name,
343
+ on: { issue_comment: { types: ["created"] } },
344
+ ...nonEmpty("env", options.variables),
345
+ // One agent run at a time, so two asks never count the limit at once.
346
+ concurrency: concurrency(jobName),
347
+ permissions: { contents: "read" },
348
+ jobsDoc: {},
349
+ agentDoc: { [jobName]: agent, [agentPushJobName(jobName)]: push },
350
+ },
351
+ });
352
+ continue;
353
+ }
312
354
  const findingMode = spec.findingMode ?? "report";
313
355
  const trigger = resolveOpTrigger(spec);
314
356
  assertTriggerSupportsMode(spec.name, findingMode, trigger);
@@ -321,7 +363,14 @@ export function buildGithubOpPipelineDocs(ops, options = {}) {
321
363
  const split = spec.reviewAgent === true;
322
364
  if (split)
323
365
  assertReviewSplit(spec.name, trigger, findingMode, spec.permissions);
366
+ // #4616: a drift agent follows the Op's job, which hands its output on and says whether it found drift.
367
+ const driftAgent = spec.agent?.on === "drift" ? resolveAgentSpec(spec.name, spec.agent, words.product) : undefined;
368
+ if (driftAgent && (split || trigger.kind === "push")) {
369
+ throw new Error(`Scheduled Op "${spec.name}" sets a drift agent, which follows a drift check's run; give it a cron or pull_request trigger and no reviewAgent.`);
370
+ }
324
371
  const stepEnv = split ? {} : jobTokenEnv(findingMode !== "report");
372
+ const cloud = spec.cloud;
373
+ const update = spec.update;
325
374
  const setup = spec.setup ?? [];
326
375
  assertSetupSteps(spec.name, setup);
327
376
  if (spec.environment)
@@ -334,11 +383,17 @@ export function buildGithubOpPipelineDocs(ops, options = {}) {
334
383
  const invocation = gated
335
384
  ? [...runParts, ...GATED_EXIT_FLAG, "--json"].join(" ")
336
385
  : runParts.join(" ");
337
- const steps = [checkoutStep(), ...setup.map(setupStepDoc), ...runSteps(beforeScript)];
338
- steps.push(gated
386
+ const later = [...setup.map(setupStepDoc), ...runSteps(beforeScript)];
387
+ later.push(gated
339
388
  ? gatedRunStep(RUN_STEP_ID, spec.name, invocation, stepEnv)
340
- : { run: invocation, env: stepEnv });
341
- steps.push(...runSteps(extraScript));
389
+ : driftAgent
390
+ ? driftRunStep(invocation, stepEnv, forge)
391
+ : update
392
+ ? driftFlagStep(invocation, stepEnv)
393
+ : { run: invocation, env: stepEnv });
394
+ later.push(...runSteps(extraScript));
395
+ // #4631: the trade before the checkout, and every step after it without the token request variables.
396
+ const steps = cloud ? [tradeStep(cloud), checkoutStep(), ...later.map(withoutTokenRequest)] : [checkoutStep(), ...later];
342
397
  const review = split
343
398
  ? reviewJobs({
344
399
  name: spec.name,
@@ -352,6 +407,7 @@ export function buildGithubOpPipelineDocs(ops, options = {}) {
352
407
  ...(options.member ? { member: options.member } : {}),
353
408
  })
354
409
  : undefined;
410
+ const workflowPermissions = mergePermissions(spec.name, permissionsFor(findingMode, trigger), spec.permissions ?? {}, trigger);
355
411
  const doc = {
356
412
  name: spec.name,
357
413
  on: onTrigger(trigger),
@@ -359,10 +415,10 @@ export function buildGithubOpPipelineDocs(ops, options = {}) {
359
415
  // One run at a time per Op — a slow audit must not overlap its own next
360
416
  // scheduled trigger. A push Op's `build` (#4169) widens the group.
361
417
  concurrency: concurrency(opConcurrencyGroup(jobName, trigger)),
362
- permissions: mergePermissions(spec.name, permissionsFor(findingMode, trigger), spec.permissions ?? {}, trigger),
418
+ permissions: workflowPermissions,
363
419
  jobsDoc: {
364
420
  [jobName]: {
365
- "runs-on": GITHUB_RUNNER,
421
+ "runs-on": githubRunsOn(spec.runner),
366
422
  container: image,
367
423
  // Per-Op credentials (#2290): job-level `env:`, one level more
368
424
  // specific than `options.variables`'s workflow-level `env:` above,
@@ -374,6 +430,8 @@ export function buildGithubOpPipelineDocs(ops, options = {}) {
374
430
  // behind the same reviewer would hold the message back until
375
431
  // somebody had already acted.
376
432
  ...(spec.environment ? { environment: environmentDoc(spec.environment) } : {}),
433
+ // #4631: the trade needs id-token: write, on this job alone; the finding-mode's scopes stay as they are.
434
+ ...(cloud ? { permissions: { ...workflowPermissions, "id-token": "write" } } : {}),
377
435
  ...(gated
378
436
  ? {
379
437
  outputs: Object.fromEntries(["gated", "op", "gate", "approve"].map((name) => [
@@ -381,16 +439,47 @@ export function buildGithubOpPipelineDocs(ops, options = {}) {
381
439
  `\${{ steps.${RUN_STEP_ID}.outputs.${name} }}`,
382
440
  ])),
383
441
  }
384
- : {}),
442
+ : driftAgent
443
+ ? { outputs: driftRunOutputs(forge) }
444
+ : update
445
+ ? { outputs: { drift: `\${{ steps.${DRIFT_FLAG_STEP_ID}.outputs.drift }}` } }
446
+ : {}),
385
447
  steps,
386
448
  },
449
+ ...(update
450
+ ? {
451
+ [scheduledOpUpdateJobName(jobName)]: updateJob(jobName, update, {
452
+ image,
453
+ ...(spec.runner !== undefined ? { runner: spec.runner } : {}),
454
+ setup: setup.map(setupStepDoc),
455
+ beforeScript,
456
+ }),
457
+ }
458
+ : {}),
387
459
  },
388
- ...(gated ? { gatedNoticeDoc: { [`${jobName}-gate-notice`]: gateNoticeJob(jobName, image) } } : {}),
460
+ ...(gated ? { gatedNoticeDoc: { [`${jobName}-gate-notice`]: gateNoticeJob(jobName, image, spec.runner) } } : {}),
389
461
  };
462
+ if (driftAgent) {
463
+ const name = agentJobName(jobName, "drift");
464
+ const { agent, push } = githubAgentJobs({
465
+ forge,
466
+ jobName: name,
467
+ image,
468
+ settings: driftAgent,
469
+ ...(spec.runner !== undefined ? { runner: spec.runner } : {}),
470
+ setup: setup.map(setupStepDoc),
471
+ beforeScript,
472
+ ...(spec.agent?.tokenEnv ? { tokenEnv: spec.agent.tokenEnv } : {}),
473
+ ...(words.bin ? { bin: words.bin } : {}),
474
+ after: jobName,
475
+ });
476
+ doc.agentDoc = { [name]: agent, [agentPushJobName(name)]: push };
477
+ }
390
478
  // A split review (#4326): the Op's job is the review job, and the note job rides beside it.
391
479
  if (review) {
392
- doc.jobsDoc = { [jobName]: { ...review.review, ...(spec.environment ? { environment: environmentDoc(spec.environment) } : {}) } };
393
- doc.reviewNoteDoc = { [reviewNoteJobName(jobName)]: review.note };
480
+ const runsOn = spec.runner !== undefined ? { "runs-on": githubRunsOn(spec.runner) } : {};
481
+ doc.jobsDoc = { [jobName]: { ...review.review, ...runsOn, ...(spec.environment ? { environment: environmentDoc(spec.environment) } : {}) } };
482
+ doc.reviewNoteDoc = { [reviewNoteJobName(jobName)]: { ...review.note, ...runsOn } };
394
483
  }
395
484
  files.push({ name: `${spec.name}.yml`, doc });
396
485
  }
@@ -445,8 +534,8 @@ export function emitOpPipelineYAML(doc) {
445
534
  // The gated-apply notice job rides in `jobs:` beside the Op's own job; it
446
535
  // is carried separately on the doc so a dialect transforms it on its own.
447
536
  // A dialect's `header` note (#2257) is absent on github.
448
- const { gatedNoticeDoc, reviewNoteDoc, ...workflow } = doc;
449
- return emitWorkflowYAML({ ...workflow, jobsDoc: { ...doc.jobsDoc, ...(gatedNoticeDoc ?? {}), ...(reviewNoteDoc ?? {}) } });
537
+ const { gatedNoticeDoc, reviewNoteDoc, agentDoc, ...workflow } = doc;
538
+ return emitWorkflowYAML({ ...workflow, jobsDoc: { ...doc.jobsDoc, ...(gatedNoticeDoc ?? {}), ...(reviewNoteDoc ?? {}), ...(agentDoc ?? {}) } });
450
539
  }
451
540
  /**
452
541
  * Synthesize one `.github/workflows/*.yml` per scheduled Op. Thin wrapper
@@ -461,3 +550,42 @@ export function generateGithubOpPipeline(ops, options = {}) {
461
550
  jobs,
462
551
  };
463
552
  }
553
+ /** The id of the run step that says whether an update's drift check found drift (#4631). */
554
+ const DRIFT_FLAG_STEP_ID = "drift-check";
555
+ /** The Op's run step when an update follows it (#4631): `drift=true` as an output on chant's drift code, and the run's own exit code. */
556
+ function driftFlagStep(invocation, env) {
557
+ return {
558
+ id: DRIFT_FLAG_STEP_ID,
559
+ env,
560
+ run: ["set +e", invocation, "code=$?", `if [ "$code" -eq ${CHANT_EXIT.drift} ]; then echo "drift=true" >>"$GITHUB_OUTPUT"; fi`, 'exit "$code"'].join("\n"),
561
+ };
562
+ }
563
+ /** The trade step (#4631), as an op wave job's (#4332): the job's OIDC token for `cloud`, written to GITHUB_ENV. */
564
+ function tradeStep(cloud) {
565
+ return { name: "Trade the job's OIDC token for its cloud identities", env: { [TRADE_SPEC_ENV]: JSON.stringify(cloud) }, run: tradeLine("$GITHUB_ENV", "github-env") };
566
+ }
567
+ /** A run step after the trade, with the token request variables unset first. A `uses:` step is left as it is. */
568
+ function withoutTokenRequest(step) {
569
+ return typeof step.run === "string" ? { ...step, run: withoutTokenRequestLine(step.run) } : step;
570
+ }
571
+ /**
572
+ * The update job (#4631): after the Op's job, only when its run found
573
+ * drift; its own trade for `update.cloud` before its checkout, then the
574
+ * update's command. Its permissions are `contents: read`, `id-token: write`
575
+ * with a cloud, and the update's own scopes, which may raise `contents` to
576
+ * `write` so the update records on `chant/lifecycle` (2026-10-28 ruling).
577
+ * The Op's own job keeps its scopes.
578
+ */
579
+ function updateJob(after, update, i) {
580
+ const later = [...i.setup, ...runSteps(i.beforeScript), { name: "Update what drifted", run: update.command }];
581
+ return {
582
+ needs: after,
583
+ if: `always() && needs.${after}.outputs.drift == 'true'`,
584
+ "runs-on": githubRunsOn(i.runner),
585
+ container: i.image,
586
+ permissions: { contents: "read", ...(update.cloud ? { "id-token": "write" } : {}), ...(update.permissions ?? {}) },
587
+ ...nonEmpty("env", update.variables),
588
+ ...(update.environment ? { environment: environmentDoc(update.environment) } : {}),
589
+ steps: update.cloud ? [tradeStep(update.cloud), checkoutStep(), ...later.map(withoutTokenRequest)] : [checkoutStep(), ...later],
590
+ };
591
+ }
@@ -22,6 +22,9 @@
22
22
  * downloads it. The forgejo generator reuses {@link buildGithubOpWavesDoc}
23
23
  * and applies its dialect.
24
24
  *
25
+ * `options.bin` (#4607) is a product's command in place of `chant` in every
26
+ * job line: the wave jobs, the resume job and the plans job.
27
+ *
25
28
  * With `resume` on the spec (#3683), a second workflow, `<name>-resume.yml`,
26
29
  * runs `chant run resume --op <name>` on that schedule: it re-runs the failed
27
30
  * jobs of a run whose waiting wave has been approved since, with the job's
@@ -31,6 +34,8 @@
31
34
  * `pull-requests: read`, so a wave can read the merged pull request's
32
35
  * reviews, and a second workflow, `<name>-plans.yml`, runs `chant run wave
33
36
  * --record-plans` on each pull request to record the digests its head plans.
37
+ * A `pull-request` wave (#4630) gets the plans workflow too, for the approval
38
+ * status before merge; it asks for no `pull-requests: read` on the waves.
34
39
  *
35
40
  * With `notify` on the spec (#3761), the workflow maps each variable it names
36
41
  * from the repository secret of the same name, so a wave job can post to
@@ -49,6 +54,25 @@
49
54
  * checkout. Every later run step starts with an unset of the variables a
50
55
  * token is requested with, so the code the job runs holds no OIDC token
51
56
  * and cannot request one for another group's identity.
57
+ *
58
+ * With `control` on the spec (#4617), a `control-dispatch` job needs the
59
+ * last wave's jobs and runs after them unless the run was cancelled, so it
60
+ * runs after a failure too. It downloads every job's outcome file, adds
61
+ * them up with `chant run wave --waves-outcome` (through the product's
62
+ * bin) into `CHANT_CONTROL_OUTCOME`, and sends the control repo one
63
+ * dispatch carrying that outcome and the pushed commit. Only the dispatch
64
+ * step holds the control token: the `tokenSecret` secret, mapped into
65
+ * `CHANT_CONTROL_TOKEN` and handed to curl on its stdin
66
+ * (`controlDispatchScript`, #4568).
67
+ *
68
+ * Every wave, deciding and share job starts with the apply-job guard
69
+ * (#4624, `@intentius/chant/ci-job-guard`), before the trade and the
70
+ * checkout: it stops the job unless `GITHUB_REF` is one of the spec's
71
+ * branches, the job runs in its forge environment (read from an OIDC
72
+ * token's claims, so only in a job that may request one), and, for a
73
+ * runner whose labels include `self-hosted`, on a self-hosted runner. The
74
+ * forgejo generator passes `forge: "forgejo"`, which checks the ref and
75
+ * runner only.
52
76
  */
53
77
  import { type OpWavesSpec } from "@intentius/chant/op/op-waves";
54
78
  import type { OpWavesPipelineOptions, OpWavesPipelineResult } from "@intentius/chant/lexicon";
@@ -60,7 +84,9 @@ export declare function opWaveRecordArtifact(spec: Pick<OpWavesSpec, "name">, wa
60
84
  /** The artifact a job keeps its outcome file in (#4322). */
61
85
  export declare function opWaveOutcomeArtifact(spec: Pick<OpWavesSpec, "name">, jobName: string): string;
62
86
  /** The workflow document, before emission, and the jobs it holds. */
63
- export declare function buildGithubOpWavesDoc(spec: OpWavesSpec, options: OpWavesPipelineOptions): {
87
+ export declare function buildGithubOpWavesDoc(spec: OpWavesSpec, options: OpWavesPipelineOptions, dialect?: {
88
+ forge?: "github" | "forgejo";
89
+ }): {
64
90
  doc: GithubOpPipelineDoc;
65
91
  jobs: OpWavesPipelineResult["jobs"];
66
92
  resumeDoc?: GithubOpPipelineDoc;