@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,41 @@
1
+ /**
2
+ * GitHub Actions predefined context variable references.
3
+ *
4
+ * These provide type-safe access to GitHub and Runner context values
5
+ * that expand to `${{ context.property }}` expressions in YAML.
6
+ */
7
+ import { Expression } from "./expression.js";
8
+ export const GitHub = {
9
+ Ref: /* @__PURE__ */ new Expression("github.ref"),
10
+ RefName: /* @__PURE__ */ new Expression("github.ref_name"),
11
+ RefType: /* @__PURE__ */ new Expression("github.ref_type"),
12
+ Sha: /* @__PURE__ */ new Expression("github.sha"),
13
+ Actor: /* @__PURE__ */ new Expression("github.actor"),
14
+ TriggeringActor: /* @__PURE__ */ new Expression("github.triggering_actor"),
15
+ Repository: /* @__PURE__ */ new Expression("github.repository"),
16
+ RepositoryOwner: /* @__PURE__ */ new Expression("github.repository_owner"),
17
+ EventName: /* @__PURE__ */ new Expression("github.event_name"),
18
+ Event: /* @__PURE__ */ new Expression("github.event"),
19
+ RunId: /* @__PURE__ */ new Expression("github.run_id"),
20
+ RunNumber: /* @__PURE__ */ new Expression("github.run_number"),
21
+ RunAttempt: /* @__PURE__ */ new Expression("github.run_attempt"),
22
+ Workflow: /* @__PURE__ */ new Expression("github.workflow"),
23
+ WorkflowRef: /* @__PURE__ */ new Expression("github.workflow_ref"),
24
+ Workspace: /* @__PURE__ */ new Expression("github.workspace"),
25
+ Token: /* @__PURE__ */ new Expression("github.token"),
26
+ Job: /* @__PURE__ */ new Expression("github.job"),
27
+ HeadRef: /* @__PURE__ */ new Expression("github.head_ref"),
28
+ BaseRef: /* @__PURE__ */ new Expression("github.base_ref"),
29
+ ServerUrl: /* @__PURE__ */ new Expression("github.server_url"),
30
+ ApiUrl: /* @__PURE__ */ new Expression("github.api_url"),
31
+ GraphqlUrl: /* @__PURE__ */ new Expression("github.graphql_url"),
32
+ Action: /* @__PURE__ */ new Expression("github.action"),
33
+ ActionPath: /* @__PURE__ */ new Expression("github.action_path"),
34
+ };
35
+ export const Runner = {
36
+ Os: /* @__PURE__ */ new Expression("runner.os"),
37
+ Arch: /* @__PURE__ */ new Expression("runner.arch"),
38
+ Name: /* @__PURE__ */ new Expression("runner.name"),
39
+ Temp: /* @__PURE__ */ new Expression("runner.temp"),
40
+ ToolCache: /* @__PURE__ */ new Expression("runner.tool_cache"),
41
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-github",
3
- "version": "0.124.31",
3
+ "version": "0.124.32",
4
4
  "description": "GitHub Actions lexicon for chant — declarative IaC in TypeScript",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -32,17 +32,17 @@
32
32
  ".": {
33
33
  "development": "./src/index.ts",
34
34
  "types": "./dist/index.d.ts",
35
- "default": "./src/index.ts"
35
+ "default": "./dist/index.js"
36
36
  },
37
37
  "./*": {
38
38
  "development": "./src/*.ts",
39
39
  "types": "./dist/*.d.ts",
40
- "default": "./src/*.ts"
40
+ "default": "./dist/*.js"
41
41
  },
42
42
  "./lint/post-synth": {
43
43
  "development": "./src/lint/post-synth/index.ts",
44
44
  "types": "./dist/lint/post-synth/index.d.ts",
45
- "default": "./src/lint/post-synth/index.ts"
45
+ "default": "./dist/lint/post-synth/index.js"
46
46
  },
47
47
  "./manifest": "./dist/manifest.json",
48
48
  "./meta": "./dist/meta.json",
@@ -54,13 +54,13 @@
54
54
  "validate": "tsx src/validate-cli.ts",
55
55
  "docs": "tsx src/codegen/docs-cli.ts",
56
56
  "prepack": "npm run generate && npm run bundle && npm run validate && npm run build",
57
- "build": "../../scripts/tsc7.sh -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && find dist -type f \\( -name \"*.js\" -o -name \"*.js.map\" \\) -delete"
57
+ "build": "../../scripts/tsc7.sh -p tsconfig.build.json --sourceMap false && tsc-alias -p tsconfig.build.json"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@intentius/chant": "*",
61
61
  "typescript": "^5.9.3"
62
62
  },
63
63
  "peerDependencies": {
64
- "@intentius/chant": "^0.124.31"
64
+ "@intentius/chant": "^0.124.32"
65
65
  }
66
66
  }
@@ -5,7 +5,7 @@
5
5
  import { readFileSync } from "fs";
6
6
  import { githubPlugin } from "../plugin";
7
7
  import { join, dirname } from "path";
8
- import { fileURLToPath } from "url";
8
+ import { moduleSourceDir } from "@intentius/chant/module-source";
9
9
  import type { IntrinsicDef } from "@intentius/chant/lexicon";
10
10
  import {
11
11
  packagePipeline,
@@ -17,7 +17,7 @@ import { generate } from "./generate";
17
17
 
18
18
  export type { PackageOptions, PackageResult };
19
19
 
20
- const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
20
+ const pkgDir = dirname(moduleSourceDir(import.meta.url));
21
21
 
22
22
  /**
23
23
  * Package the GitHub Actions lexicon into a distributable BundleSpec.
@@ -34,7 +34,8 @@ jobs:
34
34
  BASE_SHA: '${{ github.event.pull_request.base.sha }}'
35
35
  PR_NUMBER: '${{ github.event.pull_request.number }}'
36
36
  GITHUB_TOKEN: '${{ github.token }}'
37
- run: chant components pr-plan --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --output .chant/pr --forge github
37
+ CHANT_RUNNER_KEY: '${{ secrets.CHANT_RUNNER_KEY }}'
38
+ run: chant ci run plan --in-place --no-push --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --forge github
38
39
  - name: Keep the plan report
39
40
  if: always()
40
41
  uses: actions/upload-artifact@v4
@@ -73,7 +74,10 @@ jobs:
73
74
  env:
74
75
  BASE_SHA: '${{ github.event.before }}'
75
76
  GITHUB_TOKEN: '${{ github.token }}'
76
- run: chant components pr-apply --base "$BASE_SHA" --env prod --gate pr-apply --output .chant/pr --forge github --require-review --resume .chant/pr-resume/pr-apply.json
77
+ CHANT_RUNNER_KEY: '${{ secrets.CHANT_RUNNER_KEY }}'
78
+ run: |
79
+ if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then git fetch origin "main" && BASE_SHA="$(git merge-base FETCH_HEAD "$GITHUB_SHA")"; if [ "$BASE_SHA" = "$GITHUB_SHA" ]; then BASE_SHA="$(git rev-parse --verify --quiet "$GITHUB_SHA^" || echo 4b825dc642cb6eb9a060e54bf8d69288fbee4904)"; fi; fi; export BASE_SHA
80
+ chant ci run apply --in-place --base "$BASE_SHA" --env prod --gate pr-apply --forge github
77
81
  - name: Keep the record for a re-run
78
82
  if: always()
79
83
  uses: actions/cache/save@v4
@@ -0,0 +1,178 @@
1
+ name: chant-pr-prod
2
+
3
+ on:
4
+ pull_request:
5
+ branches:
6
+ - main
7
+ push:
8
+ branches:
9
+ - main
10
+
11
+ env:
12
+ CHANT_ENV: prod
13
+
14
+ jobs:
15
+ plan:
16
+ if: github.event_name == 'pull_request'
17
+ runs-on: ubuntu-latest
18
+ container: node:22
19
+ permissions:
20
+ contents: read
21
+ pull-requests: write
22
+ statuses: write
23
+ steps:
24
+ - uses: actions/checkout@v7
25
+ with:
26
+ fetch-depth: 0
27
+ - run: command -v tofu >/dev/null 2>&1 || (curl -fsSL https://get.opentofu.org/install-opentofu.sh -o /tmp/install-opentofu.sh && sh /tmp/install-opentofu.sh --install-method standalone)
28
+ - name: Plan the members this pull request reaches
29
+ env:
30
+ BASE_SHA: '${{ github.event.pull_request.base.sha }}'
31
+ PR_NUMBER: '${{ github.event.pull_request.number }}'
32
+ GITHUB_TOKEN: '${{ github.token }}'
33
+ CHANT_RUNNER_KEY: '${{ secrets.CHANT_RUNNER_KEY }}'
34
+ run: chant ci run plan --in-place --no-push --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --forge github --wave-gate release
35
+ - name: Keep the plan report
36
+ if: always()
37
+ uses: actions/upload-artifact@v4
38
+ with:
39
+ name: chant-pr-plan
40
+ path: .chant/pr
41
+ if-no-files-found: ignore
42
+ include-hidden-files: true
43
+ apply-wave-1:
44
+ if: github.event_name == 'push'
45
+ runs-on: ubuntu-latest
46
+ container: node:22
47
+ concurrency:
48
+ group: chant-apply-prod
49
+ cancel-in-progress: false
50
+ permissions:
51
+ contents: write
52
+ pull-requests: write
53
+ statuses: write
54
+ steps:
55
+ - uses: actions/checkout@v7
56
+ with:
57
+ fetch-depth: 0
58
+ - run: command -v tofu >/dev/null 2>&1 || (curl -fsSL https://get.opentofu.org/install-opentofu.sh -o /tmp/install-opentofu.sh && sh /tmp/install-opentofu.sh --install-method standalone)
59
+ - name: Restore the newest record of this commit's apply
60
+ uses: actions/cache/restore@v4
61
+ with:
62
+ path: .chant/pr-resume/pr-apply.json
63
+ key: chant-apply-prod-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}-wave-1
64
+ restore-keys: chant-apply-prod-${{ github.sha }}-
65
+ - name: Apply wave 1, under the approval its plan has
66
+ env:
67
+ BASE_SHA: '${{ github.event.before }}'
68
+ GITHUB_TOKEN: '${{ github.token }}'
69
+ CHANT_RUNNER_KEY: '${{ secrets.CHANT_RUNNER_KEY }}'
70
+ run: |
71
+ if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then git fetch origin "main" && BASE_SHA="$(git merge-base FETCH_HEAD "$GITHUB_SHA")"; if [ "$BASE_SHA" = "$GITHUB_SHA" ]; then BASE_SHA="$(git rev-parse --verify --quiet "$GITHUB_SHA^" || echo 4b825dc642cb6eb9a060e54bf8d69288fbee4904)"; fi; fi; export BASE_SHA
72
+ chant ci run apply --in-place --base "$BASE_SHA" --env prod --gate pr-apply --forge github --wave-gate release --wave 1
73
+ - name: Keep the record for the next wave and a re-run
74
+ if: always()
75
+ uses: actions/cache/save@v4
76
+ with:
77
+ path: .chant/pr-resume/pr-apply.json
78
+ key: chant-apply-prod-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}-wave-1
79
+ - name: Keep the wave 1 apply report
80
+ if: always()
81
+ uses: actions/upload-artifact@v4
82
+ with:
83
+ name: chant-pr-apply-wave-1
84
+ path: .chant/pr
85
+ if-no-files-found: ignore
86
+ include-hidden-files: true
87
+ apply-wave-2:
88
+ if: github.event_name == 'push'
89
+ runs-on: ubuntu-latest
90
+ needs:
91
+ - apply-wave-1
92
+ container: node:22
93
+ concurrency:
94
+ group: chant-apply-prod
95
+ cancel-in-progress: false
96
+ permissions:
97
+ contents: write
98
+ pull-requests: write
99
+ statuses: write
100
+ steps:
101
+ - uses: actions/checkout@v7
102
+ with:
103
+ fetch-depth: 0
104
+ - run: command -v tofu >/dev/null 2>&1 || (curl -fsSL https://get.opentofu.org/install-opentofu.sh -o /tmp/install-opentofu.sh && sh /tmp/install-opentofu.sh --install-method standalone)
105
+ - name: Restore the newest record of this commit's apply
106
+ uses: actions/cache/restore@v4
107
+ with:
108
+ path: .chant/pr-resume/pr-apply.json
109
+ key: chant-apply-prod-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}-wave-2
110
+ restore-keys: chant-apply-prod-${{ github.sha }}-
111
+ - name: Apply wave 2, under the approval its plan has
112
+ env:
113
+ BASE_SHA: '${{ github.event.before }}'
114
+ GITHUB_TOKEN: '${{ github.token }}'
115
+ CHANT_RUNNER_KEY: '${{ secrets.CHANT_RUNNER_KEY }}'
116
+ run: |
117
+ if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then git fetch origin "main" && BASE_SHA="$(git merge-base FETCH_HEAD "$GITHUB_SHA")"; if [ "$BASE_SHA" = "$GITHUB_SHA" ]; then BASE_SHA="$(git rev-parse --verify --quiet "$GITHUB_SHA^" || echo 4b825dc642cb6eb9a060e54bf8d69288fbee4904)"; fi; fi; export BASE_SHA
118
+ chant ci run apply --in-place --base "$BASE_SHA" --env prod --gate pr-apply --forge github --wave-gate release --wave 2
119
+ - name: Keep the record for the next wave and a re-run
120
+ if: always()
121
+ uses: actions/cache/save@v4
122
+ with:
123
+ path: .chant/pr-resume/pr-apply.json
124
+ key: chant-apply-prod-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}-wave-2
125
+ - name: Keep the wave 2 apply report
126
+ if: always()
127
+ uses: actions/upload-artifact@v4
128
+ with:
129
+ name: chant-pr-apply-wave-2
130
+ path: .chant/pr
131
+ if-no-files-found: ignore
132
+ include-hidden-files: true
133
+ apply-wave-3:
134
+ if: github.event_name == 'push'
135
+ runs-on: ubuntu-latest
136
+ needs:
137
+ - apply-wave-2
138
+ container: node:22
139
+ concurrency:
140
+ group: chant-apply-prod
141
+ cancel-in-progress: false
142
+ permissions:
143
+ contents: write
144
+ pull-requests: write
145
+ statuses: write
146
+ steps:
147
+ - uses: actions/checkout@v7
148
+ with:
149
+ fetch-depth: 0
150
+ - run: command -v tofu >/dev/null 2>&1 || (curl -fsSL https://get.opentofu.org/install-opentofu.sh -o /tmp/install-opentofu.sh && sh /tmp/install-opentofu.sh --install-method standalone)
151
+ - name: Restore the newest record of this commit's apply
152
+ uses: actions/cache/restore@v4
153
+ with:
154
+ path: .chant/pr-resume/pr-apply.json
155
+ key: chant-apply-prod-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}-wave-3
156
+ restore-keys: chant-apply-prod-${{ github.sha }}-
157
+ - name: Apply wave 3, under the approval its plan has
158
+ env:
159
+ BASE_SHA: '${{ github.event.before }}'
160
+ GITHUB_TOKEN: '${{ github.token }}'
161
+ CHANT_RUNNER_KEY: '${{ secrets.CHANT_RUNNER_KEY }}'
162
+ run: |
163
+ if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then git fetch origin "main" && BASE_SHA="$(git merge-base FETCH_HEAD "$GITHUB_SHA")"; if [ "$BASE_SHA" = "$GITHUB_SHA" ]; then BASE_SHA="$(git rev-parse --verify --quiet "$GITHUB_SHA^" || echo 4b825dc642cb6eb9a060e54bf8d69288fbee4904)"; fi; fi; export BASE_SHA
164
+ chant ci run apply --in-place --base "$BASE_SHA" --env prod --gate pr-apply --forge github --wave-gate release --wave 3
165
+ - name: Keep the record for the next wave and a re-run
166
+ if: always()
167
+ uses: actions/cache/save@v4
168
+ with:
169
+ path: .chant/pr-resume/pr-apply.json
170
+ key: chant-apply-prod-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}-wave-3
171
+ - name: Keep the wave 3 apply report
172
+ if: always()
173
+ uses: actions/upload-artifact@v4
174
+ with:
175
+ name: chant-pr-apply-wave-3
176
+ path: .chant/pr
177
+ if-no-files-found: ignore
178
+ include-hidden-files: true
@@ -30,7 +30,8 @@ jobs:
30
30
  BASE_SHA: '${{ github.event.pull_request.base.sha }}'
31
31
  PR_NUMBER: '${{ github.event.pull_request.number }}'
32
32
  GITHUB_TOKEN: '${{ github.token }}'
33
- run: chant components pr-plan --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --output .chant/pr --forge github
33
+ CHANT_RUNNER_KEY: '${{ secrets.CHANT_RUNNER_KEY }}'
34
+ run: chant ci run plan --in-place --no-push --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --forge github
34
35
  - name: Keep the plan report
35
36
  if: always()
36
37
  uses: actions/upload-artifact@v4
@@ -65,7 +66,10 @@ jobs:
65
66
  env:
66
67
  BASE_SHA: '${{ github.event.before }}'
67
68
  GITHUB_TOKEN: '${{ github.token }}'
68
- run: chant components pr-apply --base "$BASE_SHA" --env prod --gate pr-apply --output .chant/pr --forge github --require-review --resume .chant/pr-resume/pr-apply.json
69
+ CHANT_RUNNER_KEY: '${{ secrets.CHANT_RUNNER_KEY }}'
70
+ run: |
71
+ if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then git fetch origin "main" && BASE_SHA="$(git merge-base FETCH_HEAD "$GITHUB_SHA")"; if [ "$BASE_SHA" = "$GITHUB_SHA" ]; then BASE_SHA="$(git rev-parse --verify --quiet "$GITHUB_SHA^" || echo 4b825dc642cb6eb9a060e54bf8d69288fbee4904)"; fi; fi; export BASE_SHA
72
+ chant ci run apply --in-place --base "$BASE_SHA" --env prod --gate pr-apply --forge github
69
73
  - name: Keep the record for a re-run
70
74
  if: always()
71
75
  uses: actions/cache/save@v4
@@ -60,7 +60,17 @@ 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 { PR_APPLY_RECORD, PR_LOOP_REPORT_DIR, prApplyGroup, prApplyRecordKey, prLoopJobs, prLoopSetup } from "@intentius/chant/components/pr-pipeline";
63
+ import {
64
+ PR_APPLY_RECORD,
65
+ PR_LOOP_REPORT_DIR,
66
+ prApplyGroup,
67
+ prApplyRecordKey,
68
+ prApplyWaveJobName,
69
+ prLoopJobs,
70
+ prLoopSetup,
71
+ prLoopWaveJobs,
72
+ prLoopWithWaves,
73
+ } from "@intentius/chant/components/pr-pipeline";
64
74
  import { memberRepoPath } from "@intentius/chant/lexicon";
65
75
  import type {
66
76
  ComponentPipelineJob as GeneratedJob,
@@ -175,8 +185,17 @@ function gatedWaveGithubDoc(
175
185
  * path filter, since a change outside the member can reach it; the plan
176
186
  * selects from the whole change and passes `--member`, which keeps the
177
187
  * member's gate, note and statuses apart from the other members'.
188
+ *
189
+ * With a gate per wave (`waveGate`, #4003) the one apply job becomes
190
+ * `apply-wave-1` to `apply-wave-<n>`, one per wave of the component graph,
191
+ * each needing the one before. Each saves the record under its own cache key
192
+ * (the run, the attempt and the wave), and each restores the newest record
193
+ * for the pushed commit, so wave k reads what wave k-1 left, and a re-run of
194
+ * a wave that stopped at its gate reads what it left itself. Each keeps its
195
+ * report as `chant-pr-apply-wave-<k>`.
178
196
  */
179
197
  function prLoopGithubDoc(
198
+ components: DriverComponent[],
180
199
  env: string,
181
200
  image: string,
182
201
  beforeScript: string[],
@@ -194,34 +213,53 @@ function prLoopGithubDoc(
194
213
  // its run and attempt, and a re-run restores the newest for the commit.
195
214
  const recordPath = member ? memberRepoPath(member, PR_APPLY_RECORD) : PR_APPLY_RECORD;
196
215
  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",
216
+ const recordKey = (wave?: number) => `${recordPrefix}\${{ github.run_id }}-\${{ github.run_attempt }}${wave ? `-wave-${wave}` : ""}`;
217
+ const restoreRecord = (wave?: number) => ({
218
+ name: wave ? "Restore the newest record of this commit's apply" : "Restore the record of an earlier attempt at this commit",
200
219
  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",
220
+ with: { path: recordPath, key: recordKey(wave), "restore-keys": recordPrefix },
221
+ });
222
+ const saveRecord = (wave?: number) => ({
223
+ name: wave ? "Keep the record for the next wave and a re-run" : "Keep the record for a re-run",
205
224
  if: "always()",
206
225
  uses: "actions/cache/save@v4",
207
- with: { path: recordPath, key: recordKey },
208
- };
226
+ with: { path: recordPath, key: recordKey(wave) },
227
+ });
228
+ const reportName = (job: typeof plan) => `chant-pr-${job.wave ? prApplyWaveJobName(job.wave) : job.jobName}`;
209
229
  const steps = (job: typeof plan, name: string): Array<Record<string, unknown>> => [
210
230
  checkoutStep(true),
211
231
  ...runSteps(prLoopSetup(image)),
212
232
  ...runSteps(beforeScript),
213
233
  ...(job.build ? [{ name: "Build the templates", run: job.build[0] }, { name: "Lint the project", run: job.build[1] }] : []),
214
- ...(job.jobName === "apply" ? [restoreRecord] : []),
215
- { name, env: job.env, run: job.command },
234
+ ...(job.jobName === "apply" ? [restoreRecord(job.wave)] : []),
235
+ // The job's setup lines (#4004) run in the command's shell, so a value they settle reaches it.
236
+ // The command runs through chant ci run (#4156), which writes the job's run record, signed when the CHANT_RUNNER_KEY secret is set.
237
+ { name, env: job.run.env, run: [...(job.setup ?? []), job.run.command].join("\n") },
216
238
  ...runSteps(extraScript),
217
- ...(job.jobName === "apply" ? [saveRecord] : []),
239
+ ...(job.jobName === "apply" ? [saveRecord(job.wave)] : []),
218
240
  {
219
- name: `Keep the ${job.jobName} report`,
241
+ name: `Keep the ${job.wave ? `wave ${job.wave} ` : ""}${job.jobName} report`,
220
242
  if: "always()",
221
243
  uses: UPLOAD_ARTIFACT,
222
- with: { name: `chant-pr-${job.jobName}`, path: reportPath, "if-no-files-found": "ignore", "include-hidden-files": true },
244
+ with: { name: reportName(job), path: reportPath, "if-no-files-found": "ignore", "include-hidden-files": true },
223
245
  },
224
246
  ];
247
+ const applyJob = (job: typeof plan, needs: string[], name: string) => ({
248
+ if: "github.event_name == 'push'",
249
+ "runs-on": GITHUB_RUNNER,
250
+ ...(needs.length > 0 ? { needs } : {}),
251
+ container: image,
252
+ concurrency: concurrency(prApplyGroup(env, member?.name)),
253
+ permissions: { contents: "write", "pull-requests": "write", statuses: "write" },
254
+ steps: steps(job, name),
255
+ });
256
+ const waveJobs = loop.waveGate ? prLoopWaveJobs(components, apply) : [];
257
+ const applyJobs: Array<[string, Record<string, unknown>]> = loop.waveGate
258
+ ? waveJobs.map((job) => [
259
+ prApplyWaveJobName(job.wave!),
260
+ applyJob(job, job.wave! > 1 ? [prApplyWaveJobName(job.wave! - 1)] : [], `Apply wave ${job.wave}, under the approval its plan has`),
261
+ ])
262
+ : [["apply", applyJob(apply, [], "Apply the plan a reviewer approved")]];
225
263
  const jobsDoc: Record<string, unknown> = {
226
264
  plan: {
227
265
  if: "github.event_name == 'pull_request'",
@@ -230,15 +268,9 @@ function prLoopGithubDoc(
230
268
  permissions: { contents: "read", "pull-requests": "write", statuses: "write" },
231
269
  steps: steps(plan, "Plan the members this pull request reaches"),
232
270
  },
233
- apply: {
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
- },
271
+ ...Object.fromEntries(applyJobs),
241
272
  };
273
+ const applyNames = applyJobs.map(([name]) => name);
242
274
  return {
243
275
  name: member ? `chant-pr.${member.name}.${env}` : `chant-pr-${env}`,
244
276
  environment: env,
@@ -246,10 +278,15 @@ function prLoopGithubDoc(
246
278
  ...(rooted ? {} : { defaults: { run: { "working-directory": member!.dir } } }),
247
279
  env: { ...options.variables, CHANT_ENV: env },
248
280
  jobsDoc,
249
- stages: ["plan", "apply"],
281
+ stages: ["plan", ...applyNames],
250
282
  jobs: [
251
283
  { jobName: "plan", component: "pull request plan", stage: "plan", needs: [] },
252
- { jobName: "apply", component: "pull request apply", stage: "apply", needs: [] },
284
+ ...applyNames.map((name, i) => ({
285
+ jobName: name,
286
+ component: loop.waveGate ? `pull request apply, wave ${i + 1}` : "pull request apply",
287
+ stage: name,
288
+ needs: i > 0 ? [applyNames[i - 1]] : [],
289
+ })),
253
290
  ],
254
291
  };
255
292
  }
@@ -315,10 +352,10 @@ export function buildGithubPipelineDoc(
315
352
  const byName = new Map(components.map((c) => [c.name, c]));
316
353
 
317
354
  if (options.prLoop) {
318
- if (options.gatedWaves || options.promoteTo !== undefined) {
319
- throw new Error("a pull-request pipeline has no wave or promote jobs; drop --wave-gate and --promote-to, or --pr-loop");
320
- }
321
- return prLoopGithubDoc(env, image, beforeScript, extraScript, options);
355
+ if (options.promoteTo !== undefined) throw new Error("a pull-request pipeline has no promote job; drop --promote-to or --pr-loop");
356
+ // #4003: --wave-gate beside --pr-loop gives the apply a gate per wave.
357
+ const prLoop = prLoopWithWaves(options.prLoop, options.gatedWaves);
358
+ return prLoopGithubDoc(components, env, image, beforeScript, extraScript, { ...options, prLoop });
322
359
  }
323
360
 
324
361
  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[] = [
@@ -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("pr-plan"))!;
67
- expect(plan.run).toBe('chant components pr-plan --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --output .chant/pr --forge github');
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("pr-apply"))!;
72
- expect(apply.run).toContain("--require-review");
73
- expect(apply.env).toMatchObject({ BASE_SHA: "${{ github.event.before }}" });
74
+ const apply = doc.jobs.apply.steps.find((s) => s.run?.includes("ci run apply"))!;
75
+ // ci run builds pr-apply with --require-review unless told --no-require-review.
76
+ expect(apply.run).not.toContain("--no-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: { 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("pr-apply"));
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
- expect(steps[run].run).toContain("--resume .chant/pr-resume/pr-apply.json");
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("pr-plan"))!.run).not.toContain("--resume");
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");
@@ -109,18 +130,20 @@ describe("the GitHub pull-request workflow", () => {
109
130
  const yaml = generateGithubPipeline(ESTATE, { env: "prod", prLoop: { gate: "infra-apply", branch: "trunk", requireReview: false } }).yaml;
110
131
  expect(yaml).toContain("--gate infra-apply");
111
132
  expect(yaml).toContain("- trunk");
112
- expect(yaml).not.toContain("--require-review");
133
+ expect(yaml).toContain("--no-require-review");
134
+ expect(yaml).toContain('--branch "trunk"');
113
135
  });
114
136
 
115
- test("refuses a wave or promote job beside it", () => {
116
- expect(() => generateGithubPipeline(ESTATE, { prLoop: {}, gatedWaves: { gate: "g" } })).toThrow(/pull-request pipeline has no wave or promote jobs/);
117
- expect(() => generateGithubPipeline(ESTATE, { prLoop: {}, promoteTo: "prod" })).toThrow(/pull-request pipeline has no wave or promote jobs/);
137
+ test("refuses a promote job beside it, and a canary or a fixed base beside --wave-gate (#4003)", () => {
138
+ expect(() => generateGithubPipeline(ESTATE, { prLoop: {}, promoteTo: "prod" })).toThrow(/pull-request pipeline has no promote job/);
139
+ expect(() => generateGithubPipeline(ESTATE, { prLoop: {}, gatedWaves: { gate: "g", canary: ["net"] } })).toThrow(/takes no --canary/);
140
+ expect(() => generateGithubPipeline(ESTATE, { prLoop: {}, 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
144
  const loud = parseYAML(generateGithubPipeline(ESTATE, { env: "prod", prLoop: { 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?.startsWith("chant components"))!;
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
  }
@@ -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("pr-plan"))!;
146
- expect(plan.run).toBe('chant components pr-plan --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --output .chant/pr --forge github --member network');
147
- const apply = mdoc.jobs.apply.steps.find((s) => s.run?.includes("pr-apply"))!;
148
- expect(apply.run).toContain("--member network");
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("--resume .chant/pr-resume/pr-apply.json");
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)) {
@@ -172,7 +196,7 @@ describe("the GitHub pull-request workflow", () => {
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("--member root");
199
+ expect(yaml).toContain("chant ci run plan --in-place");
176
200
  expect(yaml).toContain("path: .chant/pr");
177
201
  });
178
202