@intentius/chant-lexicon-github 0.124.31 → 0.124.33
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/action-pins.js +53 -0
- package/dist/codegen/docs-cli.js +2 -0
- package/dist/codegen/docs.js +142 -0
- package/dist/codegen/generate-cli.js +26 -0
- package/dist/codegen/generate-lexicon.js +36 -0
- package/dist/codegen/generate-typescript.js +108 -0
- package/dist/codegen/generate.js +111 -0
- package/dist/codegen/naming.js +49 -0
- package/dist/codegen/package.js +40 -0
- package/dist/codegen/parse.js +595 -0
- package/dist/codegen/patches.js +36 -0
- package/dist/components/ci-jobs.d.ts +7 -0
- package/dist/components/ci-jobs.d.ts.map +1 -1
- package/dist/components/ci-jobs.js +98 -0
- package/dist/components/generate-op-pipeline.js +440 -0
- package/dist/components/generate-op-waves-pipeline.js +158 -0
- package/dist/components/generate-pipeline.d.ts.map +1 -1
- package/dist/components/generate-pipeline.js +481 -0
- package/dist/composites/cache.js +17 -0
- package/dist/composites/catalog.js +859 -0
- package/dist/composites/checkout.js +25 -0
- package/dist/composites/dependabot.js +43 -0
- package/dist/composites/deploy-environment.js +38 -0
- package/dist/composites/docker-build.js +70 -0
- package/dist/composites/download-artifact.js +18 -0
- package/dist/composites/go-ci.js +62 -0
- package/dist/composites/index.js +15 -0
- package/dist/composites/node-ci.js +44 -0
- package/dist/composites/node-pipeline.js +96 -0
- package/dist/composites/pr-plan-report.js +129 -0
- package/dist/composites/python-ci.js +60 -0
- package/dist/composites/setup-go.js +18 -0
- package/dist/composites/setup-node.js +21 -0
- package/dist/composites/setup-python.js +18 -0
- package/dist/composites/upload-artifact.js +19 -0
- package/dist/coverage.js +32 -0
- package/dist/expression.js +177 -0
- package/dist/generated/index.js +26 -0
- package/dist/generated/runtime.js +4 -0
- package/dist/graph-edges.js +174 -0
- package/dist/import/generator.js +108 -0
- package/dist/import/parser.js +69 -0
- package/dist/index.js +22 -0
- package/dist/integrity.json +2 -2
- package/dist/lint/audit-catalog.js +69 -0
- package/dist/lint/audit-lineage.js +168 -0
- package/dist/lint/post-synth/gha006.js +50 -0
- package/dist/lint/post-synth/gha009.js +35 -0
- package/dist/lint/post-synth/gha011.js +34 -0
- package/dist/lint/post-synth/gha013.js +40 -0
- package/dist/lint/post-synth/gha017.js +25 -0
- package/dist/lint/post-synth/gha018.js +33 -0
- package/dist/lint/post-synth/gha019.js +58 -0
- package/dist/lint/post-synth/gha021.js +42 -0
- package/dist/lint/post-synth/gha022.js +44 -0
- package/dist/lint/post-synth/gha023.js +37 -0
- package/dist/lint/post-synth/gha024.js +32 -0
- package/dist/lint/post-synth/gha025.js +33 -0
- package/dist/lint/post-synth/gha026.js +32 -0
- package/dist/lint/post-synth/gha027.js +46 -0
- package/dist/lint/post-synth/gha028.js +28 -0
- package/dist/lint/post-synth/gha029.js +56 -0
- package/dist/lint/post-synth/gha030.js +35 -0
- package/dist/lint/post-synth/gha031.js +75 -0
- package/dist/lint/post-synth/gha032.js +35 -0
- package/dist/lint/post-synth/gha033.js +39 -0
- package/dist/lint/post-synth/gha034.js +35 -0
- package/dist/lint/post-synth/gha035.js +50 -0
- package/dist/lint/post-synth/gha036.js +37 -0
- package/dist/lint/post-synth/gha037.js +41 -0
- package/dist/lint/post-synth/gha038.js +35 -0
- package/dist/lint/post-synth/gha039.js +34 -0
- package/dist/lint/post-synth/gha040.js +36 -0
- package/dist/lint/post-synth/gha041.js +32 -0
- package/dist/lint/post-synth/gha042.js +33 -0
- package/dist/lint/post-synth/gha043.js +35 -0
- package/dist/lint/post-synth/gha044.js +36 -0
- package/dist/lint/post-synth/gha045.js +33 -0
- package/dist/lint/post-synth/gha046.js +55 -0
- package/dist/lint/post-synth/gha047.js +44 -0
- package/dist/lint/post-synth/gha048.js +34 -0
- package/dist/lint/post-synth/gha049.js +36 -0
- package/dist/lint/post-synth/gha050.js +37 -0
- package/dist/lint/post-synth/gha051.js +38 -0
- package/dist/lint/post-synth/gha052.js +35 -0
- package/dist/lint/post-synth/gha053.js +34 -0
- package/dist/lint/post-synth/gha054.js +35 -0
- package/dist/lint/post-synth/gha055.js +44 -0
- package/dist/lint/post-synth/gha056.js +32 -0
- package/dist/lint/post-synth/gha057.js +35 -0
- package/dist/lint/post-synth/gha058.js +42 -0
- package/dist/lint/post-synth/gha059.js +119 -0
- package/dist/lint/post-synth/gha060.js +129 -0
- package/dist/lint/post-synth/gha061.js +78 -0
- package/dist/lint/post-synth/gha062.js +65 -0
- package/dist/lint/post-synth/gha063.js +56 -0
- package/dist/lint/post-synth/gha064.js +47 -0
- package/dist/lint/post-synth/gha065.js +125 -0
- package/dist/lint/post-synth/gha066.js +40 -0
- package/dist/lint/post-synth/gha067.js +75 -0
- package/dist/lint/post-synth/gha068.js +47 -0
- package/dist/lint/post-synth/gha069.js +99 -0
- package/dist/lint/post-synth/index.js +114 -0
- package/dist/lint/post-synth/yaml-helpers.js +465 -0
- package/dist/lint/rules/data/action-usage-policy.js +16 -0
- package/dist/lint/rules/data/advisory-feed.js +15 -0
- package/dist/lint/rules/data/deprecated-versions.js +14 -0
- package/dist/lint/rules/data/flagged-actions.js +18 -0
- package/dist/lint/rules/data/known-action-slugs.js +38 -0
- package/dist/lint/rules/data/known-actions.js +12 -0
- package/dist/lint/rules/data/preinstalled-tools.js +36 -0
- package/dist/lint/rules/data/recommended-inputs.js +9 -0
- package/dist/lint/rules/data/risky-features.js +22 -0
- package/dist/lint/rules/data/secret-patterns.js +30 -0
- package/dist/lint/rules/data/trusted-action-owners.js +12 -0
- package/dist/lint/rules/data/untrusted-contexts.js +44 -0
- package/dist/lint/rules/deprecated-action-version.js +44 -0
- package/dist/lint/rules/detect-secrets.js +45 -0
- package/dist/lint/rules/extract-inline-structs.js +55 -0
- package/dist/lint/rules/file-job-limit.js +43 -0
- package/dist/lint/rules/index.js +16 -0
- package/dist/lint/rules/job-timeout.js +53 -0
- package/dist/lint/rules/missing-recommended-inputs.js +51 -0
- package/dist/lint/rules/no-hardcoded-secrets.js +39 -0
- package/dist/lint/rules/no-raw-expressions.js +44 -0
- package/dist/lint/rules/suggest-cache.js +60 -0
- package/dist/lint/rules/use-condition-builders.js +37 -0
- package/dist/lint/rules/use-matrix-builder.js +36 -0
- package/dist/lint/rules/use-typed-actions.js +41 -0
- package/dist/lint/rules/validate-concurrency.js +59 -0
- package/dist/lsp/completions.d.ts.map +1 -1
- package/dist/lsp/completions.js +18 -0
- package/dist/lsp/hover.d.ts.map +1 -1
- package/dist/lsp/hover.js +33 -0
- package/dist/manifest.json +1 -1
- package/dist/mcp/context-tools.js +152 -0
- package/dist/package-cli.js +20 -0
- package/dist/plugin.js +344 -0
- package/dist/serializer.js +572 -0
- package/dist/spec/fetch.js +46 -0
- package/dist/validate-cli.js +16 -0
- package/dist/validate.js +38 -0
- package/dist/variables.js +41 -0
- package/package.json +6 -6
- package/src/codegen/package.ts +2 -2
- package/src/components/__fixtures__/pr-loop-build.github.golden.yml +6 -2
- package/src/components/__fixtures__/pr-loop-record.github.golden.yml +74 -0
- package/src/components/__fixtures__/pr-loop-waves.github.golden.yml +178 -0
- package/src/components/__fixtures__/pr-loop.github.golden.yml +6 -2
- package/src/components/ci-jobs.ts +16 -2
- package/src/components/generate-op-pipeline.test.ts +11 -0
- package/src/components/generate-op-pipeline.ts +3 -3
- package/src/components/generate-pipeline.ts +74 -31
- package/src/components/generate-pr-pipeline.test.ts +99 -29
- package/src/components/generate-pr-waves-pipeline.test.ts +117 -0
- package/src/lsp/completions.ts +2 -1
- package/src/lsp/hover.ts +2 -1
|
@@ -60,7 +60,18 @@ import { resolveComponentGraph, type DriverComponent } from "@intentius/chant/co
|
|
|
60
60
|
import { hasPublishStep, promoteArchivePaths } from "@intentius/chant/components/promote";
|
|
61
61
|
import { GATED_WAVE_RECORD, gatedWaveJobs } from "@intentius/chant/components/gated-wave-pipeline";
|
|
62
62
|
import { CI_IMAGE, ciJobName, dirnameOf, fillCommand, GITHUB_RUNNER } from "@intentius/chant/ci-jobs";
|
|
63
|
-
import {
|
|
63
|
+
import {
|
|
64
|
+
PR_APPLY_RECORD,
|
|
65
|
+
PR_LOOP_REPORT_DIR,
|
|
66
|
+
prApplyGroup,
|
|
67
|
+
prRecordGroup,
|
|
68
|
+
prApplyRecordKey,
|
|
69
|
+
prApplyWaveJobName,
|
|
70
|
+
prLoopJobs,
|
|
71
|
+
prLoopSetup,
|
|
72
|
+
prLoopWaveJobs,
|
|
73
|
+
prLoopWithWaves,
|
|
74
|
+
} from "@intentius/chant/components/pr-pipeline";
|
|
64
75
|
import { memberRepoPath } from "@intentius/chant/lexicon";
|
|
65
76
|
import type {
|
|
66
77
|
ComponentPipelineJob as GeneratedJob,
|
|
@@ -161,7 +172,10 @@ function gatedWaveGithubDoc(
|
|
|
161
172
|
|
|
162
173
|
/**
|
|
163
174
|
* The pull-request workflow (#3183): `plan` on each pull request into the
|
|
164
|
-
* target branch,
|
|
175
|
+
* target branch, and on each push to it `record` (#4168), or `apply` with
|
|
176
|
+
* `applyOnMerge`. The record job plans on the merged commit and writes the
|
|
177
|
+
* merge record, applying nothing, so it pushes to `chant/lifecycle` and runs
|
|
178
|
+
* one at a time per environment as the apply does. The plan job can comment and set
|
|
165
179
|
* statuses but cannot push, since it runs the pull request's code; the apply
|
|
166
180
|
* job can push, to record a pending fact on `chant/lifecycle`, and runs one
|
|
167
181
|
* at a time per environment. Both keep their report as an artifact, and both
|
|
@@ -175,8 +189,17 @@ function gatedWaveGithubDoc(
|
|
|
175
189
|
* path filter, since a change outside the member can reach it; the plan
|
|
176
190
|
* selects from the whole change and passes `--member`, which keeps the
|
|
177
191
|
* member's gate, note and statuses apart from the other members'.
|
|
192
|
+
*
|
|
193
|
+
* With a gate per wave (`waveGate`, #4003) the one apply job becomes
|
|
194
|
+
* `apply-wave-1` to `apply-wave-<n>`, one per wave of the component graph,
|
|
195
|
+
* each needing the one before. Each saves the record under its own cache key
|
|
196
|
+
* (the run, the attempt and the wave), and each restores the newest record
|
|
197
|
+
* for the pushed commit, so wave k reads what wave k-1 left, and a re-run of
|
|
198
|
+
* a wave that stopped at its gate reads what it left itself. Each keeps its
|
|
199
|
+
* report as `chant-pr-apply-wave-<k>`.
|
|
178
200
|
*/
|
|
179
201
|
function prLoopGithubDoc(
|
|
202
|
+
components: DriverComponent[],
|
|
180
203
|
env: string,
|
|
181
204
|
image: string,
|
|
182
205
|
beforeScript: string[],
|
|
@@ -187,41 +210,62 @@ function prLoopGithubDoc(
|
|
|
187
210
|
const branch = loop.branch ?? "main";
|
|
188
211
|
const member = options.member;
|
|
189
212
|
const rooted = !member || member.dir === "." || member.dir === "";
|
|
190
|
-
const [plan, apply] = prLoopJobs(loop.forge ?? "github", env, loop, member?.name);
|
|
213
|
+
const [plan, apply, record] = prLoopJobs(loop.forge ?? "github", env, loop, member?.name);
|
|
191
214
|
const reportPath = member ? memberRepoPath(member, PR_LOOP_REPORT_DIR) : PR_LOOP_REPORT_DIR;
|
|
192
215
|
// The apply's attempt record (#3543) lives in the cache under the pushed
|
|
193
216
|
// commit. Cache keys cannot be overwritten, so each attempt saves under
|
|
194
217
|
// its run and attempt, and a re-run restores the newest for the commit.
|
|
195
218
|
const recordPath = member ? memberRepoPath(member, PR_APPLY_RECORD) : PR_APPLY_RECORD;
|
|
196
219
|
const recordPrefix = `${prApplyRecordKey(env, "${{ github.sha }}", member?.name)}-`;
|
|
197
|
-
const recordKey = `${recordPrefix}\${{ github.run_id }}-\${{ github.run_attempt }}`;
|
|
198
|
-
const restoreRecord = {
|
|
199
|
-
name: "Restore the record of an earlier attempt at this commit",
|
|
220
|
+
const recordKey = (wave?: number) => `${recordPrefix}\${{ github.run_id }}-\${{ github.run_attempt }}${wave ? `-wave-${wave}` : ""}`;
|
|
221
|
+
const restoreRecord = (wave?: number) => ({
|
|
222
|
+
name: wave ? "Restore the newest record of this commit's apply" : "Restore the record of an earlier attempt at this commit",
|
|
200
223
|
uses: "actions/cache/restore@v4",
|
|
201
|
-
with: { path: recordPath, key: recordKey, "restore-keys": recordPrefix },
|
|
202
|
-
};
|
|
203
|
-
const saveRecord = {
|
|
204
|
-
name: "Keep the record for a re-run",
|
|
224
|
+
with: { path: recordPath, key: recordKey(wave), "restore-keys": recordPrefix },
|
|
225
|
+
});
|
|
226
|
+
const saveRecord = (wave?: number) => ({
|
|
227
|
+
name: wave ? "Keep the record for the next wave and a re-run" : "Keep the record for a re-run",
|
|
205
228
|
if: "always()",
|
|
206
229
|
uses: "actions/cache/save@v4",
|
|
207
|
-
with: { path: recordPath, key: recordKey },
|
|
208
|
-
};
|
|
230
|
+
with: { path: recordPath, key: recordKey(wave) },
|
|
231
|
+
});
|
|
232
|
+
const reportName = (job: typeof plan) => `chant-pr-${job.wave ? prApplyWaveJobName(job.wave) : job.jobName}`;
|
|
209
233
|
const steps = (job: typeof plan, name: string): Array<Record<string, unknown>> => [
|
|
210
234
|
checkoutStep(true),
|
|
211
235
|
...runSteps(prLoopSetup(image)),
|
|
212
236
|
...runSteps(beforeScript),
|
|
213
237
|
...(job.build ? [{ name: "Build the templates", run: job.build[0] }, { name: "Lint the project", run: job.build[1] }] : []),
|
|
214
|
-
...(job.jobName === "apply" ? [restoreRecord] : []),
|
|
215
|
-
|
|
238
|
+
...(job.jobName === "apply" ? [restoreRecord(job.wave)] : []),
|
|
239
|
+
// The job's setup lines (#4004) run in the command's shell, so a value they settle reaches it.
|
|
240
|
+
// The command runs through chant ci run (#4156), which writes the job's run record, signed when the CHANT_RUNNER_KEY secret is set.
|
|
241
|
+
{ name, env: job.run.env, run: [...(job.setup ?? []), job.run.command].join("\n") },
|
|
216
242
|
...runSteps(extraScript),
|
|
217
|
-
...(job.jobName === "apply" ? [saveRecord] : []),
|
|
243
|
+
...(job.jobName === "apply" ? [saveRecord(job.wave)] : []),
|
|
218
244
|
{
|
|
219
|
-
name: `Keep the ${job.jobName} report`,
|
|
245
|
+
name: `Keep the ${job.wave ? `wave ${job.wave} ` : ""}${job.jobName} report`,
|
|
220
246
|
if: "always()",
|
|
221
247
|
uses: UPLOAD_ARTIFACT,
|
|
222
|
-
with: { name:
|
|
248
|
+
with: { name: reportName(job), path: reportPath, "if-no-files-found": "ignore", "include-hidden-files": true },
|
|
223
249
|
},
|
|
224
250
|
];
|
|
251
|
+
const applyJob = (job: typeof plan, needs: string[], name: string) => ({
|
|
252
|
+
if: "github.event_name == 'push'",
|
|
253
|
+
"runs-on": GITHUB_RUNNER,
|
|
254
|
+
...(needs.length > 0 ? { needs } : {}),
|
|
255
|
+
container: image,
|
|
256
|
+
concurrency: concurrency(prApplyGroup(env, member?.name)),
|
|
257
|
+
permissions: { contents: "write", "pull-requests": "write", statuses: "write" },
|
|
258
|
+
steps: steps(job, name),
|
|
259
|
+
});
|
|
260
|
+
const waveJobs = loop.waveGate ? prLoopWaveJobs(components, apply) : [];
|
|
261
|
+
const applyJobs: Array<[string, Record<string, unknown>]> = loop.waveGate
|
|
262
|
+
? waveJobs.map((job) => [
|
|
263
|
+
prApplyWaveJobName(job.wave!),
|
|
264
|
+
applyJob(job, job.wave! > 1 ? [prApplyWaveJobName(job.wave! - 1)] : [], `Apply wave ${job.wave}, under the approval its plan has`),
|
|
265
|
+
])
|
|
266
|
+
: loop.applyOnMerge
|
|
267
|
+
? [["apply", applyJob(apply, [], "Apply the plan a reviewer approved")]]
|
|
268
|
+
: [["record", { ...applyJob(record, [], "Record the plan on the merged commit; nothing applies"), concurrency: concurrency(prRecordGroup(env, member?.name)) }]];
|
|
225
269
|
const jobsDoc: Record<string, unknown> = {
|
|
226
270
|
plan: {
|
|
227
271
|
if: "github.event_name == 'pull_request'",
|
|
@@ -230,15 +274,9 @@ function prLoopGithubDoc(
|
|
|
230
274
|
permissions: { contents: "read", "pull-requests": "write", statuses: "write" },
|
|
231
275
|
steps: steps(plan, "Plan the members this pull request reaches"),
|
|
232
276
|
},
|
|
233
|
-
|
|
234
|
-
if: "github.event_name == 'push'",
|
|
235
|
-
"runs-on": GITHUB_RUNNER,
|
|
236
|
-
container: image,
|
|
237
|
-
concurrency: concurrency(prApplyGroup(env, member?.name)),
|
|
238
|
-
permissions: { contents: "write", "pull-requests": "write", statuses: "write" },
|
|
239
|
-
steps: steps(apply, "Apply the plan a reviewer approved"),
|
|
240
|
-
},
|
|
277
|
+
...Object.fromEntries(applyJobs),
|
|
241
278
|
};
|
|
279
|
+
const applyNames = applyJobs.map(([name]) => name);
|
|
242
280
|
return {
|
|
243
281
|
name: member ? `chant-pr.${member.name}.${env}` : `chant-pr-${env}`,
|
|
244
282
|
environment: env,
|
|
@@ -246,10 +284,15 @@ function prLoopGithubDoc(
|
|
|
246
284
|
...(rooted ? {} : { defaults: { run: { "working-directory": member!.dir } } }),
|
|
247
285
|
env: { ...options.variables, CHANT_ENV: env },
|
|
248
286
|
jobsDoc,
|
|
249
|
-
stages: ["plan",
|
|
287
|
+
stages: ["plan", ...applyNames],
|
|
250
288
|
jobs: [
|
|
251
289
|
{ jobName: "plan", component: "pull request plan", stage: "plan", needs: [] },
|
|
252
|
-
|
|
290
|
+
...applyNames.map((name, i) => ({
|
|
291
|
+
jobName: name,
|
|
292
|
+
component: loop.waveGate ? `pull request apply, wave ${i + 1}` : loop.applyOnMerge ? "pull request apply" : "merge record",
|
|
293
|
+
stage: name,
|
|
294
|
+
needs: i > 0 ? [applyNames[i - 1]] : [],
|
|
295
|
+
})),
|
|
253
296
|
],
|
|
254
297
|
};
|
|
255
298
|
}
|
|
@@ -315,10 +358,10 @@ export function buildGithubPipelineDoc(
|
|
|
315
358
|
const byName = new Map(components.map((c) => [c.name, c]));
|
|
316
359
|
|
|
317
360
|
if (options.prLoop) {
|
|
318
|
-
if (options.
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
return prLoopGithubDoc(env, image, beforeScript, extraScript, options);
|
|
361
|
+
if (options.promoteTo !== undefined) throw new Error("a pull-request pipeline has no promote job; drop --promote-to or --pr-loop");
|
|
362
|
+
// #4003: --wave-gate beside --pr-loop gives the apply a gate per wave.
|
|
363
|
+
const prLoop = prLoopWithWaves(options.prLoop, options.gatedWaves);
|
|
364
|
+
return prLoopGithubDoc(components, env, image, beforeScript, extraScript, { ...options, prLoop });
|
|
322
365
|
}
|
|
323
366
|
|
|
324
367
|
if (options.gatedWaves) {
|
|
@@ -11,7 +11,7 @@ import { join } from "node:path";
|
|
|
11
11
|
import { describe, expect, test } from "vitest";
|
|
12
12
|
import { parseYAML } from "@intentius/chant/yaml";
|
|
13
13
|
import type { DriverComponent } from "@intentius/chant/components/driver";
|
|
14
|
-
import { prApplyGroup, prApplyRecordKey } from "@intentius/chant/components/pr-pipeline";
|
|
14
|
+
import { githubBaseFallback, prApplyGroup, prApplyRecordKey, prLoopJobs } from "@intentius/chant/components/pr-pipeline";
|
|
15
15
|
import { generateGithubPipeline } from "./generate-pipeline";
|
|
16
16
|
|
|
17
17
|
const ESTATE: DriverComponent[] = [
|
|
@@ -46,7 +46,7 @@ interface Job {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
describe("the GitHub pull-request workflow", () => {
|
|
49
|
-
const result = generateGithubPipeline(ESTATE, { env: "prod", prLoop: {} });
|
|
49
|
+
const result = generateGithubPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true } });
|
|
50
50
|
const doc = parseYAML(result.yaml) as { name: string; on: Record<string, unknown>; jobs: Record<string, Job> };
|
|
51
51
|
|
|
52
52
|
test("matches its golden", () => {
|
|
@@ -63,28 +63,49 @@ describe("the GitHub pull-request workflow", () => {
|
|
|
63
63
|
});
|
|
64
64
|
|
|
65
65
|
test("the plan job measures from the pull request's base and cannot push; the apply job can, one at a time", () => {
|
|
66
|
-
const plan = doc.jobs.plan.steps.find((s) => s.run?.includes("
|
|
67
|
-
|
|
66
|
+
const plan = doc.jobs.plan.steps.find((s) => s.run?.includes("ci run plan"))!;
|
|
67
|
+
// #4156: the job runs pr-plan through chant ci run, in its own checkout, and pushes no record from the pull request's code.
|
|
68
|
+
expect(plan.run).toBe('chant ci run plan --in-place --no-push --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --forge github');
|
|
68
69
|
expect(plan.env).toMatchObject({ BASE_SHA: "${{ github.event.pull_request.base.sha }}", PR_NUMBER: "${{ github.event.pull_request.number }}" });
|
|
70
|
+
// The runner key is an optional secret: unset, the expression is empty and the record is unsigned.
|
|
71
|
+
expect(plan.env?.CHANT_RUNNER_KEY).toBe("${{ secrets.CHANT_RUNNER_KEY }}");
|
|
69
72
|
expect(doc.jobs.plan.permissions).toEqual({ contents: "read", "pull-requests": "write", statuses: "write" });
|
|
70
73
|
|
|
71
|
-
const apply = doc.jobs.apply.steps.find((s) => s.run?.includes("
|
|
72
|
-
|
|
73
|
-
expect(apply.
|
|
74
|
+
const apply = doc.jobs.apply.steps.find((s) => s.run?.includes("ci run apply"))!;
|
|
75
|
+
// #4168: ci run builds pr-apply without --require-review unless the pipeline asks for it.
|
|
76
|
+
expect(apply.run).not.toContain("--require-review");
|
|
77
|
+
expect(apply.run).not.toContain("--no-push");
|
|
78
|
+
expect(apply.env).toMatchObject({ BASE_SHA: "${{ github.event.before }}", CHANT_RUNNER_KEY: "${{ secrets.CHANT_RUNNER_KEY }}" });
|
|
74
79
|
expect(doc.jobs.apply.permissions.contents).toBe("write");
|
|
75
80
|
expect(doc.jobs.apply.concurrency).toEqual({ group: "chant-apply-prod", "cancel-in-progress": false });
|
|
76
81
|
for (const job of Object.values(doc.jobs)) expect(job.steps[0].with).toEqual({ "fetch-depth": 0 });
|
|
77
82
|
});
|
|
78
83
|
|
|
84
|
+
test("a first push's all-zero github.event.before is settled in the apply's own shell, before pr-apply reads it (#4004)", () => {
|
|
85
|
+
const apply = doc.jobs.apply.steps.find((s) => s.run?.includes("ci run apply"))!;
|
|
86
|
+
const lines = apply.run!.trimEnd().split("\n");
|
|
87
|
+
expect(lines).toHaveLength(2);
|
|
88
|
+
expect(lines[0]).toBe(githubBaseFallback());
|
|
89
|
+
expect(lines[0]).toContain('[ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]');
|
|
90
|
+
expect(lines[0]).toContain('git merge-base FETCH_HEAD "$GITHUB_SHA"');
|
|
91
|
+
expect(lines[1]).toMatch(/^chant ci run apply --in-place --base "\$BASE_SHA"/);
|
|
92
|
+
const trunk = parseYAML(generateGithubPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true, branch: "trunk" } }).yaml) as { jobs: Record<string, Job> };
|
|
93
|
+
expect(trunk.jobs.apply.steps.find((s) => s.run?.includes("ci run apply"))!.run).toContain('git fetch origin "trunk"');
|
|
94
|
+
const plan = doc.jobs.plan.steps.find((s) => s.run?.includes("ci run plan"))!;
|
|
95
|
+
expect(plan.run).not.toContain("0000000000000000000000000000000000000000");
|
|
96
|
+
});
|
|
97
|
+
|
|
79
98
|
test("the apply resumes from a record the cache keeps for the pushed commit across a re-run (#3543)", () => {
|
|
80
99
|
const steps = doc.jobs.apply.steps;
|
|
81
100
|
const restore = steps.findIndex((s) => s.uses === "actions/cache/restore@v4");
|
|
82
|
-
const run = steps.findIndex((s) => s.run?.includes("
|
|
101
|
+
const run = steps.findIndex((s) => s.run?.includes("ci run apply"));
|
|
83
102
|
const save = steps.findIndex((s) => s.uses === "actions/cache/save@v4");
|
|
84
103
|
expect(restore).toBeGreaterThan(-1);
|
|
85
104
|
expect(restore).toBeLessThan(run);
|
|
86
105
|
expect(save).toBeGreaterThan(run);
|
|
87
|
-
|
|
106
|
+
// ci run runs the inner pr-apply in the checkout, where the cache restored the record.
|
|
107
|
+
expect(steps[run].run).toContain("--in-place");
|
|
108
|
+
expect(prLoopJobs("github", "prod")[1]!.command).toContain("--resume .chant/pr-resume/pr-apply.json");
|
|
88
109
|
const key = "chant-apply-prod-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}";
|
|
89
110
|
expect(steps[restore].with).toEqual({ path: ".chant/pr-resume/pr-apply.json", key, "restore-keys": "chant-apply-prod-${{ github.sha }}-" });
|
|
90
111
|
// Saved even when the apply failed, which is when a re-run needs it.
|
|
@@ -92,7 +113,7 @@ describe("the GitHub pull-request workflow", () => {
|
|
|
92
113
|
expect(steps[save].with).toEqual({ path: ".chant/pr-resume/pr-apply.json", key });
|
|
93
114
|
// The plan job runs the pull request's code: it neither reads nor writes the record.
|
|
94
115
|
expect(doc.jobs.plan.steps.some((s) => s.uses?.startsWith("actions/cache"))).toBe(false);
|
|
95
|
-
expect(doc.jobs.plan.steps.find((s) => s.run?.includes("
|
|
116
|
+
expect(doc.jobs.plan.steps.find((s) => s.run?.includes("ci run plan"))!.run).not.toContain("--resume");
|
|
96
117
|
// The report artifact does not carry the record, which holds the members' outputs.
|
|
97
118
|
const keep = steps.find((s) => s.uses?.startsWith("actions/upload-artifact"))!;
|
|
98
119
|
expect(keep.with?.path).toBe(".chant/pr");
|
|
@@ -100,27 +121,29 @@ describe("the GitHub pull-request workflow", () => {
|
|
|
100
121
|
|
|
101
122
|
test("runs in an image with git and installs OpenTofu, unless the caller names an image", () => {
|
|
102
123
|
expect(doc.jobs.plan.steps[1].run).toContain("install-opentofu");
|
|
103
|
-
const own = generateGithubPipeline(ESTATE, { env: "prod", image: "example/ci:1", prLoop: {} }).yaml;
|
|
124
|
+
const own = generateGithubPipeline(ESTATE, { env: "prod", image: "example/ci:1", prLoop: { applyOnMerge: true } }).yaml;
|
|
104
125
|
expect(own).toContain("container: example/ci:1");
|
|
105
126
|
expect(own).not.toContain("install-opentofu");
|
|
106
127
|
});
|
|
107
128
|
|
|
108
129
|
test("the gate, branch and review requirement are options", () => {
|
|
109
|
-
const yaml = generateGithubPipeline(ESTATE, { env: "prod", prLoop: { gate: "infra-apply", branch: "trunk", requireReview:
|
|
130
|
+
const yaml = generateGithubPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true, gate: "infra-apply", branch: "trunk", requireReview: true } }).yaml;
|
|
110
131
|
expect(yaml).toContain("--gate infra-apply");
|
|
111
132
|
expect(yaml).toContain("- trunk");
|
|
112
|
-
expect(yaml).
|
|
133
|
+
expect(yaml).toContain("ci run apply --in-place --base \"$BASE_SHA\" --env prod --gate infra-apply --forge github --branch \"trunk\" --require-review");
|
|
134
|
+
expect(yaml).toContain('--branch "trunk"');
|
|
113
135
|
});
|
|
114
136
|
|
|
115
|
-
test("refuses a
|
|
116
|
-
expect(() => generateGithubPipeline(ESTATE, { prLoop: {
|
|
117
|
-
expect(() => generateGithubPipeline(ESTATE, { prLoop: {},
|
|
137
|
+
test("refuses a promote job beside it, and a canary or a fixed base beside --wave-gate (#4003)", () => {
|
|
138
|
+
expect(() => generateGithubPipeline(ESTATE, { prLoop: { applyOnMerge: true }, promoteTo: "prod" })).toThrow(/pull-request pipeline has no promote job/);
|
|
139
|
+
expect(() => generateGithubPipeline(ESTATE, { prLoop: { applyOnMerge: true }, gatedWaves: { gate: "g", canary: ["net"] } })).toThrow(/takes no --canary/);
|
|
140
|
+
expect(() => generateGithubPipeline(ESTATE, { prLoop: { applyOnMerge: true }, gatedWaves: { gate: "g", base: "HEAD~1" } })).toThrow(/takes no --base/);
|
|
118
141
|
});
|
|
119
142
|
|
|
120
143
|
test("events and reportUrl stream the run's events into the report artifact and copy the reports out (#4007)", () => {
|
|
121
|
-
const loud = parseYAML(generateGithubPipeline(ESTATE, { env: "prod", prLoop: { events: true, reportUrl: "s3://chant-reports/acme" } }).yaml) as { jobs: Record<string, Job> };
|
|
144
|
+
const loud = parseYAML(generateGithubPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true, events: true, reportUrl: "s3://chant-reports/acme" } }).yaml) as { jobs: Record<string, Job> };
|
|
122
145
|
for (const job of ["plan", "apply"]) {
|
|
123
|
-
const run = loud.jobs[job].steps.find((s) => s.run?.
|
|
146
|
+
const run = loud.jobs[job].steps.find((s) => s.run?.includes("chant ci run"))!;
|
|
124
147
|
expect(run.run).toContain('--events-json --report-url "s3://chant-reports/acme"');
|
|
125
148
|
expect(run.env?.CHANT_RUN_EVENTS_DIR).toBe(".chant/pr/run-events");
|
|
126
149
|
}
|
|
@@ -131,7 +154,7 @@ describe("the GitHub pull-request workflow", () => {
|
|
|
131
154
|
|
|
132
155
|
test("inside a workspace member: runs in the member's directory and keeps its own gate, note and apply group (#3465)", () => {
|
|
133
156
|
const member = { name: "network", dir: "infra/network", file: ".github/workflows/chant-pr.network.prod.yml" };
|
|
134
|
-
const scoped = generateGithubPipeline(ESTATE, { env: "prod", prLoop: {}, member });
|
|
157
|
+
const scoped = generateGithubPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true }, member });
|
|
135
158
|
const mdoc = parseYAML(scoped.yaml) as {
|
|
136
159
|
name: string;
|
|
137
160
|
on: Record<string, unknown>;
|
|
@@ -142,13 +165,14 @@ describe("the GitHub pull-request workflow", () => {
|
|
|
142
165
|
// No path filter: a change outside the member can reach it.
|
|
143
166
|
expect(mdoc.on).toEqual({ pull_request: { branches: ["main"] }, push: { branches: ["main"] } });
|
|
144
167
|
expect(mdoc.defaults).toEqual({ run: { "working-directory": "infra/network" } });
|
|
145
|
-
const plan = mdoc.jobs.plan.steps.find((s) => s.run?.includes("
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
expect(
|
|
168
|
+
const plan = mdoc.jobs.plan.steps.find((s) => s.run?.includes("ci run plan"))!;
|
|
169
|
+
// ci run finds the member from the directory it runs in and passes --member to pr-plan and pr-apply.
|
|
170
|
+
expect(plan.run).toBe('chant ci run plan --in-place --no-push --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --forge github');
|
|
171
|
+
expect(prLoopJobs("github", "prod", {}, "network")[0]!.command).toContain("--member network");
|
|
172
|
+
const apply = mdoc.jobs.apply.steps.find((s) => s.run?.includes("ci run apply"))!;
|
|
149
173
|
expect(mdoc.jobs.apply.concurrency).toEqual({ group: "chant-apply.network.prod", "cancel-in-progress": false });
|
|
150
174
|
// The record path is relative to the member for --resume and to the repository for the cache.
|
|
151
|
-
expect(apply.run).toContain("--
|
|
175
|
+
expect(apply.run).toContain("--in-place");
|
|
152
176
|
const restore = mdoc.jobs.apply.steps.find((s) => s.uses === "actions/cache/restore@v4")!;
|
|
153
177
|
expect(restore.with).toMatchObject({ path: "infra/network/.chant/pr-resume/pr-apply.json", "restore-keys": "chant-apply.network.prod-${{ github.sha }}-" });
|
|
154
178
|
for (const job of Object.values(mdoc.jobs)) {
|
|
@@ -168,11 +192,11 @@ describe("the GitHub pull-request workflow", () => {
|
|
|
168
192
|
});
|
|
169
193
|
|
|
170
194
|
test("a member at the workspace root keeps the root's paths but its own names", () => {
|
|
171
|
-
const yaml = generateGithubPipeline(ESTATE, { env: "prod", prLoop: {}, member: { name: "root", dir: "." } }).yaml;
|
|
195
|
+
const yaml = generateGithubPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true }, member: { name: "root", dir: "." } }).yaml;
|
|
172
196
|
const mdoc = parseYAML(yaml) as { name: string; defaults?: unknown; jobs: Record<string, Job> };
|
|
173
197
|
expect(mdoc.name).toBe("chant-pr.root.prod");
|
|
174
198
|
expect(mdoc.defaults).toBeUndefined();
|
|
175
|
-
expect(yaml).toContain("--
|
|
199
|
+
expect(yaml).toContain("chant ci run plan --in-place");
|
|
176
200
|
expect(yaml).toContain("path: .chant/pr");
|
|
177
201
|
});
|
|
178
202
|
|
|
@@ -189,7 +213,7 @@ describe("the GitHub pull-request workflow", () => {
|
|
|
189
213
|
});
|
|
190
214
|
|
|
191
215
|
describe("the github pull-request pipeline with build (#4088)", () => {
|
|
192
|
-
const result = generateGithubPipeline(ESTATE, { env: "prod", prLoop: { build: true } });
|
|
216
|
+
const result = generateGithubPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true, build: true } });
|
|
193
217
|
const doc = parseYAML(result.yaml) as Record<string, any>;
|
|
194
218
|
const golden = join(import.meta.dirname, "__fixtures__", "pr-loop-build.github.golden.yml");
|
|
195
219
|
|
|
@@ -207,14 +231,60 @@ describe("the github pull-request pipeline with build (#4088)", () => {
|
|
|
207
231
|
});
|
|
208
232
|
|
|
209
233
|
test("buildCommand replaces the build line and turns the option on", () => {
|
|
210
|
-
const yaml = generateGithubPipeline(ESTATE, { env: "prod", prLoop: { buildCommand: "npm run build" } }).yaml;
|
|
234
|
+
const yaml = generateGithubPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true, buildCommand: "npm run build" } }).yaml;
|
|
211
235
|
expect(yaml).toContain("npm run build");
|
|
212
236
|
expect(yaml).not.toContain("chant build --env");
|
|
213
237
|
expect(yaml).toContain("chant lint");
|
|
214
238
|
});
|
|
215
239
|
|
|
216
240
|
test("the default output carries neither line", () => {
|
|
217
|
-
const plain = generateGithubPipeline(ESTATE, { env: "prod", prLoop: {} }).yaml;
|
|
241
|
+
const plain = generateGithubPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true } }).yaml;
|
|
218
242
|
expect(plain).not.toContain("chant lint");
|
|
219
243
|
});
|
|
220
244
|
});
|
|
245
|
+
|
|
246
|
+
describe("the GitHub pull-request workflow records on merge by default (#4168)", () => {
|
|
247
|
+
const result = generateGithubPipeline(ESTATE, { env: "prod", prLoop: {} });
|
|
248
|
+
const doc = parseYAML(result.yaml) as { jobs: Record<string, Job> };
|
|
249
|
+
const golden = join(import.meta.dirname, "__fixtures__", "pr-loop-record.github.golden.yml");
|
|
250
|
+
|
|
251
|
+
test("matches its golden", () => {
|
|
252
|
+
if (process.env.UPDATE_GOLDEN) writeFileSync(golden, result.yaml);
|
|
253
|
+
expect(result.yaml).toBe(readFileSync(golden, "utf-8"));
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("a push runs the record job, which applies nothing and keeps no resume record", () => {
|
|
257
|
+
expect(result.jobs.map((j) => j.jobName)).toEqual(["plan", "record"]);
|
|
258
|
+
expect(doc.jobs.apply).toBeUndefined();
|
|
259
|
+
expect(doc.jobs.record.if).toBe("github.event_name == 'push'");
|
|
260
|
+
const run = doc.jobs.record.steps.find((s) => s.run?.includes("ci run record"))!;
|
|
261
|
+
expect(run.run!.trimEnd().split("\n")).toEqual([githubBaseFallback(), 'chant ci run record --in-place --base "$BASE_SHA" --env prod --gate pr-apply --forge github']);
|
|
262
|
+
expect(doc.jobs.record.steps.some((s) => s.uses?.startsWith("actions/cache"))).toBe(false);
|
|
263
|
+
// It writes the merge record and the change set to chant/lifecycle, one at a time per environment.
|
|
264
|
+
expect(doc.jobs.record.permissions.contents).toBe("write");
|
|
265
|
+
expect(doc.jobs.record.concurrency).toEqual({ group: "chant-record-prod", "cancel-in-progress": false });
|
|
266
|
+
expect(result.yaml).not.toContain("ci run apply");
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("a gate per wave gates the apply, so it needs applyOnMerge", () => {
|
|
270
|
+
expect(() => generateGithubPipeline(ESTATE, { env: "prod", prLoop: {}, gatedWaves: { gate: "release" } })).toThrow(/needs --apply-on-merge/);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("the record job runs pr-record with the apply's flags", () => {
|
|
274
|
+
const [, , record] = prLoopJobs("github", "prod", { events: true, reportUrl: "s3://r/acme", waveGate: "w", requireReview: true, branch: "trunk" });
|
|
275
|
+
expect(record.jobName).toBe("record");
|
|
276
|
+
expect(record.command).toBe('chant components pr-record --base "$BASE_SHA" --env prod --gate pr-apply --output .chant/pr --forge github --events-json --report-url "s3://r/acme"');
|
|
277
|
+
expect(record.run.command).toBe('chant ci run record --in-place --base "$BASE_SHA" --env prod --gate pr-apply --forge github --branch "trunk" --events-json --report-url "s3://r/acme"');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test.skipIf(!hasActionlint())("passes actionlint", () => {
|
|
281
|
+
const dir = mkdtempSync(join(tmpdir(), "chant-pr-record-"));
|
|
282
|
+
try {
|
|
283
|
+
mkdirSync(join(dir, ".github", "workflows"), { recursive: true });
|
|
284
|
+
writeFileSync(join(dir, ".github", "workflows", "chant-pr.yml"), result.yaml);
|
|
285
|
+
execFileSync("actionlint", [join(dir, ".github", "workflows", "chant-pr.yml")], { cwd: dir, stdio: "pipe" });
|
|
286
|
+
} finally {
|
|
287
|
+
rmSync(dir, { recursive: true, force: true });
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The GitHub pull-request workflow with a gate per wave on merge (#4003):
|
|
3
|
+
* `--pr-loop --wave-gate <gate>` keeps the plan job on the pull request and
|
|
4
|
+
* writes one apply job per wave of the component graph, each running
|
|
5
|
+
* `pr-apply --wave-gate <gate> --wave <k>` after the one before it. The whole
|
|
6
|
+
* file has a golden, and actionlint checks it when it is installed.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { execFileSync } from "node:child_process";
|
|
10
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { describe, expect, test } from "vitest";
|
|
14
|
+
import { parseYAML } from "@intentius/chant/yaml";
|
|
15
|
+
import type { DriverComponent } from "@intentius/chant/components/driver";
|
|
16
|
+
import { generateGithubPipeline } from "./generate-pipeline";
|
|
17
|
+
|
|
18
|
+
const root = (name: string, dependsOn: string[] = []): DriverComponent => ({
|
|
19
|
+
name,
|
|
20
|
+
dependsOn,
|
|
21
|
+
deploy: [{ phase: "Apply", steps: [{ kind: "terraform-apply", root: name }] }],
|
|
22
|
+
});
|
|
23
|
+
/** Three waves: net, then app, then web. */
|
|
24
|
+
const ESTATE: DriverComponent[] = [root("net"), root("app", ["net"]), root("web", ["app"])];
|
|
25
|
+
|
|
26
|
+
const GOLDEN = join(import.meta.dirname, "__fixtures__", "pr-loop-waves.github.golden.yml");
|
|
27
|
+
|
|
28
|
+
interface Step {
|
|
29
|
+
name?: string;
|
|
30
|
+
if?: string;
|
|
31
|
+
uses?: string;
|
|
32
|
+
run?: string;
|
|
33
|
+
env?: Record<string, string>;
|
|
34
|
+
with?: Record<string, unknown>;
|
|
35
|
+
}
|
|
36
|
+
interface Job {
|
|
37
|
+
if: string;
|
|
38
|
+
needs?: string[];
|
|
39
|
+
concurrency?: Record<string, unknown>;
|
|
40
|
+
permissions: Record<string, string>;
|
|
41
|
+
steps: Step[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function hasActionlint(): boolean {
|
|
45
|
+
try {
|
|
46
|
+
execFileSync("actionlint", ["-version"], { stdio: "ignore" });
|
|
47
|
+
return true;
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe("the GitHub pull-request workflow with a gate per wave (#4003)", () => {
|
|
54
|
+
const result = generateGithubPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true }, gatedWaves: { gate: "release" } });
|
|
55
|
+
const doc = parseYAML(result.yaml) as { jobs: Record<string, Job> };
|
|
56
|
+
|
|
57
|
+
test("matches its golden", () => {
|
|
58
|
+
if (process.env.UPDATE_GOLDEN) writeFileSync(GOLDEN, result.yaml);
|
|
59
|
+
expect(result.yaml).toBe(readFileSync(GOLDEN, "utf-8"));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("keeps the plan job, and writes one apply job per wave, each needing the one before", () => {
|
|
63
|
+
expect(Object.keys(doc.jobs)).toEqual(["plan", "apply-wave-1", "apply-wave-2", "apply-wave-3"]);
|
|
64
|
+
expect(result.jobs.map((j) => [j.jobName, j.needs])).toEqual([
|
|
65
|
+
["plan", []],
|
|
66
|
+
["apply-wave-1", []],
|
|
67
|
+
["apply-wave-2", ["apply-wave-1"]],
|
|
68
|
+
["apply-wave-3", ["apply-wave-2"]],
|
|
69
|
+
]);
|
|
70
|
+
expect(doc.jobs["apply-wave-2"]!.needs).toEqual(["apply-wave-1"]);
|
|
71
|
+
const plan = doc.jobs.plan!.steps.find((s) => s.run?.includes("ci run plan"))!;
|
|
72
|
+
// The plan passes the wave gate for its note (#4003), and runs no wave.
|
|
73
|
+
expect(plan.run).toContain("--forge github --wave-gate release");
|
|
74
|
+
expect(plan.run).not.toContain("--wave ");
|
|
75
|
+
for (const k of [1, 2, 3]) {
|
|
76
|
+
const job = doc.jobs[`apply-wave-${k}`]!;
|
|
77
|
+
expect(job.if).toBe("github.event_name == 'push'");
|
|
78
|
+
expect(job.concurrency).toEqual({ group: "chant-apply-prod", "cancel-in-progress": false });
|
|
79
|
+
expect(job.permissions.contents).toBe("write");
|
|
80
|
+
const run = job.steps.find((s) => s.run?.includes("ci run apply"))!.run!;
|
|
81
|
+
// #4156: through chant ci run, which runs pr-apply --wave-gate release (no --require-review since #4168) --resume ... --wave <k>.
|
|
82
|
+
expect(run).toContain(`chant ci run apply --in-place --base "$BASE_SHA" --env prod --gate pr-apply --forge github --wave-gate release --wave ${k}`);
|
|
83
|
+
expect(job.steps.find((s) => s.run?.includes("ci run apply"))!.env?.CHANT_RUNNER_KEY).toBe("${{ secrets.CHANT_RUNNER_KEY }}");
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("each wave restores the commit's newest record and saves its own, so wave k reads what wave k-1 left", () => {
|
|
88
|
+
for (const k of [1, 2, 3]) {
|
|
89
|
+
const steps = doc.jobs[`apply-wave-${k}`]!.steps;
|
|
90
|
+
const restore = steps.find((s) => s.uses === "actions/cache/restore@v4")!;
|
|
91
|
+
const save = steps.find((s) => s.uses === "actions/cache/save@v4")!;
|
|
92
|
+
const key = `chant-apply-prod-\${{ github.sha }}-\${{ github.run_id }}-\${{ github.run_attempt }}-wave-${k}`;
|
|
93
|
+
expect(restore.with).toEqual({ path: ".chant/pr-resume/pr-apply.json", key, "restore-keys": "chant-apply-prod-${{ github.sha }}-" });
|
|
94
|
+
expect(save).toMatchObject({ if: "always()", with: { path: ".chant/pr-resume/pr-apply.json", key } });
|
|
95
|
+
const keep = steps.find((s) => s.uses?.startsWith("actions/upload-artifact"))!;
|
|
96
|
+
expect(keep.with?.name).toBe(`chant-pr-apply-wave-${k}`);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("without --wave-gate the workflow is the one apply job it was", () => {
|
|
101
|
+
const plain = generateGithubPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true } });
|
|
102
|
+
expect(plain.jobs.map((j) => j.jobName)).toEqual(["plan", "apply"]);
|
|
103
|
+
expect(plain.yaml).not.toContain("--wave");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test.skipIf(!hasActionlint())("actionlint passes it", () => {
|
|
107
|
+
const tmp = mkdtempSync(join(tmpdir(), "pr-waves-actionlint-"));
|
|
108
|
+
try {
|
|
109
|
+
mkdirSync(join(tmp, ".github", "workflows"), { recursive: true });
|
|
110
|
+
const file = join(tmp, ".github", "workflows", "chant-pr.yml");
|
|
111
|
+
writeFileSync(file, result.yaml);
|
|
112
|
+
execFileSync("actionlint", [file], { cwd: tmp, stdio: "pipe" });
|
|
113
|
+
} finally {
|
|
114
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
});
|
package/src/lsp/completions.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createRequire } from "module";
|
|
2
|
+
import { moduleSourceUrl } from "@intentius/chant/module-source";
|
|
2
3
|
import type { CompletionContext, CompletionItem } from "@intentius/chant/lsp/types";
|
|
3
4
|
import { LexiconIndex, lexiconCompletions, type LexiconEntry } from "@intentius/chant/lsp/lexicon-providers";
|
|
4
|
-
const require = createRequire(import.meta.url);
|
|
5
|
+
const require = createRequire(moduleSourceUrl(import.meta.url));
|
|
5
6
|
|
|
6
7
|
let cachedIndex: LexiconIndex | null = null;
|
|
7
8
|
|
package/src/lsp/hover.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createRequire } from "module";
|
|
2
|
+
import { moduleSourceUrl } from "@intentius/chant/module-source";
|
|
2
3
|
import type { HoverContext, HoverInfo } from "@intentius/chant/lsp/types";
|
|
3
4
|
import { LexiconIndex, lexiconHover, type LexiconEntry } from "@intentius/chant/lsp/lexicon-providers";
|
|
4
|
-
const require = createRequire(import.meta.url);
|
|
5
|
+
const require = createRequire(moduleSourceUrl(import.meta.url));
|
|
5
6
|
|
|
6
7
|
let cachedIndex: LexiconIndex | null = null;
|
|
7
8
|
|