@intentius/chant-lexicon-github 0.61.0 → 0.63.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/generate-op-pipeline.d.ts +29 -1
- package/dist/components/generate-op-pipeline.d.ts.map +1 -1
- package/dist/composites/pr-plan-report.d.ts +3 -1
- package/dist/composites/pr-plan-report.d.ts.map +1 -1
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/package.json +2 -2
- package/src/components/generate-op-pipeline.test.ts +130 -2
- package/src/components/generate-op-pipeline.ts +113 -16
- package/src/composites/pr-plan-report.test.ts +26 -2
- package/src/composites/pr-plan-report.ts +35 -15
|
@@ -57,6 +57,17 @@
|
|
|
57
57
|
* environment protection is repository configuration, not a token scope — and
|
|
58
58
|
* {@link assertEnvironment} refuses only what would emit as configured and
|
|
59
59
|
* bind nothing.
|
|
60
|
+
*
|
|
61
|
+
* A fourth is per-Op credentials (#2290): a spec's `variables` emits the Op's
|
|
62
|
+
* own job-level `env:`, beside (and layered over, on a key collision)
|
|
63
|
+
* `options.variables`'s workflow-level `env:`. `options.variables` keeps
|
|
64
|
+
* meaning what it always has — set once, landing on every generated file — so
|
|
65
|
+
* a caller who declares nothing per-Op sees byte-identical output; a spec's
|
|
66
|
+
* own `variables` is the way one Op's job can hold a credential no other Op's
|
|
67
|
+
* job receives, which a workflow-level declaration can never express because
|
|
68
|
+
* every generated file here has exactly the one Op job (plus, on a `push`
|
|
69
|
+
* trigger, the gate-notice job beside it — job-level `env:` does not reach
|
|
70
|
+
* that job either, same as `environment:` above does not).
|
|
60
71
|
*/
|
|
61
72
|
import type { ComponentPipelineOptions as GenerateGithubOpOptions, OpEnvironment, OpPipelineJob, OpPipelineResult as GenerateGithubOpResult, OpSetupStep, OpTrigger, ScheduledOpSpec } from "@intentius/chant/lexicon";
|
|
62
73
|
export type { GenerateGithubOpOptions, GenerateGithubOpResult };
|
|
@@ -157,6 +168,23 @@ export declare function assertEnvironment(name: string, environment: OpEnvironme
|
|
|
157
168
|
* push run, grants write access no step in the generated job can spend.
|
|
158
169
|
*/
|
|
159
170
|
export declare function mergePermissions(name: string, base: Record<string, "read" | "write">, additive: Record<string, "read" | "write">, trigger: OpTrigger): Record<string, "read" | "write">;
|
|
171
|
+
/**
|
|
172
|
+
* Internal knobs beside the public {@link GenerateGithubOpOptions} — not part
|
|
173
|
+
* of that type because they are not something a project author sets, only
|
|
174
|
+
* something a dialect on top of this builder (forgejo, #2294) needs to
|
|
175
|
+
* change about the shape this function emits for every spec alike.
|
|
176
|
+
*/
|
|
177
|
+
export interface BuildGithubOpPipelineDocsInternalOptions {
|
|
178
|
+
/**
|
|
179
|
+
* Whether a `push` job's outputs (`gated`/`op`/`gate`/`approve`) and the
|
|
180
|
+
* `node -e` step that writes them to `$GITHUB_OUTPUT` are emitted (#2294).
|
|
181
|
+
* Default true (github's own behavior, unchanged): the gate-notice job
|
|
182
|
+
* beside it reads them. The forgejo generator passes `false` — it never
|
|
183
|
+
* carries a gate-notice job (`gatedNoticeDoc` never crosses the dialect),
|
|
184
|
+
* so nothing would ever read them there.
|
|
185
|
+
*/
|
|
186
|
+
emitGatedOutputs?: boolean;
|
|
187
|
+
}
|
|
160
188
|
/**
|
|
161
189
|
* Build one `GithubOpPipelineDoc` per scheduled Op: its trigger, its `setup`
|
|
162
190
|
* steps, least-privilege `permissions:` for its finding-mode plus whatever
|
|
@@ -169,7 +197,7 @@ export declare function mergePermissions(name: string, base: Record<string, "rea
|
|
|
169
197
|
* assertSetupSteps}), or a `permissions` entry that is not additive ({@link
|
|
170
198
|
* mergePermissions}).
|
|
171
199
|
*/
|
|
172
|
-
export declare function buildGithubOpPipelineDocs(ops: ScheduledOpSpec[], options?: GenerateGithubOpOptions): {
|
|
200
|
+
export declare function buildGithubOpPipelineDocs(ops: ScheduledOpSpec[], options?: GenerateGithubOpOptions, internalOptions?: BuildGithubOpPipelineDocsInternalOptions): {
|
|
173
201
|
files: GithubOpPipelineFile[];
|
|
174
202
|
jobs: OpPipelineJob[];
|
|
175
203
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-op-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-op-pipeline.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"generate-op-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-op-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsEG;AAIH,OAAO,KAAK,EACV,wBAAwB,IAAI,uBAAuB,EACnD,aAAa,EAEb,aAAa,EACb,gBAAgB,IAAI,sBAAsB,EAC1C,WAAW,EACX,SAAS,EACT,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAElC,YAAY,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,CAAC;AAEhE;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB;;;;OAIG;IACH,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC5B,2DAA2D;IAC3D,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,6DAA6D;IAC7D,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,8DAA8D;IAC9D,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC;;;;;;;;;OASG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC1C;AAED,qFAAqF;AACrF,MAAM,WAAW,oBAAoB;IACnC,4FAA4F;IAC5F,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,mBAAmB,CAAC;CAC1B;AAmUD;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,GAAG,IAAI,CA8BzE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,aAAa,GAAG,IAAI,CAwBhF;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,EACtC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,EAC1C,OAAO,EAAE,SAAS,GACjB,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,CAqClC;AA6BD;;;;;GAKG;AACH,MAAM,WAAW,wCAAwC;IACvD;;;;;;;OAOG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,eAAe,EAAE,EACtB,OAAO,GAAE,uBAA4B,EACrC,eAAe,GAAE,wCAA6C,GAC7D;IAAE,KAAK,EAAE,oBAAoB,EAAE,CAAC;IAAC,IAAI,EAAE,aAAa,EAAE,CAAA;CAAE,CA6F1D;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,mBAAmB,GAAG,MAAM,CAenE;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,eAAe,EAAE,EACtB,OAAO,GAAE,uBAA4B,GACpC,sBAAsB,CAMxB"}
|
|
@@ -51,7 +51,9 @@ export interface PrPlanReportProps {
|
|
|
51
51
|
* proved (#1223), reused here rather than reinvented. The forgejo lexicon
|
|
52
52
|
* inherits this composite through its github re-export, since Forgejo Actions
|
|
53
53
|
* runs the same workflow shape and the Forgejo API accepts the same `gh api`
|
|
54
|
-
* calls against its GitHub-compatible surface
|
|
54
|
+
* calls against its GitHub-compatible surface — once they target a full URL
|
|
55
|
+
* rather than a bare relative path, which `stickyCommentScript` now does
|
|
56
|
+
* (#2305, the same fix #2291 made for `reconcilePr`'s `postOrUpdateComment`).
|
|
55
57
|
*
|
|
56
58
|
* Only meaningful on a `pull_request`-triggered workflow — the job guards on
|
|
57
59
|
* `github.event_name` since the comment targets `github.event.number`, which
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pr-plan-report.d.ts","sourceRoot":"","sources":["../../src/composites/pr-plan-report.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,GAAG,EAAqB,MAAM,oBAAoB,CAAC;AAI5D,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,4EAA4E;IAC5E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kEAAkE;IAClE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,+CAA+C;IAC/C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+DAA+D;IAC/D,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6DAA6D;IAC7D,QAAQ,CAAC,EAAE;QACT,GAAG,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;KACrD,CAAC;CACH;
|
|
1
|
+
{"version":3,"file":"pr-plan-report.d.ts","sourceRoot":"","sources":["../../src/composites/pr-plan-report.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,GAAG,EAAqB,MAAM,oBAAoB,CAAC;AAI5D,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,4EAA4E;IAC5E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kEAAkE;IAClE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,+CAA+C;IAC/C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+DAA+D;IAC/D,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6DAA6D;IAC7D,QAAQ,CAAC,EAAE;QACT,GAAG,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;KACrD,CAAC;CACH;AA2CD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,YAAY;;EAkEP,CAAC"}
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "ee70227500205251bb2525d85be15c82185d173f1942686400135abc08e56786",
|
|
5
5
|
"meta.json": "dc3977afc2b6ddc4904de906d1b1e448b786b2aced9d82f8eade2ff915203344",
|
|
6
6
|
"types/index.d.ts": "f207946d7ab52f712d0c09995366440a6bba8b39358057b0da38ff9c0acca429",
|
|
7
7
|
"rules/deprecated-action-version.ts": "d41e6e532ab7f623af1bee4ac5279fcb2baada7defa1c5d022a5bc71983e8797",
|
|
@@ -77,5 +77,5 @@
|
|
|
77
77
|
"skills/chant-github-patterns.md": "bb3abef289a8fdfcf07d6bb2d7289dcb2f38bc0cb0321ea320b78b45a6f548c0",
|
|
78
78
|
"skills/chant-github-security.md": "aab111cb0871cad30281ce48d7da23663689619351029219e2be019a1a61e394"
|
|
79
79
|
},
|
|
80
|
-
"composite": "
|
|
80
|
+
"composite": "241cea113916c7340ca4cb0383b1e1c909eff2a6d4ffa0bb99b957499e41c71c"
|
|
81
81
|
}
|
package/dist/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-github",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.63.0",
|
|
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",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"typescript": "^5.9.3"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
|
-
"@intentius/chant": "^0.
|
|
64
|
+
"@intentius/chant": "^0.63.0",
|
|
65
65
|
"typescript": "^5.9.3"
|
|
66
66
|
}
|
|
67
67
|
}
|
|
@@ -21,6 +21,7 @@ interface ParsedStep {
|
|
|
21
21
|
id?: string;
|
|
22
22
|
uses?: string;
|
|
23
23
|
run?: string;
|
|
24
|
+
shell?: string;
|
|
24
25
|
env?: Record<string, string>;
|
|
25
26
|
}
|
|
26
27
|
|
|
@@ -32,6 +33,7 @@ interface ParsedJob {
|
|
|
32
33
|
if?: string;
|
|
33
34
|
permissions?: Record<string, string>;
|
|
34
35
|
outputs?: Record<string, string>;
|
|
36
|
+
env?: Record<string, string>;
|
|
35
37
|
steps: ParsedStep[];
|
|
36
38
|
}
|
|
37
39
|
|
|
@@ -494,11 +496,31 @@ describe("generateGithubOpPipeline: the gated apply on push (#2243)", () => {
|
|
|
494
496
|
// container image does not.
|
|
495
497
|
expect(notice.container).toBeUndefined();
|
|
496
498
|
const script = notice.steps[0].run ?? "";
|
|
497
|
-
expect(script).toContain('gh api "repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA/pulls"');
|
|
499
|
+
expect(script).toContain('gh api "$api_base/repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA/pulls"');
|
|
498
500
|
expect(script).toContain('marker="<!-- chant-gate:$CHANT_OP -->"');
|
|
499
501
|
expect(script).toContain("gh issue create");
|
|
500
502
|
});
|
|
501
503
|
|
|
504
|
+
// chant #2305 — same anti-pattern `stickyCommentScript`
|
|
505
|
+
// (`lexicons/github/src/composites/pr-plan-report.ts`) and `reconcilePr`'s
|
|
506
|
+
// `postOrUpdateComment` (#2291) had: a bare relative path, which `gh`
|
|
507
|
+
// resolves against `/api/v3` for any host but github.com. This job never
|
|
508
|
+
// reaches Forgejo today (the forgejo generator never carries
|
|
509
|
+
// `gatedNoticeDoc` across the dialect, #2294), so the fix changes nothing
|
|
510
|
+
// observable on github.com or GHES — it only stops a future Forgejo notice
|
|
511
|
+
// job from inheriting the same bug.
|
|
512
|
+
test("the notice job's gh api calls resolve their base from $GITHUB_API_URL, not a bare path (#2305)", () => {
|
|
513
|
+
const script = pushDoc().jobs!["app-apply-gate-notice"].steps[0].run ?? "";
|
|
514
|
+
expect(script).toContain('api_base="${GITHUB_API_URL%/}"');
|
|
515
|
+
expect(script).toContain('api_base="${api_base:-https://api.github.com}"');
|
|
516
|
+
expect(script).toContain('gh api "$api_base/repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA/pulls"');
|
|
517
|
+
expect(script).toContain('gh api "$api_base/repos/$GITHUB_REPOSITORY/issues/$pr/comments"');
|
|
518
|
+
expect(script).toContain('gh api --method PATCH "$api_base/repos/$GITHUB_REPOSITORY/issues/comments/$id"');
|
|
519
|
+
expect(script).toContain('gh api --method POST "$api_base/repos/$GITHUB_REPOSITORY/issues/$pr/comments"');
|
|
520
|
+
// No bare-path call survives.
|
|
521
|
+
expect(script).not.toMatch(/gh api[^\n]*"repos\//);
|
|
522
|
+
});
|
|
523
|
+
|
|
502
524
|
test("the notice job's permissions are its two posting paths and the lookup", () => {
|
|
503
525
|
const notice = pushDoc().jobs!["app-apply-gate-notice"];
|
|
504
526
|
expect(notice.permissions).toEqual({
|
|
@@ -513,7 +535,49 @@ describe("generateGithubOpPipeline: the gated apply on push (#2243)", () => {
|
|
|
513
535
|
|
|
514
536
|
test("a failing run stays a failing job: the pipe cannot swallow its exit code", () => {
|
|
515
537
|
const step = pushDoc().jobs!["app-apply"].steps.find((s) => s.id === "chant-run");
|
|
516
|
-
expect(step?.run).toContain(
|
|
538
|
+
expect(step?.run).toContain(
|
|
539
|
+
'{ chant run app-apply --gated-exit 0 --json; echo "$?" >"$status"; } | tee "$json"',
|
|
540
|
+
);
|
|
541
|
+
expect(step?.run).toContain('code=$(cat "$status")');
|
|
542
|
+
expect(step?.run).toContain('[ "$code" -eq 0 ] || exit "$code"');
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* chant #2299 — every Op job carries `container:` (this generator sets one
|
|
547
|
+
* unconditionally, default `node:22-slim`), and a container job's default
|
|
548
|
+
* shell on GitHub Actions is `sh`, not bash. `sh` rejects `set -o pipefail`
|
|
549
|
+
* outright (`Illegal option -o pipefail`) and fails the step before `chant`
|
|
550
|
+
* is ever reached, which is exactly what happened on a real run (choudoufu
|
|
551
|
+
* #1026, run 34312967579). The exit-code capture above needs neither
|
|
552
|
+
* `pipefail` nor a `shell:` override to survive that — it is plain POSIX
|
|
553
|
+
* `sh`, so the default shell a `container:` job gets already runs it.
|
|
554
|
+
*/
|
|
555
|
+
test("the run step needs no shell override — its script is POSIX sh, not bash (#2299)", () => {
|
|
556
|
+
const doc = pushDoc();
|
|
557
|
+
const job = doc.jobs!["app-apply"];
|
|
558
|
+
expect(job.container).toBe("node:22-slim");
|
|
559
|
+
const step = job.steps.find((s) => s.id === "chant-run");
|
|
560
|
+
expect(step?.shell).toBeUndefined();
|
|
561
|
+
expect(step?.run).not.toContain("pipefail");
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* chant #2321 — #2307 fixed #2299 by pairing the `pipefail` line with an
|
|
566
|
+
* unconditional `shell: "bash"` on this step, which then broke every
|
|
567
|
+
* consumer whose `options.image` has no bash on it at all (alpine,
|
|
568
|
+
* distroless, …): `shell: bash` on such an image fails with `bash: not
|
|
569
|
+
* found` before `chant` is ever reached, a failure busybox `ash` never had
|
|
570
|
+
* before #2307 introduced the unconditional `shell: bash`. The fix removes
|
|
571
|
+
* the bash-ism instead of moving it, so a non-bash image's own default
|
|
572
|
+
* `sh` runs the step unchanged.
|
|
573
|
+
*/
|
|
574
|
+
test("a consumer-set non-bash image's gated job can still start (#2321)", () => {
|
|
575
|
+
const doc = parseFile(generateGithubOpPipeline([pushSpec], { image: "alpine:3.20" }).files[0].yaml);
|
|
576
|
+
const job = doc.jobs!["app-apply"];
|
|
577
|
+
expect(job.container).toBe("alpine:3.20");
|
|
578
|
+
const step = job.steps.find((s) => s.id === "chant-run");
|
|
579
|
+
expect(step?.shell).toBeUndefined();
|
|
580
|
+
expect(step?.run).not.toContain("pipefail");
|
|
517
581
|
});
|
|
518
582
|
});
|
|
519
583
|
|
|
@@ -642,3 +706,67 @@ describe("generateGithubOpPipeline: a deployment environment on the Op's job (#2
|
|
|
642
706
|
).toThrow(/has an empty `url`/);
|
|
643
707
|
});
|
|
644
708
|
});
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* chant #2290 — per-Op credentials, so a pull-request job need not hold the
|
|
712
|
+
* apply credential. `variables` lands as the Op's own job-level `env:`, one
|
|
713
|
+
* level more specific than `options.variables`'s workflow-level `env:`, and
|
|
714
|
+
* never on the gate-notice job beside it.
|
|
715
|
+
*/
|
|
716
|
+
describe("generateGithubOpPipeline: per-Op variables on the job (#2290)", () => {
|
|
717
|
+
const CREDENTIAL = { AWS_ACCESS_KEY_ID: "${{ secrets.AWS_ACCESS_KEY_ID }}" };
|
|
718
|
+
|
|
719
|
+
test("a spec with no variables emits no job-level env:, even when options.variables is set", () => {
|
|
720
|
+
const doc = parseFile(
|
|
721
|
+
generateGithubOpPipeline([{ name: "live-check", trigger: { kind: "pull_request", branches: ["main"] } }], {
|
|
722
|
+
variables: { CHANT_FORGE: "github" },
|
|
723
|
+
}).files[0].yaml,
|
|
724
|
+
);
|
|
725
|
+
expect(doc.jobs!["live-check"].env).toBeUndefined();
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
test("a spec's own variables land as the job's env:, beside the workflow env:", () => {
|
|
729
|
+
const doc = parseFile(
|
|
730
|
+
generateGithubOpPipeline(
|
|
731
|
+
[{ name: "live-apply", trigger: { kind: "push", branches: ["main"] }, variables: CREDENTIAL }],
|
|
732
|
+
{ variables: { CHANT_FORGE: "github" } },
|
|
733
|
+
).files[0].yaml,
|
|
734
|
+
);
|
|
735
|
+
expect(doc.jobs!["live-apply"].env).toEqual(CREDENTIAL);
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
test("a per-Op key wins over a same-named forge-wide one", () => {
|
|
739
|
+
const doc = parseFile(
|
|
740
|
+
generateGithubOpPipeline(
|
|
741
|
+
[
|
|
742
|
+
{
|
|
743
|
+
name: "live-apply",
|
|
744
|
+
trigger: { kind: "push", branches: ["main"] },
|
|
745
|
+
variables: { CHANT_FORGE: "overridden" },
|
|
746
|
+
},
|
|
747
|
+
],
|
|
748
|
+
{ variables: { CHANT_FORGE: "github" } },
|
|
749
|
+
).files[0].yaml,
|
|
750
|
+
);
|
|
751
|
+
expect(doc.jobs!["live-apply"].env).toEqual({ CHANT_FORGE: "overridden" });
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
test("the gate-notice job never carries the Op's own job-level variables", () => {
|
|
755
|
+
const doc = parseFile(
|
|
756
|
+
generateGithubOpPipeline([
|
|
757
|
+
{ name: "live-apply", trigger: { kind: "push", branches: ["main"] }, variables: CREDENTIAL },
|
|
758
|
+
]).files[0].yaml,
|
|
759
|
+
);
|
|
760
|
+
expect(doc.jobs!["live-apply-gate-notice"].env).toBeUndefined();
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
test("a spec declaring variables changes exactly the job's env: block and nothing else", () => {
|
|
764
|
+
const spec: ScheduledOpSpec = { name: "actions-audit", schedule: "0 6 * * *" };
|
|
765
|
+
const before = generateGithubOpPipeline([spec]).files[0].yaml;
|
|
766
|
+
const after = generateGithubOpPipeline([{ ...spec, variables: CREDENTIAL }]).files[0].yaml;
|
|
767
|
+
expect(after).toContain(" env:\n AWS_ACCESS_KEY_ID: '${{ secrets.AWS_ACCESS_KEY_ID }}'\n");
|
|
768
|
+
expect(
|
|
769
|
+
after.replace(" env:\n AWS_ACCESS_KEY_ID: '${{ secrets.AWS_ACCESS_KEY_ID }}'\n", ""),
|
|
770
|
+
).toBe(before);
|
|
771
|
+
});
|
|
772
|
+
});
|
|
@@ -57,6 +57,17 @@
|
|
|
57
57
|
* environment protection is repository configuration, not a token scope — and
|
|
58
58
|
* {@link assertEnvironment} refuses only what would emit as configured and
|
|
59
59
|
* bind nothing.
|
|
60
|
+
*
|
|
61
|
+
* A fourth is per-Op credentials (#2290): a spec's `variables` emits the Op's
|
|
62
|
+
* own job-level `env:`, beside (and layered over, on a key collision)
|
|
63
|
+
* `options.variables`'s workflow-level `env:`. `options.variables` keeps
|
|
64
|
+
* meaning what it always has — set once, landing on every generated file — so
|
|
65
|
+
* a caller who declares nothing per-Op sees byte-identical output; a spec's
|
|
66
|
+
* own `variables` is the way one Op's job can hold a credential no other Op's
|
|
67
|
+
* job receives, which a workflow-level declaration can never express because
|
|
68
|
+
* every generated file here has exactly the one Op job (plus, on a `push`
|
|
69
|
+
* trigger, the gate-notice job beside it — job-level `env:` does not reach
|
|
70
|
+
* that job either, same as `environment:` above does not).
|
|
60
71
|
*/
|
|
61
72
|
|
|
62
73
|
import { emitYAML } from "@intentius/chant/yaml";
|
|
@@ -234,18 +245,64 @@ const GATE_OUTPUT_SCRIPT =
|
|
|
234
245
|
* `--json`, tee'd so the record is both in the log and on disk, then read for
|
|
235
246
|
* the job's outputs.
|
|
236
247
|
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
248
|
+
* The exit-code capture below is not decoration. Piping straight into `tee`
|
|
249
|
+
* — `chant run ... | tee "$json"` — reports `tee`'s own zero as the step's
|
|
250
|
+
* result no matter what `chant run` exited with, and turns a broken apply
|
|
251
|
+
* green, which is the exact thing this step must not do. bash's fix for that
|
|
252
|
+
* is `set -o pipefail`, but `pipefail` is a bash-ism, and this generator's
|
|
253
|
+
* own Op job always sets `container:` (see {@link buildGithubOpPipelineDocs}'s
|
|
254
|
+
* `container: image`) — GitHub's (and Forgejo's) default shell for a
|
|
255
|
+
* `container:` job is plain `sh`, which rejects `set -o pipefail` outright
|
|
256
|
+
* (`Illegal option -o pipefail`) and fails the step before `chant` ever runs
|
|
257
|
+
* (#2299). #2307 first fixed that by pairing the `pipefail` line with an
|
|
258
|
+
* unconditional `shell: "bash"` on the step — which then broke every
|
|
259
|
+
* consumer whose `options.image` has no bash on it at all, e.g. an
|
|
260
|
+
* `alpine:*` or distroless image (#2321): busybox `ash` ran the old
|
|
261
|
+
* plain-pipe script fine, so pinning `shell: bash` regressed exactly the
|
|
262
|
+
* images that never needed pipefail's workaround in the first place.
|
|
263
|
+
*
|
|
264
|
+
* So this step needs no shell declaration and runs under whatever `sh` the
|
|
265
|
+
* consumer's image provides. It gets the same failure-cannot-hide guarantee
|
|
266
|
+
* `pipefail` gave, without the bash-ism, by capturing the invocation's own
|
|
267
|
+
* exit code into a file from *inside* the pipeline's first stage — where
|
|
268
|
+
* `tee` can never see it — and checking that file once the pipe finishes:
|
|
269
|
+
* `{ invocation; echo "$?" >status; } | tee "$json"` followed by `exit
|
|
270
|
+
* "$(cat "$status")"` when that is non-zero. `set +e` up front makes this
|
|
271
|
+
* work under any default shell a container job might get, `-e` included:
|
|
272
|
+
* GitHub's documented `sh` template is `sh -e {0}`, and without `set +e`
|
|
273
|
+
* `-e` would abort the brace group at the first failing command — before
|
|
274
|
+
* `echo "$?"` ever runs — which was verified by running both this script and
|
|
275
|
+
* that failure mode under bash and dash directly, `-e` on. Everything past
|
|
276
|
+
* that point (`set +e`, `{ }`, `$(...)`, `[ ]`) is POSIX `sh`, so it needs no
|
|
277
|
+
* `shell:` field to run correctly under `ash`, `dash`, or bash alike, and
|
|
278
|
+
* #2299 does not reopen: nothing here is a bash-only construct.
|
|
279
|
+
*
|
|
280
|
+
* The `node -e` line and the job's own `outputs:` mapping exist for exactly
|
|
281
|
+
* one reader: the gate-notice job beside it, which is `needs:`-only readable
|
|
282
|
+
* through `steps.<id>.outputs`/`needs.<job>.outputs`. `emitOutputs: false`
|
|
283
|
+
* (forgejo, #2294 — the notice job does not cross the dialect at all) drops
|
|
284
|
+
* that line: nothing on Forgejo reads `$GITHUB_OUTPUT` for this job, and
|
|
285
|
+
* keeping the line would be a wired-up mechanism with no reader, which is the
|
|
286
|
+
* exact thing #2294 is about. The tee'd invocation survives either way — it's
|
|
287
|
+
* what puts the run's own JSON record in the log, gate-notice job or not.
|
|
241
288
|
*/
|
|
242
|
-
function
|
|
243
|
-
|
|
244
|
-
|
|
289
|
+
function gatedRunStep(
|
|
290
|
+
id: string,
|
|
291
|
+
op: string,
|
|
292
|
+
invocation: string,
|
|
293
|
+
emitOutputs: boolean,
|
|
294
|
+
env: Record<string, string>,
|
|
295
|
+
): Record<string, unknown> {
|
|
296
|
+
const lines = [
|
|
297
|
+
"set +e",
|
|
245
298
|
'json="${RUNNER_TEMP:-/tmp}/chant-run-' + op + '.json"',
|
|
246
|
-
|
|
247
|
-
`
|
|
248
|
-
|
|
299
|
+
'status="${RUNNER_TEMP:-/tmp}/chant-run-' + op + '.status"',
|
|
300
|
+
`{ ${invocation}; echo "$?" >"$status"; } | tee "$json"`,
|
|
301
|
+
'code=$(cat "$status")',
|
|
302
|
+
'[ "$code" -eq 0 ] || exit "$code"',
|
|
303
|
+
];
|
|
304
|
+
if (emitOutputs) lines.push(`node -e '${GATE_OUTPUT_SCRIPT}' "$json"`);
|
|
305
|
+
return { id, run: lines.join("\n"), env };
|
|
249
306
|
}
|
|
250
307
|
|
|
251
308
|
/**
|
|
@@ -274,24 +331,39 @@ const NOTICE_BODY_FORMAT =
|
|
|
274
331
|
* direct push to the branch, a merge whose commit the API does not associate —
|
|
275
332
|
* the notice becomes an issue instead, which is the `issue` finding mode's own
|
|
276
333
|
* recipe and the reason this job carries `issues: write`.
|
|
334
|
+
*
|
|
335
|
+
* `$api_base` (chant #2305), resolved the same way `stickyCommentScript`
|
|
336
|
+
* (`lexicons/github/src/composites/pr-plan-report.ts`) and `reconcilePr`'s
|
|
337
|
+
* `githubApiBaseFrom` (`packages/core/src/op/activities/reconcile.ts`)
|
|
338
|
+
* resolve it: `$GITHUB_API_URL` with a trailing slash trimmed, falling back
|
|
339
|
+
* to `https://api.github.com`. This job never crosses to Forgejo today — the
|
|
340
|
+
* forgejo generator's `gatedNoticeDoc` is never carried across the dialect
|
|
341
|
+
* (#2294) — so github.com and GitHub Enterprise Server are the only hosts
|
|
342
|
+
* that run this script, and both already resolved correctly under `gh`'s own
|
|
343
|
+
* bare-path guess. Built from `$api_base` instead, the URL is byte-identical
|
|
344
|
+
* on those two hosts, and the same anti-pattern that broke Forgejo elsewhere
|
|
345
|
+
* (#2291, #2305) does not get a second copy here in case a future notice job
|
|
346
|
+
* does reach Forgejo.
|
|
277
347
|
*/
|
|
278
348
|
function gateNoticeScript(): string {
|
|
279
349
|
return [
|
|
350
|
+
'api_base="${GITHUB_API_URL%/}"',
|
|
351
|
+
'api_base="${api_base:-https://api.github.com}"',
|
|
280
352
|
'marker="<!-- chant-gate:$CHANT_OP -->"',
|
|
281
353
|
"body=$(printf '" + NOTICE_BODY_FORMAT + "' " +
|
|
282
354
|
'"$marker" "$CHANT_OP" "$GITHUB_SHA" "$CHANT_GATE" "$CHANT_APPROVE" "$CHANT_OP")',
|
|
283
|
-
'pr=$(gh api "repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA/pulls" --jq ".[0].number // empty")',
|
|
355
|
+
'pr=$(gh api "$api_base/repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA/pulls" --jq ".[0].number // empty")',
|
|
284
356
|
'if [ -z "$pr" ]; then',
|
|
285
357
|
' gh issue create --title "$CHANT_OP is waiting on gate $CHANT_GATE" --body "$body"',
|
|
286
358
|
" exit 0",
|
|
287
359
|
"fi",
|
|
288
|
-
'id=$(gh api "repos/$GITHUB_REPOSITORY/issues/$pr/comments" --paginate ' +
|
|
360
|
+
'id=$(gh api "$api_base/repos/$GITHUB_REPOSITORY/issues/$pr/comments" --paginate ' +
|
|
289
361
|
'--jq "map(select(.body | startswith(\\"$marker\\"))) | .[0].id // empty" ' +
|
|
290
362
|
'| grep -m1 -E "^[0-9]+$" || true)',
|
|
291
363
|
'if [ -n "$id" ]; then',
|
|
292
|
-
' gh api --method PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$id" -f "body=$body" --jq .html_url',
|
|
364
|
+
' gh api --method PATCH "$api_base/repos/$GITHUB_REPOSITORY/issues/comments/$id" -f "body=$body" --jq .html_url',
|
|
293
365
|
"else",
|
|
294
|
-
' gh api --method POST "repos/$GITHUB_REPOSITORY/issues/$pr/comments" -f "body=$body" --jq .html_url',
|
|
366
|
+
' gh api --method POST "$api_base/repos/$GITHUB_REPOSITORY/issues/$pr/comments" -f "body=$body" --jq .html_url',
|
|
295
367
|
"fi",
|
|
296
368
|
].join("\n");
|
|
297
369
|
}
|
|
@@ -559,6 +631,24 @@ function setupStepDoc(step: OpSetupStep): Record<string, unknown> {
|
|
|
559
631
|
};
|
|
560
632
|
}
|
|
561
633
|
|
|
634
|
+
/**
|
|
635
|
+
* Internal knobs beside the public {@link GenerateGithubOpOptions} — not part
|
|
636
|
+
* of that type because they are not something a project author sets, only
|
|
637
|
+
* something a dialect on top of this builder (forgejo, #2294) needs to
|
|
638
|
+
* change about the shape this function emits for every spec alike.
|
|
639
|
+
*/
|
|
640
|
+
export interface BuildGithubOpPipelineDocsInternalOptions {
|
|
641
|
+
/**
|
|
642
|
+
* Whether a `push` job's outputs (`gated`/`op`/`gate`/`approve`) and the
|
|
643
|
+
* `node -e` step that writes them to `$GITHUB_OUTPUT` are emitted (#2294).
|
|
644
|
+
* Default true (github's own behavior, unchanged): the gate-notice job
|
|
645
|
+
* beside it reads them. The forgejo generator passes `false` — it never
|
|
646
|
+
* carries a gate-notice job (`gatedNoticeDoc` never crosses the dialect),
|
|
647
|
+
* so nothing would ever read them there.
|
|
648
|
+
*/
|
|
649
|
+
emitGatedOutputs?: boolean;
|
|
650
|
+
}
|
|
651
|
+
|
|
562
652
|
/**
|
|
563
653
|
* Build one `GithubOpPipelineDoc` per scheduled Op: its trigger, its `setup`
|
|
564
654
|
* steps, least-privilege `permissions:` for its finding-mode plus whatever
|
|
@@ -574,7 +664,9 @@ function setupStepDoc(step: OpSetupStep): Record<string, unknown> {
|
|
|
574
664
|
export function buildGithubOpPipelineDocs(
|
|
575
665
|
ops: ScheduledOpSpec[],
|
|
576
666
|
options: GenerateGithubOpOptions = {},
|
|
667
|
+
internalOptions: BuildGithubOpPipelineDocsInternalOptions = {},
|
|
577
668
|
): { files: GithubOpPipelineFile[]; jobs: OpPipelineJob[] } {
|
|
669
|
+
const emitGatedOutputs = internalOptions.emitGatedOutputs ?? true;
|
|
578
670
|
const image = options.image ?? DEFAULT_IMAGE;
|
|
579
671
|
const runCommand = options.runCommand ?? ["chant", "run", "{name}"];
|
|
580
672
|
const beforeScript = options.beforeScript ?? [];
|
|
@@ -615,7 +707,7 @@ export function buildGithubOpPipelineDocs(
|
|
|
615
707
|
for (const line of beforeScript) steps.push({ run: line });
|
|
616
708
|
steps.push(
|
|
617
709
|
gated
|
|
618
|
-
?
|
|
710
|
+
? gatedRunStep(RUN_STEP_ID, spec.name, invocation, emitGatedOutputs, stepEnv)
|
|
619
711
|
: { run: invocation, env: stepEnv },
|
|
620
712
|
);
|
|
621
713
|
for (const line of extraScript) steps.push({ run: line });
|
|
@@ -636,12 +728,17 @@ export function buildGithubOpPipelineDocs(
|
|
|
636
728
|
[jobName]: {
|
|
637
729
|
"runs-on": "ubuntu-latest",
|
|
638
730
|
container: image,
|
|
731
|
+
// Per-Op credentials (#2290): job-level `env:`, one level more
|
|
732
|
+
// specific than `options.variables`'s workflow-level `env:` above,
|
|
733
|
+
// and never on the notice job beside it — same reason `environment:`
|
|
734
|
+
// isn't either, it's this Op's own job alone.
|
|
735
|
+
...(spec.variables && Object.keys(spec.variables).length > 0 ? { env: spec.variables } : {}),
|
|
639
736
|
// On this Op's own job and never on the notice job beside it: the
|
|
640
737
|
// notice exists to say a chant gate is pending, and putting it
|
|
641
738
|
// behind the same reviewer would hold the message back until
|
|
642
739
|
// somebody had already acted.
|
|
643
740
|
...(spec.environment ? { environment: environmentDoc(spec.environment) } : {}),
|
|
644
|
-
...(gated
|
|
741
|
+
...(gated && emitGatedOutputs
|
|
645
742
|
? {
|
|
646
743
|
outputs: Object.fromEntries(
|
|
647
744
|
["gated", "op", "gate", "approve"].map((name) => [
|
|
@@ -89,8 +89,8 @@ describe("PrPlanReport composite (#1983)", () => {
|
|
|
89
89
|
const { job } = PrPlanReport({ environment: "prod" });
|
|
90
90
|
const postStep = steps(job).find((s) => s.props.name === "Post or update PR comment")!;
|
|
91
91
|
const run = postStep.props.run!;
|
|
92
|
-
expect(run).toContain('gh api -X PATCH "repos/$REPO/issues/comments/$comment_id" -F body=@plan.md');
|
|
93
|
-
expect(run).toContain('gh api -X POST "repos/$REPO/issues/$PR_NUMBER/comments" -F body=@plan.md');
|
|
92
|
+
expect(run).toContain('gh api -X PATCH "$api_base/repos/$REPO/issues/comments/$comment_id" -F body=@plan.md');
|
|
93
|
+
expect(run).toContain('gh api -X POST "$api_base/repos/$REPO/issues/$PR_NUMBER/comments" -F body=@plan.md');
|
|
94
94
|
expect(run).not.toContain("-f body=@");
|
|
95
95
|
// The plan step writes the marker as plan.md's first line, so the body the
|
|
96
96
|
// -F read now starts with `$MARKER` and the jq `startswith` search finds
|
|
@@ -101,6 +101,30 @@ describe("PrPlanReport composite (#1983)", () => {
|
|
|
101
101
|
expect(planStep.props.run).toContain("> plan.md");
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
+
// chant #2305 — this script's three `gh api` calls used to take a bare
|
|
105
|
+
// relative path (`repos/$REPO/...`), which `gh` resolves against `/api/v3`
|
|
106
|
+
// for any host but github.com. GitHub Enterprise Server serves that path,
|
|
107
|
+
// so the bug was invisible there; Forgejo does not, and answers 404 for
|
|
108
|
+
// GET and POST alike (settled against a real instance in #2291/#2304, the
|
|
109
|
+
// same fix this composite gets here). Every call now targets `$api_base`,
|
|
110
|
+
// built from `$GITHUB_API_URL` the same way `githubApiBaseFrom`
|
|
111
|
+
// (`packages/core/src/op/activities/reconcile.ts`) reads it for
|
|
112
|
+
// `reconcilePr`, with the github.com fallback inlined since this is a shell
|
|
113
|
+
// string emitted into YAML rather than a TypeScript call.
|
|
114
|
+
test("the sticky-comment script resolves its API base from $GITHUB_API_URL, not a bare path (#2305)", () => {
|
|
115
|
+
const { job } = PrPlanReport({ environment: "prod" });
|
|
116
|
+
const postStep = steps(job).find((s) => s.props.name === "Post or update PR comment")!;
|
|
117
|
+
const run = postStep.props.run!;
|
|
118
|
+
expect(run).toContain('api_base="${GITHUB_API_URL%/}"');
|
|
119
|
+
expect(run).toContain('api_base="${api_base:-https://api.github.com}"');
|
|
120
|
+
expect(run).toContain('gh api "$api_base/repos/$REPO/issues/$PR_NUMBER/comments" --paginate');
|
|
121
|
+
expect(run).toContain('gh api -X PATCH "$api_base/repos/$REPO/issues/comments/$comment_id"');
|
|
122
|
+
expect(run).toContain('gh api -X POST "$api_base/repos/$REPO/issues/$PR_NUMBER/comments"');
|
|
123
|
+
// No bare-path call survives: every `gh api` invocation is immediately
|
|
124
|
+
// followed by `"$api_base/repos/`, never by `"repos/` on its own.
|
|
125
|
+
expect(run).not.toMatch(/gh api[^\n]*"repos\//);
|
|
126
|
+
});
|
|
127
|
+
|
|
104
128
|
test("the emitted workflow passes the github lexicon's own lint — no errors, pinned actions included", () => {
|
|
105
129
|
const { job } = PrPlanReport({ environment: "prod", before: ["aws sts get-caller-identity"] });
|
|
106
130
|
const workflow = new Workflow({
|
|
@@ -46,25 +46,43 @@ export interface PrPlanReportProps {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
|
-
* Sticky-comment script (#1223's mechanism,
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
49
|
+
* Sticky-comment script (#1223's mechanism), find the comment whose body
|
|
50
|
+
* starts with `$MARKER`, PATCH it if found, POST otherwise. No marketplace
|
|
51
|
+
* action, nothing extra to pin — `gh` ships on GitHub's hosted runners. The
|
|
52
|
+
* flag is `-F`, not `-f`: `gh api`'s `-F/--field` is the typed form that
|
|
53
|
+
* reads the value from a file when it starts with `@`, while `-f/--raw-field`
|
|
54
|
+
* adds the parameter as a literal string, so the `-f` form posted the eight
|
|
55
|
+
* characters `@plan.md` (#2236). Reading the body from the file the plan step
|
|
56
|
+
* wrote means a large or multi-line plan never has to survive shell quoting.
|
|
57
|
+
* `-F`'s type coercion of `true`/`false`/`null`/integers does not reach the
|
|
58
|
+
* body: gh resolves the leading `@` first and hands back the file's bytes as
|
|
59
|
+
* a string.
|
|
60
|
+
*
|
|
61
|
+
* `$api_base` (chant #2305) is `$GITHUB_API_URL` with any trailing slash
|
|
62
|
+
* trimmed, falling back to `https://api.github.com` when it is unset —
|
|
63
|
+
* `$GITHUB_API_URL` is already correct on every GitHub Actions and Forgejo
|
|
64
|
+
* Actions job (`https://api.github.com` on github.com, `<host>/api/v3` on
|
|
65
|
+
* GitHub Enterprise Server, `<host>/api/v1` on Forgejo), the same fact
|
|
66
|
+
* `githubApiBaseFrom` in `packages/core/src/op/activities/reconcile.ts`
|
|
67
|
+
* builds on for `reconcilePr`'s `postOrUpdateComment` (#2291). This script
|
|
68
|
+
* cannot import that helper — it is a shell string emitted into YAML, not a
|
|
69
|
+
* TypeScript call — so the equivalent base-URL construction is inlined here
|
|
70
|
+
* instead. Without it, every `gh api` call below took a bare relative path,
|
|
71
|
+
* which `gh` resolves against `/api/v3` for any host but github.com; that
|
|
72
|
+
* guess is right for GitHub Enterprise Server but wrong for Forgejo, which
|
|
73
|
+
* does not serve `/api/v3` and answered 404 for GET and POST alike. On
|
|
74
|
+
* github.com and GHES the resolved URL is byte-identical to `gh`'s own
|
|
75
|
+
* guess, so this changes nothing there.
|
|
60
76
|
*/
|
|
61
77
|
const stickyCommentScript = [
|
|
62
|
-
'
|
|
78
|
+
'api_base="${GITHUB_API_URL%/}"',
|
|
79
|
+
'api_base="${api_base:-https://api.github.com}"',
|
|
80
|
+
'comment_id=$(gh api "$api_base/repos/$REPO/issues/$PR_NUMBER/comments" --paginate ' +
|
|
63
81
|
'--jq "map(select(.body | startswith(\\"$MARKER\\"))) | .[0].id // empty")',
|
|
64
82
|
'if [ -n "$comment_id" ]; then',
|
|
65
|
-
' gh api -X PATCH "repos/$REPO/issues/comments/$comment_id" -F body=@plan.md > /dev/null',
|
|
83
|
+
' gh api -X PATCH "$api_base/repos/$REPO/issues/comments/$comment_id" -F body=@plan.md > /dev/null',
|
|
66
84
|
"else",
|
|
67
|
-
' gh api -X POST "repos/$REPO/issues/$PR_NUMBER/comments" -F body=@plan.md > /dev/null',
|
|
85
|
+
' gh api -X POST "$api_base/repos/$REPO/issues/$PR_NUMBER/comments" -F body=@plan.md > /dev/null',
|
|
68
86
|
"fi",
|
|
69
87
|
].join("\n");
|
|
70
88
|
|
|
@@ -79,7 +97,9 @@ const stickyCommentScript = [
|
|
|
79
97
|
* proved (#1223), reused here rather than reinvented. The forgejo lexicon
|
|
80
98
|
* inherits this composite through its github re-export, since Forgejo Actions
|
|
81
99
|
* runs the same workflow shape and the Forgejo API accepts the same `gh api`
|
|
82
|
-
* calls against its GitHub-compatible surface
|
|
100
|
+
* calls against its GitHub-compatible surface — once they target a full URL
|
|
101
|
+
* rather than a bare relative path, which `stickyCommentScript` now does
|
|
102
|
+
* (#2305, the same fix #2291 made for `reconcilePr`'s `postOrUpdateComment`).
|
|
83
103
|
*
|
|
84
104
|
* Only meaningful on a `pull_request`-triggered workflow — the job guards on
|
|
85
105
|
* `github.event_name` since the comment targets `github.event.number`, which
|