@intentius/chant-lexicon-gitlab 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 +68 -0
- package/dist/components/agent-jobs.d.ts.map +1 -0
- package/dist/components/agent-jobs.js +135 -0
- package/dist/components/control-pipeline.d.ts +24 -0
- package/dist/components/control-pipeline.d.ts.map +1 -0
- package/dist/components/control-pipeline.js +28 -0
- package/dist/components/generate-op-pipeline.d.ts +9 -0
- package/dist/components/generate-op-pipeline.d.ts.map +1 -1
- package/dist/components/generate-op-pipeline.js +107 -22
- package/dist/components/generate-op-waves-pipeline.d.ts +12 -0
- package/dist/components/generate-op-waves-pipeline.d.ts.map +1 -1
- package/dist/components/generate-op-waves-pipeline.js +59 -5
- package/dist/components/generate-pipeline.d.ts.map +1 -1
- package/dist/components/generate-pipeline.js +112 -10
- package/dist/components/review-jobs.d.ts +72 -0
- package/dist/components/review-jobs.d.ts.map +1 -0
- package/dist/components/review-jobs.js +124 -0
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/package.json +3 -3
- package/src/components/agent-jobs.ts +176 -0
- package/src/components/control-pipeline.ts +38 -0
- package/src/components/generate-op-pipeline.agent.test.ts +99 -0
- package/src/components/generate-op-pipeline.review.test.ts +72 -7
- package/src/components/generate-op-pipeline.ts +117 -23
- package/src/components/generate-op-waves-pipeline.control.test.ts +55 -0
- package/src/components/generate-op-waves-pipeline.test.ts +17 -0
- package/src/components/generate-op-waves-pipeline.ts +68 -5
- package/src/components/generate-pipeline.ts +117 -9
- package/src/components/generate-pr-pipeline.comment-locks.test.ts +43 -0
- package/src/components/generate-pr-pipeline.comment-preview.test.ts +61 -0
- package/src/components/generate-pr-pipeline.control.test.ts +81 -0
- package/src/components/generate-pr-pipeline.pulumi-identity.test.ts +46 -0
- package/src/components/review-jobs.ts +163 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two jobs of a split review on GitLab (#4544): `ScheduledOpSpec.reviewAgent`.
|
|
3
|
+
*
|
|
4
|
+
* An Op that runs a `reviewAgent` step in `handoff` mode runs the change's
|
|
5
|
+
* code (`chant run` loads the merge request's Op files) beside a model.
|
|
6
|
+
* GitHub and Forgejo keep the write token out of that job with per-job
|
|
7
|
+
* permissions (`lexicons/github/src/components/review-jobs.ts`). GitLab has
|
|
8
|
+
* none: a merge request pipeline is built from the merge request's own files
|
|
9
|
+
* and every job in it gets the project's unprotected variables. So the token
|
|
10
|
+
* stays out of that pipeline altogether, the way the plan note does (#4328):
|
|
11
|
+
*
|
|
12
|
+
* - the review job runs in the merge request pipeline. Its script starts by
|
|
13
|
+
* unsetting every forge token variable (`GITLAB_DROP_FORGE_TOKENS`), and it
|
|
14
|
+
* keeps the review directory as its artifact. `CHANT_FORGE_TOKEN` is a
|
|
15
|
+
* protected variable, which a merge request pipeline never gets.
|
|
16
|
+
* - the note job runs in a pipeline of the default branch, built from that
|
|
17
|
+
* branch's files, that a pipeline schedule (or a person, the API or a
|
|
18
|
+
* trigger) starts with `CHANT_REVIEW_NOTES` set to `1`. It checks nothing
|
|
19
|
+
* out (`GIT_STRATEGY: none`), installs the chant release this lexicon
|
|
20
|
+
* belongs to, and runs `postOpenReviewNotes` with the protected token: for
|
|
21
|
+
* each open merge request whose newest pipeline's review job succeeded it
|
|
22
|
+
* reads `review.json` from that job's artifacts as data and posts the note.
|
|
23
|
+
* The Op's name, the review job's name and the branches come from this
|
|
24
|
+
* file, never from the artifact.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { readFileSync } from "node:fs";
|
|
28
|
+
import { GITLAB_DROP_FORGE_TOKENS } from "@intentius/chant/ci-handover";
|
|
29
|
+
import type { OpTrigger } from "@intentius/chant/lexicon";
|
|
30
|
+
|
|
31
|
+
/** The variable `reviewAgent` reads its handoff directory from (`REVIEW_DIR_ENV`, review-agent.ts). */
|
|
32
|
+
export const REVIEW_DIR_ENV = "CHANT_REVIEW_DIR";
|
|
33
|
+
|
|
34
|
+
/** Where the review job writes the review, from the directory its script runs in. */
|
|
35
|
+
export const REVIEW_DIR = ".chant/review";
|
|
36
|
+
|
|
37
|
+
/** The variable a pipeline of the default branch is started with to run the note jobs. */
|
|
38
|
+
export const GITLAB_REVIEW_NOTES_VAR = "CHANT_REVIEW_NOTES";
|
|
39
|
+
|
|
40
|
+
/** How long a review artifact is kept: long enough for a schedule to come round and post it. */
|
|
41
|
+
const REVIEW_ARTIFACT_EXPIRY = "7 days";
|
|
42
|
+
|
|
43
|
+
/** The name of the note job beside the Op's own job. */
|
|
44
|
+
export function reviewNoteJobName(jobName: string): string {
|
|
45
|
+
return `${jobName}-note`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The note job's rule: a pipeline of the default branch that a pipeline
|
|
50
|
+
* schedule, a person, the API or a trigger started with `CHANT_REVIEW_NOTES`
|
|
51
|
+
* set to `1`. A merge request pipeline's own trigger job starts a pipeline
|
|
52
|
+
* whose source is `pipeline`, which this rule leaves out.
|
|
53
|
+
*/
|
|
54
|
+
export const GITLAB_REVIEW_NOTES_RULE = `$CI_PIPELINE_SOURCE =~ /^(schedule|web|api|trigger)$/ && $${GITLAB_REVIEW_NOTES_VAR} == "1" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH`;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Refuse a spec whose review cannot be split the way this shape needs: a
|
|
58
|
+
* trigger with no merge request, a finding-mode that would post from the
|
|
59
|
+
* review job, or an additive scope other than `id-token: write`.
|
|
60
|
+
*/
|
|
61
|
+
export function assertReviewSplit(name: string, trigger: OpTrigger, findingMode: string, permissions: Record<string, string> | undefined): void {
|
|
62
|
+
if (trigger.kind !== "pull_request") {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`Scheduled Op "${name}" sets reviewAgent, which posts the review as a note on the merge request that triggered the run, but its trigger is "${trigger.kind}". Give it a { kind: "pull_request" } trigger.`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
if (findingMode !== "report") {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`Scheduled Op "${name}" sets reviewAgent with findingMode "${findingMode}". The review job holds no forge token, and the note job posts from a pipeline of the default branch instead, so the Op's own finding-mode must be "report".`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
for (const [scope, level] of Object.entries(permissions ?? {})) {
|
|
73
|
+
if (level === "write" && scope !== "id-token") {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Scheduled Op "${name}" sets reviewAgent and adds the permission "${scope}: write". The review job runs the change's code and may hold only "id-token: write".`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The chant release the note job installs: this lexicon's own, which releases in lockstep with `@intentius/chant`. */
|
|
82
|
+
function chantVersion(): string {
|
|
83
|
+
const pkg = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")) as { version?: unknown };
|
|
84
|
+
if (typeof pkg.version !== "string" || pkg.version === "") {
|
|
85
|
+
throw new Error("the gitlab lexicon's package.json has no version to pin the review note job's chant to");
|
|
86
|
+
}
|
|
87
|
+
return pkg.version;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** What the note job runs under node. The inputs are JSON literals from this file; nothing comes from the merge request. */
|
|
91
|
+
function noteModule(op: string, job: string, artifactDir: string, branches: readonly string[] | undefined): string {
|
|
92
|
+
return [
|
|
93
|
+
'import { forgeFromEnv } from "@intentius/chant/pr-forge";',
|
|
94
|
+
'import { postOpenReviewNotes } from "@intentius/chant/op/activities/review-agent";',
|
|
95
|
+
`const branches = ${JSON.stringify(branches ?? null)} ?? [process.env.CI_DEFAULT_BRANCH];`,
|
|
96
|
+
"const forge = forgeFromEnv(\"gitlab\");",
|
|
97
|
+
"let failed = false;",
|
|
98
|
+
"for (const branch of branches) {",
|
|
99
|
+
` const { posts, warnings } = await postOpenReviewNotes({ forge, branch, job: ${JSON.stringify(job)}, artifactDir: ${JSON.stringify(artifactDir)}, op: ${JSON.stringify(op)} });`,
|
|
100
|
+
" for (const p of posts) console.log(`!${p.pr} at ${p.head.slice(0, 12)}: ${p.skipped ?? `posted, risk ${p.risk}`}`);",
|
|
101
|
+
" for (const w of warnings) { console.error(w); failed = true; }",
|
|
102
|
+
"}",
|
|
103
|
+
"if (failed) process.exitCode = 1;",
|
|
104
|
+
].join("\n");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The note job's script: install the pinned chant outside any checkout and run {@link noteModule} with tsx. */
|
|
108
|
+
function noteScript(version: string, module: string): string[] {
|
|
109
|
+
return [
|
|
110
|
+
'dir="${TMPDIR:-/tmp}/chant-review-note"',
|
|
111
|
+
'mkdir -p "$dir"',
|
|
112
|
+
'cd "$dir"',
|
|
113
|
+
"echo '{\"private\":true}' > package.json",
|
|
114
|
+
`npm install --omit=peer --no-audit --no-fund --loglevel=error @intentius/chant@${version}`,
|
|
115
|
+
["cat > note.mjs <<'NOTE'", module, "NOTE"].join("\n"),
|
|
116
|
+
"node --import tsx note.mjs",
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface GitlabReviewJobsInput {
|
|
121
|
+
/** The Op's name: what `chant run` targets and what keys the note. */
|
|
122
|
+
name: string;
|
|
123
|
+
/** The review job's name: the Op's job name. */
|
|
124
|
+
jobName: string;
|
|
125
|
+
stage: string;
|
|
126
|
+
image: string;
|
|
127
|
+
/** The review job's `rules:`, the Op's merge request rules. */
|
|
128
|
+
rules: unknown[];
|
|
129
|
+
/** The review job's script after the token unset: cd, setup, before-script, the invocation and the extra script. */
|
|
130
|
+
script: string[];
|
|
131
|
+
/** The Op's own per-job environment (`ScheduledOpSpec.variables`). */
|
|
132
|
+
variables?: Record<string, string>;
|
|
133
|
+
idTokens?: Record<string, unknown>;
|
|
134
|
+
/** The review directory from the repository root: `.chant/review`, under a workspace member's directory for a member. */
|
|
135
|
+
artifactDir: string;
|
|
136
|
+
/** The branches the merge requests merge into, from the trigger; the default branch when absent. */
|
|
137
|
+
branches?: readonly string[];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** The review job and the note job of one split review. */
|
|
141
|
+
export function gitlabReviewJobs(i: GitlabReviewJobsInput): { review: Record<string, unknown>; note: Record<string, unknown> } {
|
|
142
|
+
const review = {
|
|
143
|
+
stage: i.stage,
|
|
144
|
+
image: i.image,
|
|
145
|
+
resource_group: i.jobName,
|
|
146
|
+
...(i.idTokens ? { id_tokens: i.idTokens } : {}),
|
|
147
|
+
variables: { ...(i.variables ?? {}), [REVIEW_DIR_ENV]: REVIEW_DIR },
|
|
148
|
+
rules: i.rules,
|
|
149
|
+
// The merge request's code runs in this job, so its shell drops every forge token first.
|
|
150
|
+
script: [GITLAB_DROP_FORGE_TOKENS, ...i.script],
|
|
151
|
+
artifacts: { paths: [i.artifactDir], expire_in: REVIEW_ARTIFACT_EXPIRY },
|
|
152
|
+
};
|
|
153
|
+
const note = {
|
|
154
|
+
stage: i.stage,
|
|
155
|
+
image: i.image,
|
|
156
|
+
resource_group: reviewNoteJobName(i.jobName),
|
|
157
|
+
// Nothing of the repository is needed: the review is read from the review job's artifacts through the API.
|
|
158
|
+
variables: { GIT_STRATEGY: "none" },
|
|
159
|
+
rules: [{ if: GITLAB_REVIEW_NOTES_RULE }],
|
|
160
|
+
script: noteScript(chantVersion(), noteModule(i.name, i.jobName, i.artifactDir, i.branches)),
|
|
161
|
+
};
|
|
162
|
+
return { review, note };
|
|
163
|
+
}
|