@intentius/chant-lexicon-github 0.124.54 → 0.124.55
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 +75 -12
- package/dist/components/generate-op-waves-pipeline.d.ts +13 -0
- package/dist/components/generate-op-waves-pipeline.d.ts.map +1 -1
- package/dist/components/generate-op-waves-pipeline.js +62 -4
- package/dist/components/generate-pipeline.d.ts.map +1 -1
- package/dist/components/generate-pipeline.js +133 -14
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/package.json +2 -2
- 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.notice.test.ts +2 -1
- package/src/components/generate-op-pipeline.ts +82 -11
- package/src/components/generate-op-waves-pipeline.control.test.ts +64 -0
- package/src/components/generate-op-waves-pipeline.test.ts +17 -0
- package/src/components/generate-op-waves-pipeline.ts +81 -4
- package/src/components/generate-pipeline.ts +139 -14
- 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.control.test.ts +94 -0
- package/src/components/generate-pr-pipeline.pulumi-identity.test.ts +43 -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;AAOlC,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,CAgL1D;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,13 @@
|
|
|
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";
|
|
77
80
|
// The setup, environment and permission checks are shared with the Op waves
|
|
78
81
|
// generator and the forgejo lexicon (#3765); they stay importable from here.
|
|
79
82
|
export { assertEnvironment, assertSetupSteps, mergePermissions };
|
|
@@ -253,12 +256,12 @@ function gateNoticeScript(version) {
|
|
|
253
256
|
* no branch and merges nothing. The Forgejo dialect drops them, as it drops
|
|
254
257
|
* every scope but `id-token`.
|
|
255
258
|
*/
|
|
256
|
-
function gateNoticeJob(applyJobName, image) {
|
|
259
|
+
function gateNoticeJob(applyJobName, image, runner) {
|
|
257
260
|
const output = (name) => "${{ needs." + applyJobName + ".outputs." + name + ' }}';
|
|
258
261
|
return {
|
|
259
262
|
needs: applyJobName,
|
|
260
263
|
if: `needs.${applyJobName}.outputs.gated == 'true'`,
|
|
261
|
-
"runs-on":
|
|
264
|
+
"runs-on": githubRunsOn(runner),
|
|
262
265
|
container: image,
|
|
263
266
|
permissions: { contents: "read", issues: "write", "pull-requests": "write" },
|
|
264
267
|
steps: [
|
|
@@ -301,14 +304,48 @@ function assertTriggerSupportsMode(name, mode, trigger) {
|
|
|
301
304
|
* assertSetupSteps}), or a `permissions` entry that is not additive ({@link
|
|
302
305
|
* mergePermissions}).
|
|
303
306
|
*/
|
|
304
|
-
export function buildGithubOpPipelineDocs(ops, options = {}) {
|
|
307
|
+
export function buildGithubOpPipelineDocs(ops, options = {}, forge = "github") {
|
|
305
308
|
const image = options.image ?? CI_IMAGE;
|
|
306
309
|
const runCommand = options.runCommand ?? ["chant", "run", "{name}"];
|
|
307
310
|
const beforeScript = options.beforeScript ?? [];
|
|
308
311
|
const extraScript = options.extraScript ?? [];
|
|
309
312
|
const files = [];
|
|
310
313
|
const jobs = [];
|
|
314
|
+
const words = activeProductWords();
|
|
311
315
|
for (const spec of ops) {
|
|
316
|
+
// #4616: the comment agent is a workflow of its own, on pull request comments; the spec's trigger is not read.
|
|
317
|
+
if (spec.agent?.on === "comment") {
|
|
318
|
+
const settings = resolveAgentSpec(spec.name, spec.agent, words.product);
|
|
319
|
+
const jobName = ciJobName(spec.name);
|
|
320
|
+
const setup = spec.setup ?? [];
|
|
321
|
+
assertSetupSteps(spec.name, setup);
|
|
322
|
+
jobs.push({ jobName, op: spec.name, trigger: { kind: "pull_request" }, findingMode: "report" });
|
|
323
|
+
const { agent, push } = githubAgentJobs({
|
|
324
|
+
forge,
|
|
325
|
+
jobName,
|
|
326
|
+
image,
|
|
327
|
+
settings,
|
|
328
|
+
...(spec.runner !== undefined ? { runner: spec.runner } : {}),
|
|
329
|
+
setup: setup.map(setupStepDoc),
|
|
330
|
+
beforeScript,
|
|
331
|
+
...(spec.agent.tokenEnv ? { tokenEnv: spec.agent.tokenEnv } : {}),
|
|
332
|
+
...(words.bin ? { bin: words.bin } : {}),
|
|
333
|
+
});
|
|
334
|
+
files.push({
|
|
335
|
+
name: `${spec.name}.yml`,
|
|
336
|
+
doc: {
|
|
337
|
+
name: spec.name,
|
|
338
|
+
on: { issue_comment: { types: ["created"] } },
|
|
339
|
+
...nonEmpty("env", options.variables),
|
|
340
|
+
// One agent run at a time, so two asks never count the limit at once.
|
|
341
|
+
concurrency: concurrency(jobName),
|
|
342
|
+
permissions: { contents: "read" },
|
|
343
|
+
jobsDoc: {},
|
|
344
|
+
agentDoc: { [jobName]: agent, [agentPushJobName(jobName)]: push },
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
312
349
|
const findingMode = spec.findingMode ?? "report";
|
|
313
350
|
const trigger = resolveOpTrigger(spec);
|
|
314
351
|
assertTriggerSupportsMode(spec.name, findingMode, trigger);
|
|
@@ -321,6 +358,11 @@ export function buildGithubOpPipelineDocs(ops, options = {}) {
|
|
|
321
358
|
const split = spec.reviewAgent === true;
|
|
322
359
|
if (split)
|
|
323
360
|
assertReviewSplit(spec.name, trigger, findingMode, spec.permissions);
|
|
361
|
+
// #4616: a drift agent follows the Op's job, which hands its output on and says whether it found drift.
|
|
362
|
+
const driftAgent = spec.agent?.on === "drift" ? resolveAgentSpec(spec.name, spec.agent, words.product) : undefined;
|
|
363
|
+
if (driftAgent && (split || trigger.kind === "push")) {
|
|
364
|
+
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.`);
|
|
365
|
+
}
|
|
324
366
|
const stepEnv = split ? {} : jobTokenEnv(findingMode !== "report");
|
|
325
367
|
const setup = spec.setup ?? [];
|
|
326
368
|
assertSetupSteps(spec.name, setup);
|
|
@@ -337,7 +379,9 @@ export function buildGithubOpPipelineDocs(ops, options = {}) {
|
|
|
337
379
|
const steps = [checkoutStep(), ...setup.map(setupStepDoc), ...runSteps(beforeScript)];
|
|
338
380
|
steps.push(gated
|
|
339
381
|
? gatedRunStep(RUN_STEP_ID, spec.name, invocation, stepEnv)
|
|
340
|
-
:
|
|
382
|
+
: driftAgent
|
|
383
|
+
? driftRunStep(invocation, stepEnv, forge)
|
|
384
|
+
: { run: invocation, env: stepEnv });
|
|
341
385
|
steps.push(...runSteps(extraScript));
|
|
342
386
|
const review = split
|
|
343
387
|
? reviewJobs({
|
|
@@ -362,7 +406,7 @@ export function buildGithubOpPipelineDocs(ops, options = {}) {
|
|
|
362
406
|
permissions: mergePermissions(spec.name, permissionsFor(findingMode, trigger), spec.permissions ?? {}, trigger),
|
|
363
407
|
jobsDoc: {
|
|
364
408
|
[jobName]: {
|
|
365
|
-
"runs-on":
|
|
409
|
+
"runs-on": githubRunsOn(spec.runner),
|
|
366
410
|
container: image,
|
|
367
411
|
// Per-Op credentials (#2290): job-level `env:`, one level more
|
|
368
412
|
// specific than `options.variables`'s workflow-level `env:` above,
|
|
@@ -381,16 +425,35 @@ export function buildGithubOpPipelineDocs(ops, options = {}) {
|
|
|
381
425
|
`\${{ steps.${RUN_STEP_ID}.outputs.${name} }}`,
|
|
382
426
|
])),
|
|
383
427
|
}
|
|
384
|
-
:
|
|
428
|
+
: driftAgent
|
|
429
|
+
? { outputs: driftRunOutputs(forge) }
|
|
430
|
+
: {}),
|
|
385
431
|
steps,
|
|
386
432
|
},
|
|
387
433
|
},
|
|
388
|
-
...(gated ? { gatedNoticeDoc: { [`${jobName}-gate-notice`]: gateNoticeJob(jobName, image) } } : {}),
|
|
434
|
+
...(gated ? { gatedNoticeDoc: { [`${jobName}-gate-notice`]: gateNoticeJob(jobName, image, spec.runner) } } : {}),
|
|
389
435
|
};
|
|
436
|
+
if (driftAgent) {
|
|
437
|
+
const name = agentJobName(jobName, "drift");
|
|
438
|
+
const { agent, push } = githubAgentJobs({
|
|
439
|
+
forge,
|
|
440
|
+
jobName: name,
|
|
441
|
+
image,
|
|
442
|
+
settings: driftAgent,
|
|
443
|
+
...(spec.runner !== undefined ? { runner: spec.runner } : {}),
|
|
444
|
+
setup: setup.map(setupStepDoc),
|
|
445
|
+
beforeScript,
|
|
446
|
+
...(spec.agent?.tokenEnv ? { tokenEnv: spec.agent.tokenEnv } : {}),
|
|
447
|
+
...(words.bin ? { bin: words.bin } : {}),
|
|
448
|
+
after: jobName,
|
|
449
|
+
});
|
|
450
|
+
doc.agentDoc = { [name]: agent, [agentPushJobName(name)]: push };
|
|
451
|
+
}
|
|
390
452
|
// A split review (#4326): the Op's job is the review job, and the note job rides beside it.
|
|
391
453
|
if (review) {
|
|
392
|
-
|
|
393
|
-
doc.
|
|
454
|
+
const runsOn = spec.runner !== undefined ? { "runs-on": githubRunsOn(spec.runner) } : {};
|
|
455
|
+
doc.jobsDoc = { [jobName]: { ...review.review, ...runsOn, ...(spec.environment ? { environment: environmentDoc(spec.environment) } : {}) } };
|
|
456
|
+
doc.reviewNoteDoc = { [reviewNoteJobName(jobName)]: { ...review.note, ...runsOn } };
|
|
394
457
|
}
|
|
395
458
|
files.push({ name: `${spec.name}.yml`, doc });
|
|
396
459
|
}
|
|
@@ -445,8 +508,8 @@ export function emitOpPipelineYAML(doc) {
|
|
|
445
508
|
// The gated-apply notice job rides in `jobs:` beside the Op's own job; it
|
|
446
509
|
// is carried separately on the doc so a dialect transforms it on its own.
|
|
447
510
|
// 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 ?? {}) } });
|
|
511
|
+
const { gatedNoticeDoc, reviewNoteDoc, agentDoc, ...workflow } = doc;
|
|
512
|
+
return emitWorkflowYAML({ ...workflow, jobsDoc: { ...doc.jobsDoc, ...(gatedNoticeDoc ?? {}), ...(reviewNoteDoc ?? {}), ...(agentDoc ?? {}) } });
|
|
450
513
|
}
|
|
451
514
|
/**
|
|
452
515
|
* Synthesize one `.github/workflows/*.yml` per scheduled Op. Thin wrapper
|
|
@@ -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
|
|
@@ -49,6 +52,16 @@
|
|
|
49
52
|
* checkout. Every later run step starts with an unset of the variables a
|
|
50
53
|
* token is requested with, so the code the job runs holds no OIDC token
|
|
51
54
|
* and cannot request one for another group's identity.
|
|
55
|
+
*
|
|
56
|
+
* With `control` on the spec (#4617), a `control-dispatch` job needs the
|
|
57
|
+
* last wave's jobs and runs after them unless the run was cancelled, so it
|
|
58
|
+
* runs after a failure too. It downloads every job's outcome file, adds
|
|
59
|
+
* them up with `chant run wave --waves-outcome` (through the product's
|
|
60
|
+
* bin) into `CHANT_CONTROL_OUTCOME`, and sends the control repo one
|
|
61
|
+
* dispatch carrying that outcome and the pushed commit. Only the dispatch
|
|
62
|
+
* step holds the control token: the `tokenSecret` secret, mapped into
|
|
63
|
+
* `CHANT_CONTROL_TOKEN` and handed to curl on its stdin
|
|
64
|
+
* (`controlDispatchScript`, #4568).
|
|
52
65
|
*/
|
|
53
66
|
import { type OpWavesSpec } from "@intentius/chant/op/op-waves";
|
|
54
67
|
import type { OpWavesPipelineOptions, OpWavesPipelineResult } from "@intentius/chant/lexicon";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-op-waves-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-op-waves-pipeline.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"generate-op-waves-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-op-waves-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgEG;AAEH,OAAO,EAUL,KAAK,WAAW,EACjB,MAAM,8BAA8B,CAAC;AAEtC,OAAO,KAAK,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAiB9F,OAAO,EAAsB,KAAK,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAEtF,yFAAyF;AACzF,eAAO,MAAM,cAAc,YAAW,CAAC;AAEvC,oEAAoE;AACpE,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1F;AAED,4DAA4D;AAC5D,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAE9F;AAED,qEAAqE;AACrE,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,WAAW,EACjB,OAAO,EAAE,sBAAsB,GAC9B;IAAE,GAAG,EAAE,mBAAmB,CAAC;IAAC,IAAI,EAAE,qBAAqB,CAAC,MAAM,CAAC,CAAC;IAAC,SAAS,CAAC,EAAE,mBAAmB,CAAC;IAAC,QAAQ,CAAC,EAAE,mBAAmB,CAAA;CAAE,CA6EpI;AAkGD,4EAA4E;AAC5E,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,sBAAsB,GAAG,qBAAqB,CAMvH"}
|
|
@@ -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
|
|
@@ -49,8 +52,19 @@
|
|
|
49
52
|
* checkout. Every later run step starts with an unset of the variables a
|
|
50
53
|
* token is requested with, so the code the job runs holds no OIDC token
|
|
51
54
|
* and cannot request one for another group's identity.
|
|
55
|
+
*
|
|
56
|
+
* With `control` on the spec (#4617), a `control-dispatch` job needs the
|
|
57
|
+
* last wave's jobs and runs after them unless the run was cancelled, so it
|
|
58
|
+
* runs after a failure too. It downloads every job's outcome file, adds
|
|
59
|
+
* them up with `chant run wave --waves-outcome` (through the product's
|
|
60
|
+
* bin) into `CHANT_CONTROL_OUTCOME`, and sends the control repo one
|
|
61
|
+
* dispatch carrying that outcome and the pushed commit. Only the dispatch
|
|
62
|
+
* step holds the control token: the `tokenSecret` secret, mapped into
|
|
63
|
+
* `CHANT_CONTROL_TOKEN` and handed to curl on its stdin
|
|
64
|
+
* (`controlDispatchScript`, #4568).
|
|
52
65
|
*/
|
|
53
|
-
import { notifyVariables, opWaveGroupOf, opWaveJobs, opWaveOutcomeDir, opWavesResumeCommand } from "@intentius/chant/op/op-waves";
|
|
66
|
+
import { notifyVariables, opWaveGroupOf, opWaveJobs, opWaveOutcomeDir, opWavesControlTarget, opWavesLastJobs, opWavesOutcomeCommand, opWavesRecordPlansCommand, opWavesResumeCommand, } from "@intentius/chant/op/op-waves";
|
|
67
|
+
import { CONTROL_OUTCOME_VAR, controlDispatchJob } from "@intentius/chant/control/dispatch";
|
|
54
68
|
import { assertEnvironment, assertSetupSteps, CI_IMAGE, dirnameOf, environmentDoc, GITHUB_JOB_TOKEN, githubRunsOn, githubSecretEnv, idTokenAudienceEnv, nonEmpty, pushBranches, setupStepDoc, } from "@intentius/chant/ci-jobs";
|
|
55
69
|
import { OIDC_REQUEST_VARS, TRADE_SPEC_ENV, tradeLine } from "@intentius/chant/ci-identity";
|
|
56
70
|
import { checkoutStep, concurrency, DOWNLOAD_ARTIFACT, downloadArtifactStep, keepRecordStep, runSteps } from "./ci-jobs.js";
|
|
@@ -67,7 +81,7 @@ export function opWaveOutcomeArtifact(spec, jobName) {
|
|
|
67
81
|
}
|
|
68
82
|
/** The workflow document, before emission, and the jobs it holds. */
|
|
69
83
|
export function buildGithubOpWavesDoc(spec, options) {
|
|
70
|
-
const jobs = opWaveJobs(spec, options.specFile);
|
|
84
|
+
const jobs = opWaveJobs(spec, options.specFile, { bin: options.bin });
|
|
71
85
|
const image = options.image ?? OP_WAVES_IMAGE;
|
|
72
86
|
const byReview = spec.waves.some((w) => w.approval === "pr-review");
|
|
73
87
|
for (const wave of spec.waves) {
|
|
@@ -121,6 +135,11 @@ export function buildGithubOpWavesDoc(spec, options) {
|
|
|
121
135
|
steps,
|
|
122
136
|
};
|
|
123
137
|
}
|
|
138
|
+
// #4617: once the last wave ran, one job tells the control repo how the waves ended.
|
|
139
|
+
if (spec.control) {
|
|
140
|
+
const [name, job] = controlJob(spec, options, jobs, image, permissions);
|
|
141
|
+
jobsDoc[name] = job;
|
|
142
|
+
}
|
|
124
143
|
// #3761: the notify variables come from the secrets of the same name.
|
|
125
144
|
const env = { ...githubSecretEnv(notifyVariables(spec.notify)), ...(options.variables ?? {}) };
|
|
126
145
|
const doc = {
|
|
@@ -138,12 +157,51 @@ export function buildGithubOpWavesDoc(spec, options) {
|
|
|
138
157
|
...(byReview ? { plansDoc: buildPlansDoc(spec, options) } : {}),
|
|
139
158
|
};
|
|
140
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* The control dispatch job (#4617): after the last wave, whatever it did,
|
|
162
|
+
* the waves' outcome read from every job's outcome file, then one dispatch
|
|
163
|
+
* carrying it. The pushed commit is the one every wave ran at.
|
|
164
|
+
*/
|
|
165
|
+
function controlJob(spec, options, jobs, image, permissions) {
|
|
166
|
+
const control = spec.control;
|
|
167
|
+
const dispatch = controlDispatchJob("github", opWavesControlTarget(control), "${{ github.sha }}", {
|
|
168
|
+
...(options.words ? { words: options.words } : {}),
|
|
169
|
+
...(control.sentCheck ? { sentCheck: control.sentCheck } : {}),
|
|
170
|
+
...(control.markSent ? { markSent: control.markSent } : {}),
|
|
171
|
+
outcome: true,
|
|
172
|
+
});
|
|
173
|
+
const marks = control.sentCheck !== undefined || control.markSent !== undefined;
|
|
174
|
+
const steps = [
|
|
175
|
+
checkoutStep(true),
|
|
176
|
+
{ name: "Download the waves' outcomes", uses: DOWNLOAD_ARTIFACT, with: { path: opWaveOutcomeDir(spec) } },
|
|
177
|
+
...(spec.waves[0]?.setup ?? []).map(setupStepDoc),
|
|
178
|
+
...runSteps(options.beforeScript ?? []),
|
|
179
|
+
{
|
|
180
|
+
name: "Add up the waves' outcomes",
|
|
181
|
+
run: `outcome="$(${opWavesOutcomeCommand(options.specFile, options.bin).join(" ")})" || exit 1\necho "${CONTROL_OUTCOME_VAR}=$outcome" >> "$GITHUB_ENV"`,
|
|
182
|
+
},
|
|
183
|
+
{ name: `Tell the control repo ${control.repo} how ${control.member}'s waves ended`, env: { ...dispatch.env, ...(marks ? { GITHUB_TOKEN: GITHUB_JOB_TOKEN } : {}) }, run: dispatch.script },
|
|
184
|
+
];
|
|
185
|
+
return [
|
|
186
|
+
dispatch.name,
|
|
187
|
+
{
|
|
188
|
+
"runs-on": githubRunsOn(spec.runner),
|
|
189
|
+
needs: opWavesLastJobs(jobs),
|
|
190
|
+
// The waves ran, whatever they did: a failed run is an outcome the control repo is told about.
|
|
191
|
+
if: "!cancelled()",
|
|
192
|
+
container: image,
|
|
193
|
+
// markSent sets a commit status; the rest reads the repository and the run's artifacts.
|
|
194
|
+
permissions: marks ? { ...permissions, statuses: "write" } : permissions,
|
|
195
|
+
steps,
|
|
196
|
+
},
|
|
197
|
+
];
|
|
198
|
+
}
|
|
141
199
|
/** The scheduled resume workflow (#3683). */
|
|
142
200
|
function buildResumeDoc(spec, options) {
|
|
143
201
|
const steps = [checkoutStep(), ...runSteps(options.beforeScript ?? [])];
|
|
144
202
|
steps.push({
|
|
145
203
|
name: `Resume the waves of ${spec.name} whose approval arrived`,
|
|
146
|
-
run: opWavesResumeCommand(spec).join(" "),
|
|
204
|
+
run: opWavesResumeCommand(spec, options.bin).join(" "),
|
|
147
205
|
env: { GITHUB_TOKEN: GITHUB_JOB_TOKEN },
|
|
148
206
|
});
|
|
149
207
|
return {
|
|
@@ -158,7 +216,7 @@ function buildResumeDoc(spec, options) {
|
|
|
158
216
|
/** The pull request workflow a `pr-review` wave needs (#3684): record the digests each head plans. */
|
|
159
217
|
function buildPlansDoc(spec, options) {
|
|
160
218
|
const steps = [checkoutStep(true), ...(spec.waves[0]?.setup ?? []).map(setupStepDoc), ...runSteps(options.beforeScript ?? [])];
|
|
161
|
-
steps.push({ name: "Plan every wave at the head and record its digests", run:
|
|
219
|
+
steps.push({ name: "Plan every wave at the head and record its digests", run: opWavesRecordPlansCommand(options.specFile, options.bin).join(" ") });
|
|
162
220
|
return {
|
|
163
221
|
name: `${spec.name}-plans`,
|
|
164
222
|
on: { pull_request: { branches: pushBranches(spec.branches) } },
|