@intentius/chant-lexicon-gitlab 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/codegen/docs-cli.js +2 -0
- package/dist/codegen/docs.js +157 -0
- package/dist/codegen/fetch.js +93 -0
- package/dist/codegen/generate-cli.js +30 -0
- package/dist/codegen/generate-lexicon.js +36 -0
- package/dist/codegen/generate-typescript.js +105 -0
- package/dist/codegen/generate.js +125 -0
- package/dist/codegen/naming.js +48 -0
- package/dist/codegen/package.js +41 -0
- package/dist/codegen/parse.js +539 -0
- package/dist/codegen/patches.js +84 -0
- package/dist/codegen/typecheck.js +4 -0
- package/dist/components/ci-jobs.js +16 -0
- package/dist/components/generate-op-pipeline.d.ts.map +1 -1
- package/dist/components/generate-op-pipeline.js +294 -0
- package/dist/components/generate-op-waves-pipeline.js +125 -0
- package/dist/components/generate-pipeline.d.ts.map +1 -1
- package/dist/components/generate-pipeline.js +376 -0
- package/dist/composites/catalog.js +418 -0
- package/dist/composites/docker-build.js +38 -0
- package/dist/composites/index.js +5 -0
- package/dist/composites/mr-plan-report.js +42 -0
- package/dist/composites/node-pipeline.js +67 -0
- package/dist/composites/python-pipeline.js +45 -0
- package/dist/composites/review-app.js +32 -0
- package/dist/coverage.js +35 -0
- package/dist/generated/index.js +25 -0
- package/dist/generated/runtime.js +4 -0
- package/dist/graph-edges.js +202 -0
- package/dist/import/generator.js +196 -0
- package/dist/import/parser.js +111 -0
- package/dist/index.js +26 -0
- package/dist/integrity.json +3 -3
- package/dist/intrinsics.js +33 -0
- package/dist/lint/audit-catalog.js +53 -0
- package/dist/lint/audit-lineage.js +64 -0
- package/dist/lint/post-synth/index.js +84 -0
- package/dist/lint/post-synth/wgl010.js +34 -0
- package/dist/lint/post-synth/wgl011.js +48 -0
- package/dist/lint/post-synth/wgl012.d.ts.map +1 -1
- package/dist/lint/post-synth/wgl012.js +68 -0
- package/dist/lint/post-synth/wgl013.js +57 -0
- package/dist/lint/post-synth/wgl014.js +43 -0
- package/dist/lint/post-synth/wgl015.js +70 -0
- package/dist/lint/post-synth/wgl016.js +70 -0
- package/dist/lint/post-synth/wgl017.js +38 -0
- package/dist/lint/post-synth/wgl018.js +34 -0
- package/dist/lint/post-synth/wgl019.js +43 -0
- package/dist/lint/post-synth/wgl020.js +47 -0
- package/dist/lint/post-synth/wgl021.js +52 -0
- package/dist/lint/post-synth/wgl022.js +38 -0
- package/dist/lint/post-synth/wgl023.js +44 -0
- package/dist/lint/post-synth/wgl024.js +41 -0
- package/dist/lint/post-synth/wgl025.js +42 -0
- package/dist/lint/post-synth/wgl026.js +58 -0
- package/dist/lint/post-synth/wgl027.js +49 -0
- package/dist/lint/post-synth/wgl028.js +58 -0
- package/dist/lint/post-synth/wgl029.js +55 -0
- package/dist/lint/post-synth/wgl030.js +43 -0
- package/dist/lint/post-synth/wgl031.js +33 -0
- package/dist/lint/post-synth/wgl032.js +64 -0
- package/dist/lint/post-synth/wgl033.js +32 -0
- package/dist/lint/post-synth/wgl034.js +37 -0
- package/dist/lint/post-synth/wgl035.js +40 -0
- package/dist/lint/post-synth/wgl036.js +38 -0
- package/dist/lint/post-synth/wgl037.js +41 -0
- package/dist/lint/post-synth/wgl038.js +43 -0
- package/dist/lint/post-synth/wgl039.js +34 -0
- package/dist/lint/post-synth/wgl040.js +35 -0
- package/dist/lint/post-synth/wgl041.js +53 -0
- package/dist/lint/post-synth/wgl042.js +41 -0
- package/dist/lint/post-synth/wgl043.js +39 -0
- package/dist/lint/post-synth/wgl044.js +35 -0
- package/dist/lint/post-synth/wgl045.js +54 -0
- package/dist/lint/post-synth/wgl046.js +40 -0
- package/dist/lint/post-synth/wgl047.js +34 -0
- package/dist/lint/post-synth/wgl048.js +32 -0
- package/dist/lint/post-synth/wgl049.js +53 -0
- package/dist/lint/post-synth/wgl050.js +42 -0
- package/dist/lint/post-synth/yaml-helpers.js +432 -0
- package/dist/lint/rules/artifact-no-expiry.js +53 -0
- package/dist/lint/rules/data/known-include-sources.js +44 -0
- package/dist/lint/rules/data/untrusted-variables.js +30 -0
- package/dist/lint/rules/deprecated-only-except.js +46 -0
- package/dist/lint/rules/import-source.js +61 -0
- package/dist/lint/rules/index.js +7 -0
- package/dist/lint/rules/missing-script.js +67 -0
- package/dist/lint/rules/missing-stage.js +60 -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 +262 -0
- package/dist/migrate/from-github/actions/index.js +17 -0
- package/dist/migrate/from-github/actions/registry.js +61 -0
- package/dist/migrate/from-github/actions/tier-1.js +298 -0
- package/dist/migrate/from-github/actions/tier-2.js +241 -0
- package/dist/migrate/from-github/actions/tier-3.js +96 -0
- package/dist/migrate/from-github/composites/patterns.js +151 -0
- package/dist/migrate/from-github/composites/rewriter.js +22 -0
- package/dist/migrate/from-github/diagnostics.js +53 -0
- package/dist/migrate/from-github/emit-yaml.js +121 -0
- package/dist/migrate/from-github/expressions.js +275 -0
- package/dist/migrate/from-github/index.js +96 -0
- package/dist/migrate/from-github/provenance.js +35 -0
- package/dist/migrate/from-github/rules.js +74 -0
- package/dist/migrate/from-github/security.js +189 -0
- package/dist/migrate/from-github/stages.js +156 -0
- package/dist/migrate/from-github/transformer.js +633 -0
- package/dist/op/activities/gitlab.js +38 -0
- package/dist/op/activities/index.js +10 -0
- package/dist/op/builders.js +26 -0
- package/dist/package-cli.js +21 -0
- package/dist/plugin.js +536 -0
- package/dist/rules/wgl012.ts +2 -2
- package/dist/serializer.js +197 -0
- package/dist/testdata/create-fixture.js +40 -0
- package/dist/testdata/load-fixtures.js +20 -0
- package/dist/validate-cli.js +16 -0
- package/dist/validate.js +29 -0
- package/dist/variables.js +30 -0
- package/package.json +8 -8
- package/src/codegen/fetch.ts +2 -2
- package/src/codegen/package.ts +2 -2
- package/src/components/__fixtures__/pr-loop-build.gitlab.golden.yml +3 -3
- package/src/components/__fixtures__/pr-loop-record.gitlab.golden.yml +46 -0
- package/src/components/__fixtures__/pr-loop-waves.gitlab.golden.yml +101 -0
- package/src/components/__fixtures__/pr-loop.gitlab.golden.yml +3 -3
- package/src/components/generate-op-pipeline.test.ts +10 -0
- package/src/components/generate-op-pipeline.ts +8 -1
- package/src/components/generate-pipeline.ts +71 -30
- package/src/components/generate-pr-pipeline.test.ts +61 -18
- package/src/components/generate-pr-waves-pipeline.test.ts +88 -0
- package/src/lint/post-synth/wgl012.ts +2 -2
- package/src/lsp/completions.ts +2 -1
- package/src/lsp/hover.ts +2 -1
|
@@ -44,7 +44,18 @@ import {
|
|
|
44
44
|
import { resolveComponentGraph, type DriverComponent } from "@intentius/chant/components/driver";
|
|
45
45
|
import { hasPublishStep, promoteArchivePaths } from "@intentius/chant/components/promote";
|
|
46
46
|
import { GATED_WAVE_RECORD, gatedWaveJobs } from "@intentius/chant/components/gated-wave-pipeline";
|
|
47
|
-
import {
|
|
47
|
+
import {
|
|
48
|
+
PR_APPLY_RECORD,
|
|
49
|
+
PR_LOOP_REPORT_DIR,
|
|
50
|
+
prApplyGroup,
|
|
51
|
+
prRecordGroup,
|
|
52
|
+
prApplyRecordKey,
|
|
53
|
+
prApplyWaveJobName,
|
|
54
|
+
prLoopJobs,
|
|
55
|
+
prLoopSetup,
|
|
56
|
+
prLoopWaveJobs,
|
|
57
|
+
prLoopWithWaves,
|
|
58
|
+
} from "@intentius/chant/components/pr-pipeline";
|
|
48
59
|
import { memberGitlabChanges, memberRepoPath, memberShellDir } from "@intentius/chant/lexicon";
|
|
49
60
|
import type {
|
|
50
61
|
ComponentPipelineJob as GeneratedJob,
|
|
@@ -79,10 +90,10 @@ export function generateGitlabPipeline(
|
|
|
79
90
|
const byName = new Map(components.map((c) => [c.name, c]));
|
|
80
91
|
|
|
81
92
|
if (options.prLoop) {
|
|
82
|
-
if (options.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return prLoopGitlabPipeline(env, image, beforeScript, extraScript, options);
|
|
93
|
+
if (options.promoteTo !== undefined) throw new Error("a pull-request pipeline has no promote job; drop --promote-to or --pr-loop");
|
|
94
|
+
// #4003: --wave-gate beside --pr-loop gives the apply a gate per wave.
|
|
95
|
+
const prLoop = prLoopWithWaves(options.prLoop, options.gatedWaves);
|
|
96
|
+
return prLoopGitlabPipeline(components, env, image, beforeScript, extraScript, { ...options, prLoop });
|
|
86
97
|
}
|
|
87
98
|
|
|
88
99
|
if (options.gatedWaves) {
|
|
@@ -262,8 +273,8 @@ function gatedWaveGitlabPipeline(
|
|
|
262
273
|
|
|
263
274
|
/**
|
|
264
275
|
* The merge-request pipeline (#3183): `plan` in each merge request pipeline,
|
|
265
|
-
*
|
|
266
|
-
* environment (`resource_group`). `GIT_DEPTH: "0"` gives both the history
|
|
276
|
+
* and on each push to the target branch `record` (#4168), or `apply` with
|
|
277
|
+
* `applyOnMerge`, one at a time per environment (`resource_group`). `GIT_DEPTH: "0"` gives both the history
|
|
267
278
|
* they measure the change with. The jobs talk to GitLab with
|
|
268
279
|
* `CHANT_FORGE_TOKEN`, a project or group access token with the `api` scope
|
|
269
280
|
* set as a masked CI/CD variable, since a job token cannot write notes.
|
|
@@ -282,8 +293,15 @@ function gatedWaveGitlabPipeline(
|
|
|
282
293
|
* apply passes it to `pr-apply --resume` (#3543). GitLab keeps protected
|
|
283
294
|
* branches' caches apart from other branches' by default, so a merge request
|
|
284
295
|
* pipeline cannot write the record the target branch's apply reads.
|
|
296
|
+
*
|
|
297
|
+
* With a gate per wave (`waveGate`, #4003) the apply job becomes
|
|
298
|
+
* `apply-wave-1` to `apply-wave-<n>`, one stage each, each needing the one
|
|
299
|
+
* before. They share the commit's cache key, so wave k pulls the record wave
|
|
300
|
+
* k-1 pushed, and a retry of a wave that stopped at its gate pulls the one it
|
|
301
|
+
* pushed itself.
|
|
285
302
|
*/
|
|
286
303
|
function prLoopGitlabPipeline(
|
|
304
|
+
components: DriverComponent[],
|
|
287
305
|
env: string,
|
|
288
306
|
image: string,
|
|
289
307
|
beforeScript: string[],
|
|
@@ -293,17 +311,24 @@ function prLoopGitlabPipeline(
|
|
|
293
311
|
const loop = options.prLoop!;
|
|
294
312
|
const member = options.member;
|
|
295
313
|
const rooted = !member || member.dir === "." || member.dir === "";
|
|
296
|
-
const [plan, apply] = prLoopJobs("gitlab", env, loop, member?.name);
|
|
314
|
+
const [plan, apply, record] = prLoopJobs("gitlab", env, loop, member?.name);
|
|
297
315
|
const reportPath = member ? memberRepoPath(member, PR_LOOP_REPORT_DIR) : PR_LOOP_REPORT_DIR;
|
|
298
316
|
const planJob = member ? `${member.name}-plan` : "plan";
|
|
299
|
-
const
|
|
317
|
+
const named = (job: string) => (member ? `${member.name}-${job}` : job);
|
|
318
|
+
const waveJobs = loop.waveGate ? prLoopWaveJobs(components, apply) : [];
|
|
319
|
+
const applies = loop.waveGate
|
|
320
|
+
? waveJobs.map((job) => ({ job, name: named(prApplyWaveJobName(job.wave!)), stage: prApplyWaveJobName(job.wave!) }))
|
|
321
|
+
: loop.applyOnMerge
|
|
322
|
+
? [{ job: apply, name: named("apply"), stage: "apply" }]
|
|
323
|
+
: [{ job: record, name: named("record"), stage: "record" }];
|
|
300
324
|
const onMergeRequest = GITLAB_MERGE_REQUEST_SOURCE;
|
|
301
325
|
if (loop.branch !== undefined) assertGitlabBranch("The merge-request pipeline", loop.branch);
|
|
302
326
|
const onMerge = gitlabPushRule(loop.branch ? JSON.stringify(loop.branch) : "$CI_DEFAULT_BRANCH");
|
|
303
327
|
// A member's jobs start in its directory, after the image's install lines.
|
|
304
328
|
const enter = rooted ? [] : [`cd ${memberShellDir(member!)}`];
|
|
305
|
-
|
|
306
|
-
const
|
|
329
|
+
// The command runs through chant ci run (#4156), which writes the job's run record, signed when the CHANT_RUNNER_KEY variable is set.
|
|
330
|
+
const script = (job: typeof plan): string[] => [...prLoopSetup(image), ...enter, ...beforeScript, ...(job.setup ?? []), ...(job.build ?? []), job.run.command, ...extraScript];
|
|
331
|
+
const stages = ["plan", ...applies.map((a) => a.stage)];
|
|
307
332
|
const doc: Record<string, unknown> = {
|
|
308
333
|
// Included files merge their top-level keys, so a member's file carries
|
|
309
334
|
// the rules every member shares and no name: two names would collide.
|
|
@@ -314,32 +339,48 @@ function prLoopGitlabPipeline(
|
|
|
314
339
|
stage: "plan",
|
|
315
340
|
image,
|
|
316
341
|
rules: [{ if: onMergeRequest }],
|
|
317
|
-
variables: plan.env,
|
|
342
|
+
variables: plan.run.env,
|
|
318
343
|
script: script(plan),
|
|
319
344
|
artifacts: { when: "always", paths: [reportPath] },
|
|
320
345
|
},
|
|
321
|
-
|
|
322
|
-
stage
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
346
|
+
...Object.fromEntries(
|
|
347
|
+
applies.map(({ job, name, stage }, i) => [
|
|
348
|
+
name,
|
|
349
|
+
{
|
|
350
|
+
stage,
|
|
351
|
+
image,
|
|
352
|
+
...(i > 0 ? { needs: [applies[i - 1]!.name] } : {}),
|
|
353
|
+
resource_group: job.jobName === "record" ? prRecordGroup(env, member?.name) : prApplyGroup(env, member?.name),
|
|
354
|
+
rules: [{ if: onMerge }],
|
|
355
|
+
variables: job.run.env,
|
|
356
|
+
script: script(job),
|
|
357
|
+
// The attempt record (#3543), kept under the pushed commit even when
|
|
358
|
+
// the job fails, so a retry finishes the apply under the same approval.
|
|
359
|
+
// The record job (#4168) applies nothing and keeps none.
|
|
360
|
+
...(job.jobName === "record"
|
|
361
|
+
? {}
|
|
362
|
+
: {
|
|
363
|
+
cache: {
|
|
364
|
+
key: prApplyRecordKey(env, "$CI_COMMIT_SHA", member?.name),
|
|
365
|
+
paths: [member ? memberRepoPath(member, PR_APPLY_RECORD) : PR_APPLY_RECORD],
|
|
366
|
+
when: "always",
|
|
367
|
+
},
|
|
368
|
+
}),
|
|
369
|
+
artifacts: { when: "always", paths: [reportPath] },
|
|
370
|
+
},
|
|
371
|
+
]),
|
|
372
|
+
),
|
|
337
373
|
};
|
|
338
374
|
const jobs: GeneratedJob[] = [
|
|
339
375
|
{ jobName: planJob, component: "merge request plan", stage: "plan", needs: [] },
|
|
340
|
-
{
|
|
376
|
+
...applies.map(({ job, name, stage }, i) => ({
|
|
377
|
+
jobName: name,
|
|
378
|
+
component: job.wave ? `merge request apply, wave ${job.wave}` : job.jobName === "record" ? "merge record" : "merge request apply",
|
|
379
|
+
stage,
|
|
380
|
+
needs: i > 0 ? [applies[i - 1]!.name] : [],
|
|
381
|
+
})),
|
|
341
382
|
];
|
|
342
|
-
const yaml = emitGitlabFile(["workflow", "stages", "variables", planJob,
|
|
383
|
+
const yaml = emitGitlabFile(["workflow", "stages", "variables", planJob, ...applies.map((a) => a.name)].map((key) => [key, doc[key]] as const));
|
|
343
384
|
return { yaml, stages, jobs, env };
|
|
344
385
|
}
|
|
345
386
|
|
|
@@ -12,7 +12,7 @@ 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
14
|
import { generateGitlabPipeline } from "./generate-pipeline";
|
|
15
|
-
import { PR_LOOP_SETUP } from "@intentius/chant/components/pr-pipeline";
|
|
15
|
+
import { PR_LOOP_SETUP, prLoopJobs } from "@intentius/chant/components/pr-pipeline";
|
|
16
16
|
|
|
17
17
|
const ESTATE: DriverComponent[] = [
|
|
18
18
|
{ name: "net", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "terraform-apply", root: "net" }] }] },
|
|
@@ -33,7 +33,7 @@ interface Job {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
describe("the GitLab merge-request pipeline", () => {
|
|
36
|
-
const result = generateGitlabPipeline(ESTATE, { env: "prod", prLoop: {} });
|
|
36
|
+
const result = generateGitlabPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true } });
|
|
37
37
|
const doc = parseYAML(result.yaml) as { workflow: { rules: unknown[] }; variables: Record<string, string>; plan: Job; apply: Job };
|
|
38
38
|
|
|
39
39
|
test("matches its golden", () => {
|
|
@@ -54,17 +54,19 @@ describe("the GitLab merge-request pipeline", () => {
|
|
|
54
54
|
expect(doc.plan.rules).toEqual([{ if: '$CI_PIPELINE_SOURCE == "merge_request_event"' }]);
|
|
55
55
|
expect(doc.plan.variables).toEqual({ BASE_SHA: "$CI_MERGE_REQUEST_DIFF_BASE_SHA", PR_NUMBER: "$CI_MERGE_REQUEST_IID" });
|
|
56
56
|
expect(doc.plan.script).toEqual([
|
|
57
|
-
...PR_LOOP_SETUP,'chant
|
|
57
|
+
...PR_LOOP_SETUP,'chant ci run plan --in-place --no-push --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --forge gitlab']);
|
|
58
58
|
expect(doc.apply.rules).toEqual([{ if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' }]);
|
|
59
59
|
expect(doc.apply.variables).toEqual({ BASE_SHA: "$CI_COMMIT_BEFORE_SHA" });
|
|
60
|
-
|
|
60
|
+
// #4156: ci run builds pr-apply (with no --require-review since #4168), and GitLab hands CHANT_RUNNER_KEY to the job as a CI/CD variable when it has one.
|
|
61
|
+
expect(doc.apply.script.at(-1)).toBe('chant ci run apply --in-place --base "$BASE_SHA" --env prod --gate pr-apply --forge gitlab');
|
|
62
|
+
expect(result.yaml).not.toContain("CHANT_RUNNER_KEY");
|
|
61
63
|
expect(doc.apply.resource_group).toBe("chant-apply-prod");
|
|
62
64
|
expect(doc.variables.GIT_DEPTH).toBe("0");
|
|
63
65
|
expect(doc.apply.artifacts).toEqual({ when: "always", paths: [".chant/pr"] });
|
|
64
66
|
});
|
|
65
67
|
|
|
66
68
|
test("events and reportUrl stream the run's events into the report artifact and copy the reports out (#4007)", () => {
|
|
67
|
-
const loud = parseYAML(generateGitlabPipeline(ESTATE, { env: "prod", prLoop: { events: true, reportUrl: "$CHANT_REPORT_URL" } }).yaml) as { plan: Job; apply: Job };
|
|
69
|
+
const loud = parseYAML(generateGitlabPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true, events: true, reportUrl: "$CHANT_REPORT_URL" } }).yaml) as { plan: Job; apply: Job };
|
|
68
70
|
for (const job of [loud.plan, loud.apply]) {
|
|
69
71
|
expect(job.script.at(-1)).toContain('--events-json --report-url "$CHANT_REPORT_URL"');
|
|
70
72
|
expect(job.variables.CHANT_RUN_EVENTS_DIR).toBe(".chant/pr/run-events");
|
|
@@ -76,7 +78,7 @@ describe("the GitLab merge-request pipeline", () => {
|
|
|
76
78
|
test("runs in an image with git and installs OpenTofu, unless the caller names an image", () => {
|
|
77
79
|
expect(doc.plan.image).toBe("node:22");
|
|
78
80
|
expect(doc.apply.script[0]).toBe(PR_LOOP_SETUP[0]);
|
|
79
|
-
const own = generateGitlabPipeline(ESTATE, { env: "prod", image: "example/ci:1", prLoop: {} }).yaml;
|
|
81
|
+
const own = generateGitlabPipeline(ESTATE, { env: "prod", image: "example/ci:1", prLoop: { applyOnMerge: true } }).yaml;
|
|
80
82
|
expect(own).toContain("image: example/ci:1");
|
|
81
83
|
expect(own).not.toContain("install-opentofu");
|
|
82
84
|
});
|
|
@@ -90,25 +92,27 @@ describe("the GitLab merge-request pipeline", () => {
|
|
|
90
92
|
});
|
|
91
93
|
|
|
92
94
|
test("a named branch replaces the default branch in the apply rule", () => {
|
|
93
|
-
const yaml = generateGitlabPipeline(ESTATE, { env: "prod", prLoop: { branch: "trunk" } }).yaml;
|
|
95
|
+
const yaml = generateGitlabPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true, branch: "trunk" } }).yaml;
|
|
94
96
|
expect(yaml).toContain('$CI_COMMIT_BRANCH == "trunk"');
|
|
95
97
|
});
|
|
96
98
|
|
|
97
99
|
test("the apply resumes from a record the job cache keeps for the pushed commit, failed or not (#3543)", () => {
|
|
98
|
-
expect(doc.apply.script.at(-1)).toContain("--
|
|
100
|
+
expect(doc.apply.script.at(-1)).toContain("--in-place");
|
|
101
|
+
expect(prLoopJobs("gitlab", "prod")[1]!.command).toContain("--resume .chant/pr-resume/pr-apply.json");
|
|
99
102
|
expect(doc.apply.cache).toEqual({ key: "chant-apply-prod-$CI_COMMIT_SHA", paths: [".chant/pr-resume/pr-apply.json"], when: "always" });
|
|
100
103
|
// The plan runs the merge request's code: no cache, no record.
|
|
101
104
|
expect(doc.plan.cache).toBeUndefined();
|
|
102
105
|
expect(doc.plan.script.at(-1)).not.toContain("--resume");
|
|
103
106
|
});
|
|
104
107
|
|
|
105
|
-
test("refuses a
|
|
106
|
-
expect(() => generateGitlabPipeline(ESTATE, { prLoop: {
|
|
108
|
+
test("refuses a promote job beside it, and a canary beside --wave-gate (#4003)", () => {
|
|
109
|
+
expect(() => generateGitlabPipeline(ESTATE, { prLoop: { applyOnMerge: true }, promoteTo: "prod" })).toThrow(/pull-request pipeline has no promote job/);
|
|
110
|
+
expect(() => generateGitlabPipeline(ESTATE, { prLoop: { applyOnMerge: true }, gatedWaves: { gate: "g", canary: ["net"] } })).toThrow(/takes no --canary/);
|
|
107
111
|
});
|
|
108
112
|
|
|
109
113
|
test("inside a workspace member: member-named jobs that cd into the member, with no changes rule (#3465)", () => {
|
|
110
114
|
const member = { name: "network", dir: "infra/network", file: ".gitlab/ci/chant-pr.network.prod.gitlab-ci.yml" };
|
|
111
|
-
const scoped = generateGitlabPipeline(ESTATE, { env: "prod", prLoop: {}, member });
|
|
115
|
+
const scoped = generateGitlabPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true }, member });
|
|
112
116
|
const mdoc = parseYAML(scoped.yaml) as Record<string, unknown> & { workflow: { name?: string; rules: unknown[] } };
|
|
113
117
|
expect(scoped.jobs.map((j) => j.jobName)).toEqual(["network-plan", "network-apply"]);
|
|
114
118
|
expect(mdoc.plan).toBeUndefined();
|
|
@@ -118,9 +122,10 @@ describe("the GitLab merge-request pipeline", () => {
|
|
|
118
122
|
const plan = mdoc["network-plan"] as Job;
|
|
119
123
|
const apply = mdoc["network-apply"] as Job;
|
|
120
124
|
expect(plan.script).toContain("cd infra/network");
|
|
121
|
-
|
|
122
|
-
expect(plan.script.
|
|
123
|
-
expect(apply.script.
|
|
125
|
+
// ci run, started in the member's directory, finds the member and passes --member to pr-plan and pr-apply.
|
|
126
|
+
expect(plan.script.indexOf("cd infra/network")).toBeLessThan(plan.script.findIndex((l) => l.includes("ci run plan")));
|
|
127
|
+
expect(apply.script.indexOf("cd infra/network")).toBeLessThan(apply.script.findIndex((l) => l.includes("ci run apply")));
|
|
128
|
+
expect(prLoopJobs("gitlab", "prod", {}, "network")[0]!.command).toContain("--member network");
|
|
124
129
|
// A change outside the member can reach it, so the jobs keep only their pipeline-source rules.
|
|
125
130
|
expect(plan.rules).toEqual([{ if: '$CI_PIPELINE_SOURCE == "merge_request_event"' }]);
|
|
126
131
|
expect(apply.resource_group).toBe("chant-apply.network.prod");
|
|
@@ -132,12 +137,12 @@ describe("the GitLab merge-request pipeline", () => {
|
|
|
132
137
|
|
|
133
138
|
describe("the merge-request pipeline's branch (#3765)", () => {
|
|
134
139
|
test("a branch that would break the rules: expression is refused, as an Op's is", () => {
|
|
135
|
-
expect(() => generateGitlabPipeline(ESTATE, { env: "prod", prLoop: { branch: "re$lease" } })).toThrow(/filters on branch "re\$lease"/);
|
|
140
|
+
expect(() => generateGitlabPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true, branch: "re$lease" } })).toThrow(/filters on branch "re\$lease"/);
|
|
136
141
|
});
|
|
137
142
|
});
|
|
138
143
|
|
|
139
144
|
describe("the gitlab pull-request pipeline with build (#4088)", () => {
|
|
140
|
-
const result = generateGitlabPipeline(ESTATE, { env: "prod", prLoop: { build: true } });
|
|
145
|
+
const result = generateGitlabPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true, build: true } });
|
|
141
146
|
const doc = parseYAML(result.yaml) as Record<string, any>;
|
|
142
147
|
const golden = join(import.meta.dirname, "__fixtures__", "pr-loop-build.gitlab.golden.yml");
|
|
143
148
|
|
|
@@ -156,14 +161,52 @@ describe("the gitlab pull-request pipeline with build (#4088)", () => {
|
|
|
156
161
|
});
|
|
157
162
|
|
|
158
163
|
test("buildCommand replaces the build line and turns the option on", () => {
|
|
159
|
-
const yaml = generateGitlabPipeline(ESTATE, { env: "prod", prLoop: { buildCommand: "npm run build" } }).yaml;
|
|
164
|
+
const yaml = generateGitlabPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true, buildCommand: "npm run build" } }).yaml;
|
|
160
165
|
expect(yaml).toContain("npm run build");
|
|
161
166
|
expect(yaml).not.toContain("chant build --env");
|
|
162
167
|
expect(yaml).toContain("chant lint");
|
|
163
168
|
});
|
|
164
169
|
|
|
165
170
|
test("the default output carries neither line", () => {
|
|
166
|
-
const plain = generateGitlabPipeline(ESTATE, { env: "prod", prLoop: {} }).yaml;
|
|
171
|
+
const plain = generateGitlabPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true } }).yaml;
|
|
167
172
|
expect(plain).not.toContain("chant lint");
|
|
168
173
|
});
|
|
169
174
|
});
|
|
175
|
+
|
|
176
|
+
describe("the GitLab merge-request pipeline records on merge by default (#4168)", () => {
|
|
177
|
+
const result = generateGitlabPipeline(ESTATE, { env: "prod", prLoop: {} });
|
|
178
|
+
const doc = parseYAML(result.yaml) as Record<string, unknown> & { stages: string[]; record: Job };
|
|
179
|
+
const golden = join(import.meta.dirname, "__fixtures__", "pr-loop-record.gitlab.golden.yml");
|
|
180
|
+
|
|
181
|
+
test("matches its golden", () => {
|
|
182
|
+
if (process.env.UPDATE_GOLDEN) writeFileSync(golden, result.yaml);
|
|
183
|
+
expect(result.yaml).toBe(readFileSync(golden, "utf-8"));
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("validates against the vendored GitLab CI schema", () => {
|
|
187
|
+
const req = createRequire(import.meta.url);
|
|
188
|
+
const mod = req("ajv") as { default?: unknown };
|
|
189
|
+
const Ajv = (mod.default ?? mod) as new (opts: object) => { compile(s: object): ((d: unknown) => boolean) & { errors?: unknown } };
|
|
190
|
+
const validate = new Ajv({ strict: false, allErrors: true }).compile(JSON.parse(readFileSync(SCHEMA, "utf-8")));
|
|
191
|
+
expect(validate(doc), JSON.stringify(validate.errors, null, 2)).toBe(true);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("a push to the default branch runs the record job, which applies nothing and keeps no cache", () => {
|
|
195
|
+
expect(doc.stages).toEqual(["plan", "record"]);
|
|
196
|
+
expect(doc.apply).toBeUndefined();
|
|
197
|
+
expect(doc.record.rules).toEqual([{ if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' }]);
|
|
198
|
+
expect(doc.record.script.at(-1)).toBe('chant ci run record --in-place --base "$BASE_SHA" --env prod --gate pr-apply --forge gitlab');
|
|
199
|
+
expect(doc.record.resource_group).toBe("chant-record-prod");
|
|
200
|
+
expect(doc.record.cache).toBeUndefined();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("a gate per wave gates the apply, so it needs applyOnMerge", () => {
|
|
204
|
+
expect(() => generateGitlabPipeline(ESTATE, { env: "prod", prLoop: {}, gatedWaves: { gate: "release" } })).toThrow(/needs --apply-on-merge/);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("inside a workspace member the record job takes the member's name and group", () => {
|
|
208
|
+
const scoped = parseYAML(generateGitlabPipeline(ESTATE, { env: "prod", prLoop: {}, member: { name: "network", dir: "infra/network" } }).yaml) as Record<string, Job>;
|
|
209
|
+
expect(scoped["network-record"].resource_group).toBe("chant-record.network.prod");
|
|
210
|
+
expect(scoped["network-record"].script.at(-1)).toContain("chant ci run record");
|
|
211
|
+
});
|
|
212
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The GitLab merge-request pipeline with a gate per wave on merge (#4003):
|
|
3
|
+
* the plan job in each merge request pipeline, and one apply job per wave of
|
|
4
|
+
* the component graph, one stage each, sharing the commit's record in the
|
|
5
|
+
* job cache. The whole file has a golden, and it validates against the GitLab
|
|
6
|
+
* CI schema this lexicon vendors.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { createRequire } from "node:module";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { describe, expect, test } from "vitest";
|
|
13
|
+
import { parseYAML } from "@intentius/chant/yaml";
|
|
14
|
+
import type { DriverComponent } from "@intentius/chant/components/driver";
|
|
15
|
+
import { generateGitlabPipeline } from "./generate-pipeline";
|
|
16
|
+
|
|
17
|
+
const root = (name: string, dependsOn: string[] = []): DriverComponent => ({
|
|
18
|
+
name,
|
|
19
|
+
dependsOn,
|
|
20
|
+
deploy: [{ phase: "Apply", steps: [{ kind: "terraform-apply", root: name }] }],
|
|
21
|
+
});
|
|
22
|
+
const ESTATE: DriverComponent[] = [root("net"), root("app", ["net"]), root("web", ["app"])];
|
|
23
|
+
|
|
24
|
+
const GOLDEN = join(import.meta.dirname, "__fixtures__", "pr-loop-waves.gitlab.golden.yml");
|
|
25
|
+
const SCHEMA = join(import.meta.dirname, "..", "codegen", "vendored", "gitlab-ci-schema.json");
|
|
26
|
+
|
|
27
|
+
interface Job {
|
|
28
|
+
stage: string;
|
|
29
|
+
needs?: string[];
|
|
30
|
+
rules: Array<{ if: string }>;
|
|
31
|
+
script: string[];
|
|
32
|
+
resource_group?: string;
|
|
33
|
+
cache?: Record<string, unknown>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe("the GitLab merge-request pipeline with a gate per wave (#4003)", () => {
|
|
37
|
+
const result = generateGitlabPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true }, gatedWaves: { gate: "release" } });
|
|
38
|
+
const doc = parseYAML(result.yaml) as Record<string, unknown> & { stages: string[] };
|
|
39
|
+
|
|
40
|
+
test("matches its golden", () => {
|
|
41
|
+
if (process.env.UPDATE_GOLDEN) writeFileSync(GOLDEN, result.yaml);
|
|
42
|
+
expect(result.yaml).toBe(readFileSync(GOLDEN, "utf-8"));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("validates against the vendored GitLab CI schema", () => {
|
|
46
|
+
const req = createRequire(import.meta.url);
|
|
47
|
+
const mod = req("ajv") as { default?: unknown };
|
|
48
|
+
const Ajv = (mod.default ?? mod) as new (opts: object) => { compile(s: object): ((d: unknown) => boolean) & { errors?: unknown } };
|
|
49
|
+
const validate = new Ajv({ strict: false, allErrors: true }).compile(JSON.parse(readFileSync(SCHEMA, "utf-8")));
|
|
50
|
+
expect(validate(doc), JSON.stringify(validate.errors, null, 2)).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("one apply job and stage per wave, each needing the one before, on the merge, one at a time", () => {
|
|
54
|
+
expect(doc.stages).toEqual(["plan", "apply-wave-1", "apply-wave-2", "apply-wave-3"]);
|
|
55
|
+
expect(result.jobs.map((j) => [j.jobName, j.needs])).toEqual([
|
|
56
|
+
["plan", []],
|
|
57
|
+
["apply-wave-1", []],
|
|
58
|
+
["apply-wave-2", ["apply-wave-1"]],
|
|
59
|
+
["apply-wave-3", ["apply-wave-2"]],
|
|
60
|
+
]);
|
|
61
|
+
for (const k of [1, 2, 3]) {
|
|
62
|
+
const job = doc[`apply-wave-${k}`] as Job;
|
|
63
|
+
expect(job.stage).toBe(`apply-wave-${k}`);
|
|
64
|
+
expect(job.needs).toEqual(k > 1 ? [`apply-wave-${k - 1}`] : undefined);
|
|
65
|
+
expect(job.resource_group).toBe("chant-apply-prod");
|
|
66
|
+
// #4156: through chant ci run, which runs pr-apply --wave-gate release (no --require-review since #4168) --resume ... --wave <k>.
|
|
67
|
+
expect(job.script.at(-1)).toBe(`chant ci run apply --in-place --base "$BASE_SHA" --env prod --gate pr-apply --forge gitlab --wave-gate release --wave ${k}`);
|
|
68
|
+
// Every wave shares the commit's record, so wave k pulls what wave k-1 pushed.
|
|
69
|
+
expect(job.cache).toEqual({ key: "chant-apply-prod-$CI_COMMIT_SHA", paths: [".chant/pr-resume/pr-apply.json"], when: "always" });
|
|
70
|
+
}
|
|
71
|
+
expect((doc.plan as Job).script.at(-1)).toContain("--forge gitlab --wave-gate release");
|
|
72
|
+
expect((doc.plan as Job).script.at(-1)).not.toContain("--wave ");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("inside a workspace member the wave jobs carry the member's name", () => {
|
|
76
|
+
const member = { name: "network", dir: "infra/network", file: ".gitlab/ci/chant-pr.network.prod.gitlab-ci.yml" };
|
|
77
|
+
const scoped = generateGitlabPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true }, gatedWaves: { gate: "release" }, member });
|
|
78
|
+
expect(scoped.jobs.map((j) => [j.jobName, j.needs])).toEqual([
|
|
79
|
+
["network-plan", []],
|
|
80
|
+
["network-apply-wave-1", []],
|
|
81
|
+
["network-apply-wave-2", ["network-apply-wave-1"]],
|
|
82
|
+
["network-apply-wave-3", ["network-apply-wave-2"]],
|
|
83
|
+
]);
|
|
84
|
+
// ci run, started in the member's directory, passes --member network to pr-apply.
|
|
85
|
+
expect(scoped.yaml).toContain("cd infra/network");
|
|
86
|
+
expect(scoped.yaml).toContain("chant ci run apply --in-place --base \"$BASE_SHA\" --env prod --gate pr-apply --forge gitlab --wave-gate release --wave 3");
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { readFileSync } from "fs";
|
|
9
9
|
import { join } from "path";
|
|
10
|
+
import { moduleSourceDir } from "@intentius/chant/module-source";
|
|
10
11
|
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
11
12
|
import { isPropertyDeclarable } from "@intentius/chant/declarable";
|
|
12
13
|
|
|
@@ -23,8 +24,7 @@ interface LexiconEntry {
|
|
|
23
24
|
function loadDeprecatedProperties(): Map<string, Set<string>> {
|
|
24
25
|
const map = new Map<string, Set<string>>();
|
|
25
26
|
try {
|
|
26
|
-
const
|
|
27
|
-
const lexiconPath = join(pkgDir, "src", "generated", "lexicon-gitlab.json");
|
|
27
|
+
const lexiconPath = join(moduleSourceDir(import.meta.url), "..", "..", "generated", "lexicon-gitlab.json");
|
|
28
28
|
const content = readFileSync(lexiconPath, "utf-8");
|
|
29
29
|
const data = JSON.parse(content) as Record<string, LexiconEntry>;
|
|
30
30
|
|
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
|
|