@intentius/chant-lexicon-github 0.124.31 → 0.124.32

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.
Files changed (151) hide show
  1. package/dist/action-pins.js +53 -0
  2. package/dist/codegen/docs-cli.js +2 -0
  3. package/dist/codegen/docs.js +142 -0
  4. package/dist/codegen/generate-cli.js +26 -0
  5. package/dist/codegen/generate-lexicon.js +36 -0
  6. package/dist/codegen/generate-typescript.js +108 -0
  7. package/dist/codegen/generate.js +111 -0
  8. package/dist/codegen/naming.js +49 -0
  9. package/dist/codegen/package.js +40 -0
  10. package/dist/codegen/parse.js +595 -0
  11. package/dist/codegen/patches.js +36 -0
  12. package/dist/components/ci-jobs.js +84 -0
  13. package/dist/components/generate-op-pipeline.js +440 -0
  14. package/dist/components/generate-op-waves-pipeline.js +158 -0
  15. package/dist/components/generate-pipeline.d.ts.map +1 -1
  16. package/dist/components/generate-pipeline.js +476 -0
  17. package/dist/composites/cache.js +17 -0
  18. package/dist/composites/catalog.js +859 -0
  19. package/dist/composites/checkout.js +25 -0
  20. package/dist/composites/dependabot.js +43 -0
  21. package/dist/composites/deploy-environment.js +38 -0
  22. package/dist/composites/docker-build.js +70 -0
  23. package/dist/composites/download-artifact.js +18 -0
  24. package/dist/composites/go-ci.js +62 -0
  25. package/dist/composites/index.js +15 -0
  26. package/dist/composites/node-ci.js +44 -0
  27. package/dist/composites/node-pipeline.js +96 -0
  28. package/dist/composites/pr-plan-report.js +129 -0
  29. package/dist/composites/python-ci.js +60 -0
  30. package/dist/composites/setup-go.js +18 -0
  31. package/dist/composites/setup-node.js +21 -0
  32. package/dist/composites/setup-python.js +18 -0
  33. package/dist/composites/upload-artifact.js +19 -0
  34. package/dist/coverage.js +32 -0
  35. package/dist/expression.js +177 -0
  36. package/dist/generated/index.js +26 -0
  37. package/dist/generated/runtime.js +4 -0
  38. package/dist/graph-edges.js +174 -0
  39. package/dist/import/generator.js +108 -0
  40. package/dist/import/parser.js +69 -0
  41. package/dist/index.js +22 -0
  42. package/dist/integrity.json +2 -2
  43. package/dist/lint/audit-catalog.js +69 -0
  44. package/dist/lint/audit-lineage.js +168 -0
  45. package/dist/lint/post-synth/gha006.js +50 -0
  46. package/dist/lint/post-synth/gha009.js +35 -0
  47. package/dist/lint/post-synth/gha011.js +34 -0
  48. package/dist/lint/post-synth/gha013.js +40 -0
  49. package/dist/lint/post-synth/gha017.js +25 -0
  50. package/dist/lint/post-synth/gha018.js +33 -0
  51. package/dist/lint/post-synth/gha019.js +58 -0
  52. package/dist/lint/post-synth/gha021.js +42 -0
  53. package/dist/lint/post-synth/gha022.js +44 -0
  54. package/dist/lint/post-synth/gha023.js +37 -0
  55. package/dist/lint/post-synth/gha024.js +32 -0
  56. package/dist/lint/post-synth/gha025.js +33 -0
  57. package/dist/lint/post-synth/gha026.js +32 -0
  58. package/dist/lint/post-synth/gha027.js +46 -0
  59. package/dist/lint/post-synth/gha028.js +28 -0
  60. package/dist/lint/post-synth/gha029.js +56 -0
  61. package/dist/lint/post-synth/gha030.js +35 -0
  62. package/dist/lint/post-synth/gha031.js +75 -0
  63. package/dist/lint/post-synth/gha032.js +35 -0
  64. package/dist/lint/post-synth/gha033.js +39 -0
  65. package/dist/lint/post-synth/gha034.js +35 -0
  66. package/dist/lint/post-synth/gha035.js +50 -0
  67. package/dist/lint/post-synth/gha036.js +37 -0
  68. package/dist/lint/post-synth/gha037.js +41 -0
  69. package/dist/lint/post-synth/gha038.js +35 -0
  70. package/dist/lint/post-synth/gha039.js +34 -0
  71. package/dist/lint/post-synth/gha040.js +36 -0
  72. package/dist/lint/post-synth/gha041.js +32 -0
  73. package/dist/lint/post-synth/gha042.js +33 -0
  74. package/dist/lint/post-synth/gha043.js +35 -0
  75. package/dist/lint/post-synth/gha044.js +36 -0
  76. package/dist/lint/post-synth/gha045.js +33 -0
  77. package/dist/lint/post-synth/gha046.js +55 -0
  78. package/dist/lint/post-synth/gha047.js +44 -0
  79. package/dist/lint/post-synth/gha048.js +34 -0
  80. package/dist/lint/post-synth/gha049.js +36 -0
  81. package/dist/lint/post-synth/gha050.js +37 -0
  82. package/dist/lint/post-synth/gha051.js +38 -0
  83. package/dist/lint/post-synth/gha052.js +35 -0
  84. package/dist/lint/post-synth/gha053.js +34 -0
  85. package/dist/lint/post-synth/gha054.js +35 -0
  86. package/dist/lint/post-synth/gha055.js +44 -0
  87. package/dist/lint/post-synth/gha056.js +32 -0
  88. package/dist/lint/post-synth/gha057.js +35 -0
  89. package/dist/lint/post-synth/gha058.js +42 -0
  90. package/dist/lint/post-synth/gha059.js +119 -0
  91. package/dist/lint/post-synth/gha060.js +129 -0
  92. package/dist/lint/post-synth/gha061.js +78 -0
  93. package/dist/lint/post-synth/gha062.js +65 -0
  94. package/dist/lint/post-synth/gha063.js +56 -0
  95. package/dist/lint/post-synth/gha064.js +47 -0
  96. package/dist/lint/post-synth/gha065.js +125 -0
  97. package/dist/lint/post-synth/gha066.js +40 -0
  98. package/dist/lint/post-synth/gha067.js +75 -0
  99. package/dist/lint/post-synth/gha068.js +47 -0
  100. package/dist/lint/post-synth/gha069.js +99 -0
  101. package/dist/lint/post-synth/index.js +114 -0
  102. package/dist/lint/post-synth/yaml-helpers.js +465 -0
  103. package/dist/lint/rules/data/action-usage-policy.js +16 -0
  104. package/dist/lint/rules/data/advisory-feed.js +15 -0
  105. package/dist/lint/rules/data/deprecated-versions.js +14 -0
  106. package/dist/lint/rules/data/flagged-actions.js +18 -0
  107. package/dist/lint/rules/data/known-action-slugs.js +38 -0
  108. package/dist/lint/rules/data/known-actions.js +12 -0
  109. package/dist/lint/rules/data/preinstalled-tools.js +36 -0
  110. package/dist/lint/rules/data/recommended-inputs.js +9 -0
  111. package/dist/lint/rules/data/risky-features.js +22 -0
  112. package/dist/lint/rules/data/secret-patterns.js +30 -0
  113. package/dist/lint/rules/data/trusted-action-owners.js +12 -0
  114. package/dist/lint/rules/data/untrusted-contexts.js +44 -0
  115. package/dist/lint/rules/deprecated-action-version.js +44 -0
  116. package/dist/lint/rules/detect-secrets.js +45 -0
  117. package/dist/lint/rules/extract-inline-structs.js +55 -0
  118. package/dist/lint/rules/file-job-limit.js +43 -0
  119. package/dist/lint/rules/index.js +16 -0
  120. package/dist/lint/rules/job-timeout.js +53 -0
  121. package/dist/lint/rules/missing-recommended-inputs.js +51 -0
  122. package/dist/lint/rules/no-hardcoded-secrets.js +39 -0
  123. package/dist/lint/rules/no-raw-expressions.js +44 -0
  124. package/dist/lint/rules/suggest-cache.js +60 -0
  125. package/dist/lint/rules/use-condition-builders.js +37 -0
  126. package/dist/lint/rules/use-matrix-builder.js +36 -0
  127. package/dist/lint/rules/use-typed-actions.js +41 -0
  128. package/dist/lint/rules/validate-concurrency.js +59 -0
  129. package/dist/lsp/completions.d.ts.map +1 -1
  130. package/dist/lsp/completions.js +18 -0
  131. package/dist/lsp/hover.d.ts.map +1 -1
  132. package/dist/lsp/hover.js +33 -0
  133. package/dist/manifest.json +1 -1
  134. package/dist/mcp/context-tools.js +152 -0
  135. package/dist/package-cli.js +20 -0
  136. package/dist/plugin.js +344 -0
  137. package/dist/serializer.js +572 -0
  138. package/dist/spec/fetch.js +46 -0
  139. package/dist/validate-cli.js +16 -0
  140. package/dist/validate.js +38 -0
  141. package/dist/variables.js +41 -0
  142. package/package.json +6 -6
  143. package/src/codegen/package.ts +2 -2
  144. package/src/components/__fixtures__/pr-loop-build.github.golden.yml +6 -2
  145. package/src/components/__fixtures__/pr-loop-waves.github.golden.yml +178 -0
  146. package/src/components/__fixtures__/pr-loop.github.golden.yml +6 -2
  147. package/src/components/generate-pipeline.ts +66 -29
  148. package/src/components/generate-pr-pipeline.test.ts +44 -20
  149. package/src/components/generate-pr-waves-pipeline.test.ts +117 -0
  150. package/src/lsp/completions.ts +2 -1
  151. package/src/lsp/hover.ts +2 -1
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Op waves → GitHub Actions workflow (#3679).
3
+ *
4
+ * One workflow for the whole spec, on a push to its branches (and by hand),
5
+ * one run at a time. Each job is one of core's `opWaveJobs`: a wave, or a wide
6
+ * wave's deciding job and share jobs, chained by `needs:` so a wave that waits
7
+ * at its gate (exit 3) or fails stops every later wave. Re-running the failed
8
+ * jobs after `chant approve` carries on from that wave.
9
+ *
10
+ * Every job checks out the full history, because the runner reads the gate
11
+ * policy from the spec file at the base commit and records pending gates on
12
+ * `chant/lifecycle`, which is why the workflow asks for `contents: write`. A
13
+ * deciding job uploads its decision, even when it waits, and each share job
14
+ * downloads it. The forgejo generator reuses {@link buildGithubOpWavesDoc}
15
+ * and applies its dialect.
16
+ *
17
+ * With `resume` on the spec (#3683), a second workflow, `<name>-resume.yml`,
18
+ * runs `chant run resume --op <name>` on that schedule: it re-runs the failed
19
+ * jobs of a run whose waiting wave has been approved since, with the job's
20
+ * token and `actions: write`.
21
+ *
22
+ * When a wave's approval is `pr-review` (#3684), the workflow also asks for
23
+ * `pull-requests: read`, so a wave can read the merged pull request's
24
+ * reviews, and a second workflow, `<name>-plans.yml`, runs `chant run wave
25
+ * --record-plans` on each pull request to record the digests its head plans.
26
+ *
27
+ * With `notify` on the spec (#3761), the workflow maps each variable it names
28
+ * from the repository secret of the same name, so a wave job can post to
29
+ * Slack, Teams or a signed webhook. `options.variables` wins over the mapping.
30
+ *
31
+ * `runner` (on the spec, a wave or a job group, #3914) is the job's `runs-on`.
32
+ * A wave's `jobs` renders its deciding job with the wave's own settings and a
33
+ * job per group (per share of a group) with the group's: its runner,
34
+ * environment, variables over the wave's, setup after the wave's, and id
35
+ * tokens. A job with id tokens gets `permissions:` of its own, the
36
+ * workflow's plus `id-token: write`, and each audience as `<NAME>_AUDIENCE`.
37
+ */
38
+ import { notifyVariables, opWaveGroupOf, opWaveJobs, opWavesResumeCommand } from "@intentius/chant/op/op-waves";
39
+ import { assertEnvironment, assertSetupSteps, CI_IMAGE, dirnameOf, environmentDoc, GITHUB_JOB_TOKEN, githubRunsOn, githubSecretEnv, idTokenAudienceEnv, nonEmpty, pushBranches, setupStepDoc, } from "@intentius/chant/ci-jobs";
40
+ import { checkoutStep, concurrency, downloadArtifactStep, keepRecordStep, runSteps } from "./ci-jobs.js";
41
+ import { emitOpPipelineYAML } from "./generate-op-pipeline.js";
42
+ /** The default image: it carries git, which the runner needs to read the base commit. */
43
+ export const OP_WAVES_IMAGE = CI_IMAGE;
44
+ /** The artifact a wide wave's deciding job hands its share jobs. */
45
+ export function opWaveRecordArtifact(spec, wave) {
46
+ return `${spec.name}-wave-${wave}-decision`;
47
+ }
48
+ /** The workflow document, before emission, and the jobs it holds. */
49
+ export function buildGithubOpWavesDoc(spec, options) {
50
+ const jobs = opWaveJobs(spec, options.specFile);
51
+ const image = options.image ?? OP_WAVES_IMAGE;
52
+ const byReview = spec.waves.some((w) => w.approval === "pr-review");
53
+ for (const wave of spec.waves) {
54
+ assertSetupSteps(`${spec.name}/${wave.name}`, wave.setup ?? []);
55
+ if (wave.environment)
56
+ assertEnvironment(`${spec.name}/${wave.name}`, wave.environment);
57
+ for (const group of wave.jobs ?? []) {
58
+ assertSetupSteps(`${spec.name}/${wave.name}/${group.name}`, group.setup ?? []);
59
+ if (group.environment)
60
+ assertEnvironment(`${spec.name}/${wave.name}/${group.name}`, group.environment);
61
+ }
62
+ }
63
+ const permissions = { contents: "write", ...(byReview ? { "pull-requests": "read" } : {}) };
64
+ const jobsDoc = {};
65
+ for (const job of jobs) {
66
+ const wave = spec.waves[job.wave - 1];
67
+ // #3914: a job group's job runs with the group's settings over the wave's.
68
+ const group = opWaveGroupOf(spec, job);
69
+ const steps = [checkoutStep(true)];
70
+ if (job.kind === "share") {
71
+ steps.push(downloadArtifactStep(`Download wave ${job.wave}'s decision`, opWaveRecordArtifact(spec, job.wave), dirnameOf(job.record)));
72
+ }
73
+ steps.push(...[...(wave.setup ?? []), ...(group?.setup ?? [])].map(setupStepDoc), ...runSteps(options.beforeScript ?? []));
74
+ steps.push({ name: describeJob(job.kind, job.wave, wave.name, job.share, job.group), run: job.command.join(" ") });
75
+ steps.push(...runSteps(options.extraScript ?? []));
76
+ if (job.kind === "decide") {
77
+ steps.push(keepRecordStep(`Keep wave ${job.wave}'s decision`, opWaveRecordArtifact(spec, job.wave), job.record));
78
+ }
79
+ const environment = group?.environment ?? wave.environment;
80
+ const idTokens = group?.idTokens ?? wave.idTokens;
81
+ jobsDoc[job.jobName] = {
82
+ "runs-on": githubRunsOn(group?.runner ?? wave.runner ?? spec.runner),
83
+ ...(job.needs.length > 0 ? { needs: job.needs } : {}),
84
+ container: image,
85
+ ...(environment ? { environment: environmentDoc(environment) } : {}),
86
+ // #3914: a job's own permissions replace the workflow's, so they repeat them.
87
+ ...(idTokens ? { permissions: { ...permissions, "id-token": "write" } } : {}),
88
+ ...nonEmpty("env", group || idTokens ? { ...wave.variables, ...group?.variables, ...idTokenAudienceEnv(idTokens) } : wave.variables),
89
+ steps,
90
+ };
91
+ }
92
+ // #3761: the notify variables come from the secrets of the same name.
93
+ const env = { ...githubSecretEnv(notifyVariables(spec.notify)), ...(options.variables ?? {}) };
94
+ const doc = {
95
+ name: spec.name,
96
+ on: { push: { branches: pushBranches(spec.branches) }, workflow_dispatch: {} },
97
+ ...nonEmpty("env", env),
98
+ concurrency: concurrency(spec.name),
99
+ permissions,
100
+ jobsDoc,
101
+ };
102
+ return {
103
+ doc,
104
+ jobs,
105
+ ...(spec.resume ? { resumeDoc: buildResumeDoc(spec, options) } : {}),
106
+ ...(byReview ? { plansDoc: buildPlansDoc(spec, options) } : {}),
107
+ };
108
+ }
109
+ /** The scheduled resume workflow (#3683). */
110
+ function buildResumeDoc(spec, options) {
111
+ const steps = [checkoutStep(), ...runSteps(options.beforeScript ?? [])];
112
+ steps.push({
113
+ name: `Resume the waves of ${spec.name} whose approval arrived`,
114
+ run: opWavesResumeCommand(spec).join(" "),
115
+ env: { GITHUB_TOKEN: GITHUB_JOB_TOKEN },
116
+ });
117
+ return {
118
+ name: `${spec.name}-resume`,
119
+ on: { schedule: [{ cron: spec.resume.schedule }], workflow_dispatch: {} },
120
+ concurrency: concurrency(`${spec.name}-resume`, true),
121
+ // contents: read fetches chant/lifecycle; actions: write re-runs a waiting run.
122
+ permissions: { contents: "read", actions: "write" },
123
+ jobsDoc: { resume: { "runs-on": githubRunsOn(spec.runner), container: options.image ?? OP_WAVES_IMAGE, steps } },
124
+ };
125
+ }
126
+ /** The pull request workflow a `pr-review` wave needs (#3684): record the digests each head plans. */
127
+ function buildPlansDoc(spec, options) {
128
+ const steps = [checkoutStep(true), ...(spec.waves[0]?.setup ?? []).map(setupStepDoc), ...runSteps(options.beforeScript ?? [])];
129
+ steps.push({ name: "Plan every wave at the head and record its digests", run: ["chant", "run", "wave", "--spec", options.specFile, "--record-plans"].join(" ") });
130
+ return {
131
+ name: `${spec.name}-plans`,
132
+ on: { pull_request: { branches: pushBranches(spec.branches) } },
133
+ ...nonEmpty("env", options.variables),
134
+ concurrency: concurrency(spec.name + "-plans-${{ github.event.number }}", true),
135
+ // contents: write pushes the record to chant/lifecycle.
136
+ permissions: { contents: "write" },
137
+ jobsDoc: { "record-plans": { "runs-on": githubRunsOn(spec.runner), container: options.image ?? OP_WAVES_IMAGE, steps } },
138
+ };
139
+ }
140
+ function describeJob(kind, wave, name, share, group) {
141
+ if (kind === "decide")
142
+ return `Plan wave ${wave} (${name}) and decide its gate`;
143
+ if (group !== undefined)
144
+ return `Apply ${share !== undefined ? `share ${share} of ` : ""}job group ${group} of wave ${wave} (${name})`;
145
+ if (kind === "share")
146
+ return `Apply share ${share} of wave ${wave} (${name})`;
147
+ return `Plan, gate and apply wave ${wave} (${name})`;
148
+ }
149
+ /** Render an Op waves spec as one GitHub Actions workflow, `<name>.yml`. */
150
+ export function generateGithubOpWavesPipeline(spec, options) {
151
+ const { doc, jobs, resumeDoc, plansDoc } = buildGithubOpWavesDoc(spec, options);
152
+ const files = [{ name: `${spec.name}.yml`, yaml: emitOpPipelineYAML(doc) }];
153
+ if (resumeDoc)
154
+ files.push({ name: `${spec.name}-resume.yml`, yaml: emitOpPipelineYAML(resumeDoc) });
155
+ if (plansDoc)
156
+ files.push({ name: `${spec.name}-plans.yml`, yaml: emitOpPipelineYAML(plansDoc) });
157
+ return { files, jobs };
158
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"generate-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AAEH,OAAO,EAAyB,KAAK,eAAe,EAAE,MAAM,oCAAoC,CAAC;AAMjG,OAAO,KAAK,EACV,oBAAoB,IAAI,YAAY,EACpC,wBAAwB,IAAI,qBAAqB,EACjD,uBAAuB,IAAI,oBAAoB,EAEhD,MAAM,0BAA0B,CAAC;AAGlC,YAAY,EAAE,YAAY,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,CAAC;AAE1E;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,6FAA6F;IAC7F,WAAW,EAAE,MAAM,CAAC;IACpB,8DAA8D;IAC9D,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC5B,mIAAmI;IACnI,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC;;;;;;OAMG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,qDAAqD;IACrD,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,uEAAuE;IACvE,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,0DAA0D;IAC1D,IAAI,EAAE,YAAY,EAAE,CAAC;CACtB;AAuKD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CAWpD;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,eAAe,EAAE,EAC7B,OAAO,GAAE,qBAA0B,GAClC,iBAAiB,CA+JnB;AA4CD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,iBAAiB,GAAG,MAAM,CAE/D;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,eAAe,EAAE,EAC7B,OAAO,GAAE,qBAA0B,GAClC,oBAAoB,CAGtB"}
1
+ {"version":3,"file":"generate-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AAEH,OAAO,EAAyB,KAAK,eAAe,EAAE,MAAM,oCAAoC,CAAC;AAgBjG,OAAO,KAAK,EACV,oBAAoB,IAAI,YAAY,EACpC,wBAAwB,IAAI,qBAAqB,EACjD,uBAAuB,IAAI,oBAAoB,EAEhD,MAAM,0BAA0B,CAAC;AAGlC,YAAY,EAAE,YAAY,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,CAAC;AAE1E;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,6FAA6F;IAC7F,WAAW,EAAE,MAAM,CAAC;IACpB,8DAA8D;IAC9D,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC5B,mIAAmI;IACnI,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC;;;;;;OAMG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,qDAAqD;IACrD,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,uEAAuE;IACvE,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,0DAA0D;IAC1D,IAAI,EAAE,YAAY,EAAE,CAAC;CACtB;AAkMD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CAWpD;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,eAAe,EAAE,EAC7B,OAAO,GAAE,qBAA0B,GAClC,iBAAiB,CA+JnB;AA4CD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,iBAAiB,GAAG,MAAM,CAE/D;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,eAAe,EAAE,EAC7B,OAAO,GAAE,qBAA0B,GAClC,oBAAoB,CAGtB"}
@@ -0,0 +1,476 @@
1
+ /**
2
+ * Generate mode — component → GitHub Actions workflow YAML (#891, epic #885).
3
+ *
4
+ * The github counterpart to the gitlab generator (`../../gitlab/src/components/generate-pipeline.ts`,
5
+ * #563/#688): the same `chant build --components --generate <lexicon>` seam,
6
+ * synthesizing a **thin** `.github/workflows/*.yml` from the discovered
7
+ * component graph instead of interpreting it directly.
8
+ *
9
+ * The generated workflow is a trigger, not the deploy logic:
10
+ * - Ordering + parallel-safe waves are resolved once, generically, by
11
+ * `resolveComponentGraph` (core's driver) — the exact function the local
12
+ * interpret driver and the gitlab generator both use. This module does not
13
+ * re-derive or duplicate that graph logic.
14
+ * - GitHub Actions has no `stage` concept, so wave ordering is expressed
15
+ * entirely through `needs:` edges between jobs: one job per component,
16
+ * with `needs:` pointing at the jobs for its direct `dependsOn` entries.
17
+ * Independent components therefore run in parallel, and dependents wait
18
+ * on their dependencies via GitHub's own `needs:` scheduling.
19
+ * `ComponentPipelineResult.stages` is still populated with the wave-ordered
20
+ * names (one per wave) purely for parity with gitlab's machine-readable
21
+ * `--format json` view — GitHub's YAML itself has no `stages:` section.
22
+ * - Each job's trigger step is exactly one invocation that hands off to the
23
+ * component's own composition (`chant run --components <name> ...` by
24
+ * default) — never inlined build/publish/apply steps. The deploy logic
25
+ * lives in the component's `deploy` phases and the capabilities they
26
+ * reference, not in this YAML.
27
+ * - A component that something else depends on can't hand its resolved
28
+ * stack outputs to a dependent in-memory, since each job is a separate
29
+ * GitHub Actions runner. It dumps its outputs to a file and uploads that
30
+ * file as a workflow artifact (`actions/upload-artifact`); each direct
31
+ * dependent downloads it (`actions/download-artifact`) and seeds from it,
32
+ * so a `stackOutput()` / `@<dep>.publish.*` reference still resolves even
33
+ * though the producer ran in a different job. This mirrors the dump/seed
34
+ * model `cli-support.ts` (`runComponents`'s `componentOutputs`) already
35
+ * uses, and matches gitlab's artifact-passing 1:1 in intent — only the
36
+ * transport differs (explicit upload/download steps vs. GitLab's implicit
37
+ * `needs:` artifact passing).
38
+ * - With `options.promoteTo` (#2575), one more job needs every component
39
+ * job and runs `chant components promote`. A promote publishes from the
40
+ * build archive on disk, so each component job uploads the files its
41
+ * build steps wrote and the promote job downloads them to the same paths.
42
+ * Each component job with a publish step also writes the digest its run
43
+ * recorded (`--digest-file`) to a job output, and the promote job passes
44
+ * it back as `--digest <component>=<digest>` (#2602), so it promotes the
45
+ * release this run built rather than whatever is latest in the source
46
+ * environment.
47
+ * - For a workspace member (`options.member`, #2542) the workflow keeps its
48
+ * triggers, and its `run:` steps start in the member's directory. See
49
+ * `scopeToMember`.
50
+ *
51
+ * Cross-cutting changes (e.g. "sign every image before deploy") are made by
52
+ * editing `GenerateGithubOptions.extraScript`/`beforeScript` (or the
53
+ * component's own composition) ONCE here — never per generated job. See
54
+ * `generate-pipeline.test.ts`'s "cross-cutting change" cases for a
55
+ * demonstration: one generator-option edit reflects in every job without
56
+ * touching the component declarations.
57
+ */
58
+ import { resolveComponentGraph } from "@intentius/chant/components/driver";
59
+ import { hasPublishStep, promoteArchivePaths } from "@intentius/chant/components/promote";
60
+ import { GATED_WAVE_RECORD, gatedWaveJobs } from "@intentius/chant/components/gated-wave-pipeline";
61
+ import { CI_IMAGE, ciJobName, dirnameOf, fillCommand, GITHUB_RUNNER } from "@intentius/chant/ci-jobs";
62
+ import { PR_APPLY_RECORD, PR_LOOP_REPORT_DIR, prApplyGroup, prApplyRecordKey, prApplyWaveJobName, prLoopJobs, prLoopSetup, prLoopWaveJobs, prLoopWithWaves, } from "@intentius/chant/components/pr-pipeline";
63
+ import { memberRepoPath } from "@intentius/chant/lexicon";
64
+ import { checkoutStep, concurrency, downloadArtifactStep, emitWorkflowYAML, keepRecordStep, runSteps, UPLOAD_ARTIFACT } from "./ci-jobs.js";
65
+ /** The workflow artifact the gated-wave jobs hand their attempt record through, one per wave. */
66
+ function waveRecordArtifact(wave) {
67
+ return `fan-out-record-wave-${wave}`;
68
+ }
69
+ /**
70
+ * The gated-wave workflow (#3049): one job per wave, each running one wave of
71
+ * `chant components fan-out --wave-gate`, chained by `needs:`. Each job
72
+ * downloads the record the job before it uploaded and uploads its own, even
73
+ * when it stops at a gate (exit 3), so re-running it after `chant approve`
74
+ * reads the record. The checkout fetches full history for `--base`.
75
+ */
76
+ function gatedWaveGithubDoc(components, env, image, beforeScript, extraScript, options) {
77
+ const waveJobs = gatedWaveJobs(components, env, options.gatedWaves);
78
+ const jobsDoc = {};
79
+ const jobs = [];
80
+ for (const job of waveJobs) {
81
+ jobs.push({ jobName: job.jobName, component: `wave ${job.wave}`, stage: job.jobName, needs: job.needs });
82
+ const steps = [checkoutStep(true)];
83
+ if (job.wave > 1) {
84
+ steps.push(downloadArtifactStep(`Download the record wave ${job.wave - 1} left`, waveRecordArtifact(job.wave - 1), dirnameOf(GATED_WAVE_RECORD)));
85
+ }
86
+ steps.push(...runSteps(prLoopSetup(image)), ...runSteps(beforeScript), { run: job.command.join(" ") }, ...runSteps(extraScript));
87
+ steps.push(keepRecordStep(`Upload the record for wave ${job.wave}`, waveRecordArtifact(job.wave), GATED_WAVE_RECORD));
88
+ jobsDoc[job.jobName] = {
89
+ "runs-on": GITHUB_RUNNER,
90
+ ...(job.needs.length > 0 ? { needs: job.needs } : {}),
91
+ container: image,
92
+ steps,
93
+ };
94
+ }
95
+ const doc = {
96
+ name: `chant-components-${env}`,
97
+ environment: env,
98
+ on: { workflow_dispatch: {} },
99
+ env: { ...options.variables, CHANT_ENV: env },
100
+ jobsDoc,
101
+ stages: waveJobs.map((j) => j.jobName),
102
+ jobs,
103
+ };
104
+ return options.member ? scopeToMember(doc, options.member) : doc;
105
+ }
106
+ /**
107
+ * The pull-request workflow (#3183): `plan` on each pull request into the
108
+ * target branch, `apply` on each push to it. The plan job can comment and set
109
+ * statuses but cannot push, since it runs the pull request's code; the apply
110
+ * job can push, to record a pending fact on `chant/lifecycle`, and runs one
111
+ * at a time per environment. Both keep their report as an artifact, and both
112
+ * check out the full history, since each measures the change with git.
113
+ * The apply job restores its attempt record from the cache before it runs
114
+ * and saves it afterwards, failed or not, so a re-run of a failed apply
115
+ * finishes it under the same approval (#3543).
116
+ *
117
+ * For a workspace member (#3465) the run steps start in the member's
118
+ * directory and the report artifact is kept from there. The triggers take no
119
+ * path filter, since a change outside the member can reach it; the plan
120
+ * selects from the whole change and passes `--member`, which keeps the
121
+ * member's gate, note and statuses apart from the other members'.
122
+ *
123
+ * With a gate per wave (`waveGate`, #4003) the one apply job becomes
124
+ * `apply-wave-1` to `apply-wave-<n>`, one per wave of the component graph,
125
+ * each needing the one before. Each saves the record under its own cache key
126
+ * (the run, the attempt and the wave), and each restores the newest record
127
+ * for the pushed commit, so wave k reads what wave k-1 left, and a re-run of
128
+ * a wave that stopped at its gate reads what it left itself. Each keeps its
129
+ * report as `chant-pr-apply-wave-<k>`.
130
+ */
131
+ function prLoopGithubDoc(components, env, image, beforeScript, extraScript, options) {
132
+ const loop = options.prLoop;
133
+ const branch = loop.branch ?? "main";
134
+ const member = options.member;
135
+ const rooted = !member || member.dir === "." || member.dir === "";
136
+ const [plan, apply] = prLoopJobs(loop.forge ?? "github", env, loop, member?.name);
137
+ const reportPath = member ? memberRepoPath(member, PR_LOOP_REPORT_DIR) : PR_LOOP_REPORT_DIR;
138
+ // The apply's attempt record (#3543) lives in the cache under the pushed
139
+ // commit. Cache keys cannot be overwritten, so each attempt saves under
140
+ // its run and attempt, and a re-run restores the newest for the commit.
141
+ const recordPath = member ? memberRepoPath(member, PR_APPLY_RECORD) : PR_APPLY_RECORD;
142
+ const recordPrefix = `${prApplyRecordKey(env, "${{ github.sha }}", member?.name)}-`;
143
+ const recordKey = (wave) => `${recordPrefix}\${{ github.run_id }}-\${{ github.run_attempt }}${wave ? `-wave-${wave}` : ""}`;
144
+ const restoreRecord = (wave) => ({
145
+ name: wave ? "Restore the newest record of this commit's apply" : "Restore the record of an earlier attempt at this commit",
146
+ uses: "actions/cache/restore@v4",
147
+ with: { path: recordPath, key: recordKey(wave), "restore-keys": recordPrefix },
148
+ });
149
+ const saveRecord = (wave) => ({
150
+ name: wave ? "Keep the record for the next wave and a re-run" : "Keep the record for a re-run",
151
+ if: "always()",
152
+ uses: "actions/cache/save@v4",
153
+ with: { path: recordPath, key: recordKey(wave) },
154
+ });
155
+ const reportName = (job) => `chant-pr-${job.wave ? prApplyWaveJobName(job.wave) : job.jobName}`;
156
+ const steps = (job, name) => [
157
+ checkoutStep(true),
158
+ ...runSteps(prLoopSetup(image)),
159
+ ...runSteps(beforeScript),
160
+ ...(job.build ? [{ name: "Build the templates", run: job.build[0] }, { name: "Lint the project", run: job.build[1] }] : []),
161
+ ...(job.jobName === "apply" ? [restoreRecord(job.wave)] : []),
162
+ // The job's setup lines (#4004) run in the command's shell, so a value they settle reaches it.
163
+ // The command runs through chant ci run (#4156), which writes the job's run record, signed when the CHANT_RUNNER_KEY secret is set.
164
+ { name, env: job.run.env, run: [...(job.setup ?? []), job.run.command].join("\n") },
165
+ ...runSteps(extraScript),
166
+ ...(job.jobName === "apply" ? [saveRecord(job.wave)] : []),
167
+ {
168
+ name: `Keep the ${job.wave ? `wave ${job.wave} ` : ""}${job.jobName} report`,
169
+ if: "always()",
170
+ uses: UPLOAD_ARTIFACT,
171
+ with: { name: reportName(job), path: reportPath, "if-no-files-found": "ignore", "include-hidden-files": true },
172
+ },
173
+ ];
174
+ const applyJob = (job, needs, name) => ({
175
+ if: "github.event_name == 'push'",
176
+ "runs-on": GITHUB_RUNNER,
177
+ ...(needs.length > 0 ? { needs } : {}),
178
+ container: image,
179
+ concurrency: concurrency(prApplyGroup(env, member?.name)),
180
+ permissions: { contents: "write", "pull-requests": "write", statuses: "write" },
181
+ steps: steps(job, name),
182
+ });
183
+ const waveJobs = loop.waveGate ? prLoopWaveJobs(components, apply) : [];
184
+ const applyJobs = loop.waveGate
185
+ ? waveJobs.map((job) => [
186
+ prApplyWaveJobName(job.wave),
187
+ applyJob(job, job.wave > 1 ? [prApplyWaveJobName(job.wave - 1)] : [], `Apply wave ${job.wave}, under the approval its plan has`),
188
+ ])
189
+ : [["apply", applyJob(apply, [], "Apply the plan a reviewer approved")]];
190
+ const jobsDoc = {
191
+ plan: {
192
+ if: "github.event_name == 'pull_request'",
193
+ "runs-on": GITHUB_RUNNER,
194
+ container: image,
195
+ permissions: { contents: "read", "pull-requests": "write", statuses: "write" },
196
+ steps: steps(plan, "Plan the members this pull request reaches"),
197
+ },
198
+ ...Object.fromEntries(applyJobs),
199
+ };
200
+ const applyNames = applyJobs.map(([name]) => name);
201
+ return {
202
+ name: member ? `chant-pr.${member.name}.${env}` : `chant-pr-${env}`,
203
+ environment: env,
204
+ on: { pull_request: { branches: [branch] }, push: { branches: [branch] } },
205
+ ...(rooted ? {} : { defaults: { run: { "working-directory": member.dir } } }),
206
+ env: { ...options.variables, CHANT_ENV: env },
207
+ jobsDoc,
208
+ stages: ["plan", ...applyNames],
209
+ jobs: [
210
+ { jobName: "plan", component: "pull request plan", stage: "plan", needs: [] },
211
+ ...applyNames.map((name, i) => ({
212
+ jobName: name,
213
+ component: loop.waveGate ? `pull request apply, wave ${i + 1}` : "pull request apply",
214
+ stage: name,
215
+ needs: i > 0 ? [applyNames[i - 1]] : [],
216
+ })),
217
+ ],
218
+ };
219
+ }
220
+ /** The file a producer dumps its resolved outputs to, and a dependent seeds from — one per component, matching gitlab's naming so both generators produce interchangeable artifacts. */
221
+ function outputsFile(name) {
222
+ return `${name}.outputs.json`;
223
+ }
224
+ /** The workflow artifact name a producer's dumped outputs are uploaded under. */
225
+ function artifactName(name) {
226
+ return `${name}-outputs`;
227
+ }
228
+ /** The file a component job writes the digest its run recorded to, for the promote job (#2602). */
229
+ function digestFile(name) {
230
+ return `${name}.digest`;
231
+ }
232
+ /** The workflow artifact name a component's build archive is uploaded under, for the promote job (#2575). */
233
+ function archiveArtifactName(name) {
234
+ return `${name}-archive`;
235
+ }
236
+ /**
237
+ * The directory `actions/upload-artifact` roots a set of files at: the
238
+ * deepest directory that holds all of them. Downloading into it puts each
239
+ * file back at the path it was uploaded from.
240
+ */
241
+ export function artifactRoot(paths) {
242
+ const dirs = paths.map((p) => {
243
+ const parts = p.replace(/\/+$/, "").split("/");
244
+ parts.pop();
245
+ return parts.filter((part) => part !== "" && part !== ".");
246
+ });
247
+ const common = [];
248
+ for (let i = 0; dirs.every((d) => i < d.length && d[i] === dirs[0][i]); i++)
249
+ common.push(dirs[0][i]);
250
+ const absolute = paths.every((p) => p.startsWith("/"));
251
+ if (common.length === 0)
252
+ return absolute ? "/" : ".";
253
+ return (absolute ? "/" : "") + common.join("/");
254
+ }
255
+ /**
256
+ * Synthesize a `.github/workflows/*.yml` pipeline from a set of components:
257
+ * one job per component, `needs:` expressing the wave-ordered dependency DAG
258
+ * from `resolveComponentGraph`. Throws `DependencyCycleError`/
259
+ * `UnknownDependencyError` (from core's driver) exactly like the interpret
260
+ * driver and the gitlab generator do, since all three consume the same graph
261
+ * resolution. Wired into core's generate mode via the github lexicon plugin's
262
+ * `generateComponentPipeline` (../plugin.ts).
263
+ */
264
+ export function buildGithubPipelineDoc(components, options = {}) {
265
+ const env = options.env ?? "production";
266
+ const image = options.image ?? CI_IMAGE;
267
+ const runCommand = options.runCommand ?? ["chant", "run", "--components", "{name}", "--env", env];
268
+ const beforeScript = options.beforeScript ?? [];
269
+ const extraScript = options.extraScript ?? [];
270
+ const { waves } = resolveComponentGraph(components);
271
+ const byName = new Map(components.map((c) => [c.name, c]));
272
+ if (options.prLoop) {
273
+ if (options.promoteTo !== undefined)
274
+ throw new Error("a pull-request pipeline has no promote job; drop --promote-to or --pr-loop");
275
+ // #4003: --wave-gate beside --pr-loop gives the apply a gate per wave.
276
+ const prLoop = prLoopWithWaves(options.prLoop, options.gatedWaves);
277
+ return prLoopGithubDoc(components, env, image, beforeScript, extraScript, { ...options, prLoop });
278
+ }
279
+ if (options.gatedWaves) {
280
+ if (options.promoteTo !== undefined)
281
+ throw new Error("a gated-wave pipeline has no promote job; drop --promote-to or --wave-gate");
282
+ return gatedWaveGithubDoc(components, env, image, beforeScript, extraScript, options);
283
+ }
284
+ // Components that something else depends on must hand their resolved outputs
285
+ // (stack outputs, published artifact refs) to their dependents, which run as
286
+ // separate jobs on separate runners. Each such producer dumps its outputs to
287
+ // a file and uploads it as a workflow artifact; each dependent downloads that
288
+ // artifact and seeds from it, so a `stackOutput()` / `@<dep>.publish.*`
289
+ // reference resolves even though the producer ran in a different job.
290
+ // Without this, a single-component job has no in-memory outputs for its
291
+ // dependencies — see epic #551 / the adopt-alb-services example.
292
+ const dependedUpon = new Set();
293
+ for (const c of components)
294
+ for (const dep of c.dependsOn ?? [])
295
+ dependedUpon.add(dep);
296
+ // A promote job (#2575) runs on its own runner, and a promote publishes from
297
+ // the build archive on disk, so each component job uploads the files its
298
+ // build steps wrote and the promote job downloads them.
299
+ const promoteTo = options.promoteTo;
300
+ const archives = new Map();
301
+ // The components the promote job pins to this run's digest (#2602): those
302
+ // with a publish step, the only ones whose deploy records a release.
303
+ const pinned = new Set();
304
+ if (promoteTo !== undefined) {
305
+ for (const c of components) {
306
+ const paths = promoteArchivePaths(c);
307
+ if (paths.length > 0)
308
+ archives.set(c.name, paths);
309
+ if (hasPublishStep(c))
310
+ pinned.add(c.name);
311
+ }
312
+ if (pinned.size === 0) {
313
+ throw new Error("no component has a publish step, so no deploy records a release for the promote job to promote");
314
+ }
315
+ }
316
+ const stages = waves.map((_, i) => `wave-${i + 1}`);
317
+ const jobs = [];
318
+ const jobNameByComponent = new Map();
319
+ for (const wave of waves) {
320
+ for (const name of wave)
321
+ jobNameByComponent.set(name, ciJobName(name));
322
+ }
323
+ const jobsDoc = {};
324
+ waves.forEach((wave, waveIndex) => {
325
+ const stage = stages[waveIndex];
326
+ for (const name of wave) {
327
+ const component = byName.get(name);
328
+ const jobName = jobNameByComponent.get(name);
329
+ const needs = (component.dependsOn ?? []).map((dep) => jobNameByComponent.get(dep)).sort();
330
+ jobs.push({ jobName, component: name, stage, needs });
331
+ // Build the run invocation, then append output-threading flags: seed from
332
+ // each dependency's dumped outputs (downloaded as an artifact below), and
333
+ // dump this component's own outputs if a dependent will need them.
334
+ const runParts = fillCommand(runCommand, name);
335
+ for (const dep of component.dependsOn ?? [])
336
+ runParts.push("--seed-outputs", outputsFile(dep));
337
+ if (dependedUpon.has(name))
338
+ runParts.push("--dump-outputs", outputsFile(name));
339
+ if (pinned.has(name))
340
+ runParts.push("--digest-file", digestFile(name));
341
+ // One step per script line — mirrors gitlab's `script:` array of
342
+ // discrete shell lines, rather than a single multi-line `run:` block, so
343
+ // each line is independently inspectable (and machine-parseable).
344
+ const steps = [checkoutStep()];
345
+ for (const dep of component.dependsOn ?? [])
346
+ steps.push(downloadArtifactStep(`Download ${dep} outputs`, artifactName(dep), "."));
347
+ steps.push(...runSteps(beforeScript), { run: runParts.join(" ") }, ...runSteps(extraScript));
348
+ // The file holds `<component>=<digest>`; the job output holds the digest.
349
+ if (pinned.has(name)) {
350
+ steps.push({
351
+ name: `Record ${name} digest`,
352
+ id: "digest",
353
+ run: `echo "digest=$(cut -d= -f2- ${digestFile(name)})" >> "$GITHUB_OUTPUT"`,
354
+ });
355
+ }
356
+ if (dependedUpon.has(name)) {
357
+ steps.push({
358
+ name: `Upload ${name} outputs`,
359
+ uses: UPLOAD_ARTIFACT,
360
+ with: { name: artifactName(name), path: outputsFile(name) },
361
+ });
362
+ }
363
+ const archive = archives.get(name);
364
+ if (archive) {
365
+ steps.push({
366
+ name: `Upload ${name} build archive`,
367
+ uses: UPLOAD_ARTIFACT,
368
+ with: {
369
+ name: archiveArtifactName(name),
370
+ path: archive.join("\n"),
371
+ "if-no-files-found": "error",
372
+ "include-hidden-files": true,
373
+ },
374
+ });
375
+ }
376
+ const jobProps = {
377
+ "runs-on": GITHUB_RUNNER,
378
+ ...(needs.length > 0 ? { needs } : {}),
379
+ container: image,
380
+ ...(pinned.has(name) ? { outputs: { digest: "${{ steps.digest.outputs.digest }}" } } : {}),
381
+ steps,
382
+ };
383
+ jobsDoc[jobName] = jobProps;
384
+ }
385
+ });
386
+ if (promoteTo !== undefined) {
387
+ const promoteJob = `promote-${ciJobName(promoteTo)}`;
388
+ if (promoteJob in jobsDoc) {
389
+ throw new Error(`the promote job "${promoteJob}" has the same name as a component job; rename the component`);
390
+ }
391
+ const command = [...(options.promoteCommand ?? ["chant", "components", "promote", "--from", env, "--to", promoteTo])];
392
+ for (const name of [...pinned].sort()) {
393
+ command.push("--digest", `"${name}=\${{ needs.${jobNameByComponent.get(name)}.outputs.digest }}"`);
394
+ }
395
+ const steps = [checkoutStep()];
396
+ for (const [name, paths] of archives) {
397
+ steps.push(downloadArtifactStep(`Download ${name} build archive`, archiveArtifactName(name), artifactRoot(paths)));
398
+ }
399
+ steps.push(...runSteps(beforeScript), { run: command.join(" ") }, ...runSteps(extraScript));
400
+ jobsDoc[promoteJob] = {
401
+ "runs-on": GITHUB_RUNNER,
402
+ needs: [...jobNameByComponent.values()].sort(),
403
+ container: image,
404
+ steps,
405
+ };
406
+ }
407
+ const doc = {
408
+ name: `chant-components-${env}`,
409
+ environment: env,
410
+ on: { workflow_dispatch: {} },
411
+ env: { ...options.variables, CHANT_ENV: env },
412
+ jobsDoc,
413
+ stages,
414
+ jobs,
415
+ };
416
+ return options.member ? scopeToMember(doc, options.member) : doc;
417
+ }
418
+ /**
419
+ * Scope a pipeline to one workspace member (#2542, #2524 D19). Its name
420
+ * carries the member's name, and every `run:` step starts in the member's
421
+ * directory through `defaults.run.working-directory`.
422
+ *
423
+ * The triggers stay exactly the plain pipeline's. A deploy pipeline gains no
424
+ * `push` or `pull_request` trigger for a member, and its only trigger,
425
+ * `workflow_dispatch`, takes no `paths:` filter.
426
+ *
427
+ * `working-directory` applies to `run:` steps only. The artifact actions
428
+ * resolve `path` against the repository root, so each upload and download
429
+ * path moves under the member's directory, where the `run:` steps read and
430
+ * write the files.
431
+ */
432
+ function scopeToMember(doc, member) {
433
+ const rooted = member.dir === "." || member.dir === "";
434
+ const jobsDoc = {};
435
+ for (const [name, job] of Object.entries(doc.jobsDoc)) {
436
+ const props = job;
437
+ jobsDoc[name] = rooted || !props.steps ? job : { ...props, steps: props.steps.map((step) => memberArtifactStep(step, member)) };
438
+ }
439
+ return {
440
+ ...doc,
441
+ name: `chant-components.${member.name}.${doc.environment}`,
442
+ ...(rooted ? {} : { defaults: { run: { "working-directory": member.dir } } }),
443
+ jobsDoc,
444
+ };
445
+ }
446
+ /** An `actions/upload-artifact` or `download-artifact` step with its `path` moved under the member's directory. */
447
+ function memberArtifactStep(step, member) {
448
+ const uses = typeof step.uses === "string" ? step.uses : "";
449
+ if (!/^actions\/(upload|download)-artifact@/.test(uses))
450
+ return step;
451
+ const withProps = step.with;
452
+ if (!withProps || typeof withProps.path !== "string")
453
+ return step;
454
+ const path = withProps.path
455
+ .split("\n")
456
+ .map((line) => memberRepoPath(member, line))
457
+ .join("\n");
458
+ return { ...step, with: { ...withProps, path } };
459
+ }
460
+ /**
461
+ * Emit a `GithubPipelineDoc`'s `on`/`env`/`jobs` mappings as workflow YAML.
462
+ * Shared with the forgejo dialect (#969), which transforms the doc first.
463
+ */
464
+ export function emitPipelineYAML(doc) {
465
+ return emitWorkflowYAML({ name: doc.name, on: doc.on, env: doc.env, defaults: doc.defaults, jobsDoc: doc.jobsDoc });
466
+ }
467
+ /**
468
+ * Synthesize a `.github/workflows/*.yml` pipeline from a set of components:
469
+ * one job per component, `needs:` expressing the wave-ordered dependency DAG
470
+ * from `resolveComponentGraph`. Thin wrapper over {@link buildGithubPipelineDoc}
471
+ * + {@link emitPipelineYAML}.
472
+ */
473
+ export function generateGithubPipeline(components, options = {}) {
474
+ const doc = buildGithubPipelineDoc(components, options);
475
+ return { yaml: emitPipelineYAML(doc), stages: doc.stages, jobs: doc.jobs, env: doc.environment };
476
+ }
@@ -0,0 +1,17 @@
1
+ import { Composite, mergeDefaults } from "@intentius/chant";
2
+ import { Step } from "../generated/index.js";
3
+ export const CacheAction = Composite((props) => {
4
+ const { defaults } = props;
5
+ const withObj = {
6
+ path: props.path,
7
+ key: props.key,
8
+ };
9
+ if (props.restoreKeys !== undefined)
10
+ withObj["restore-keys"] = props.restoreKeys.join("\n");
11
+ const step = new Step(mergeDefaults({
12
+ name: "Cache",
13
+ uses: "actions/cache@v4",
14
+ with: withObj,
15
+ }, defaults?.step));
16
+ return { step };
17
+ }, "Cache");