@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.
- package/dist/components/agent-jobs.d.ts +67 -0
- package/dist/components/agent-jobs.d.ts.map +1 -0
- package/dist/components/agent-jobs.js +139 -0
- package/dist/components/control-pipeline.d.ts +34 -0
- package/dist/components/control-pipeline.d.ts.map +1 -0
- package/dist/components/control-pipeline.js +45 -0
- package/dist/components/generate-op-pipeline.d.ts +8 -1
- package/dist/components/generate-op-pipeline.d.ts.map +1 -1
- package/dist/components/generate-op-pipeline.js +144 -16
- package/dist/components/generate-op-waves-pipeline.d.ts +27 -1
- package/dist/components/generate-op-waves-pipeline.d.ts.map +1 -1
- package/dist/components/generate-op-waves-pipeline.js +135 -16
- package/dist/components/generate-pipeline.d.ts.map +1 -1
- package/dist/components/generate-pipeline.js +160 -15
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/package.json +2 -2
- package/src/components/__golden__/op-waves-jobs.yml +20 -0
- package/src/components/agent-jobs.ts +177 -0
- package/src/components/control-pipeline.ts +65 -0
- package/src/components/generate-op-pipeline.agent.test.ts +147 -0
- package/src/components/generate-op-pipeline.cloud.test.ts +96 -0
- package/src/components/generate-op-pipeline.notice.test.ts +2 -1
- package/src/components/generate-op-pipeline.ts +165 -20
- package/src/components/generate-op-waves-pipeline.approval.test.ts +46 -0
- package/src/components/generate-op-waves-pipeline.control.test.ts +64 -0
- package/src/components/generate-op-waves-pipeline.guard.test.ts +57 -0
- package/src/components/generate-op-waves-pipeline.identity.test.ts +8 -8
- package/src/components/generate-op-waves-pipeline.test.ts +17 -0
- package/src/components/generate-op-waves-pipeline.ts +157 -15
- package/src/components/generate-pipeline.ts +170 -15
- package/src/components/generate-pr-pipeline.comment-locks.test.ts +53 -0
- package/src/components/generate-pr-pipeline.comment-preview.test.ts +64 -0
- package/src/components/generate-pr-pipeline.comment-targets.test.ts +24 -0
- package/src/components/generate-pr-pipeline.control.test.ts +94 -0
- package/src/components/generate-pr-pipeline.pulumi-identity.test.ts +43 -0
- package/src/components/generate-pr-pipeline.respond.test.ts +47 -0
- package/src/components/generate-pr-pipeline.runners.test.ts +61 -0
- 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;
|
|
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,
|
|
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":
|
|
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
|
|
338
|
-
|
|
386
|
+
const later = [...setup.map(setupStepDoc), ...runSteps(beforeScript)];
|
|
387
|
+
later.push(gated
|
|
339
388
|
? gatedRunStep(RUN_STEP_ID, spec.name, invocation, stepEnv)
|
|
340
|
-
:
|
|
341
|
-
|
|
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:
|
|
418
|
+
permissions: workflowPermissions,
|
|
363
419
|
jobsDoc: {
|
|
364
420
|
[jobName]: {
|
|
365
|
-
"runs-on":
|
|
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
|
-
|
|
393
|
-
doc.
|
|
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;
|