@intentius/chant-lexicon-forgejo 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 +64 -6
- package/dist/components/generate-op-pipeline.d.ts.map +1 -1
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/package.json +3 -3
- package/src/components/generate-op-pipeline.test.ts +227 -16
- package/src/components/generate-op-pipeline.ts +83 -12
- package/src/serializer.test.ts +25 -4
|
@@ -28,12 +28,52 @@
|
|
|
28
28
|
* no mapping in ../actions.ts passes through verbatim and resolves only if
|
|
29
29
|
* the runner can fetch it.
|
|
30
30
|
*
|
|
31
|
-
*
|
|
32
|
-
* triggering pull request by shelling to `gh` against
|
|
33
|
-
*
|
|
34
|
-
* GitHub
|
|
35
|
-
*
|
|
36
|
-
*
|
|
31
|
+
* `comment` (#2231) crosses over, since chant #2291: it posts onto the
|
|
32
|
+
* triggering pull request by shelling to `gh` against `${GITHUB_API_URL}`,
|
|
33
|
+
* and a Forgejo Actions job already sets that (and `github.token`) the same
|
|
34
|
+
* way a GitHub Actions job does. This generator used to refuse the mode by
|
|
35
|
+
* name here, on the premise that chant had no way to point `gh` at a Forgejo
|
|
36
|
+
* instance; that premise was checked against a real Forgejo
|
|
37
|
+
* 12.0.4+gitea-1.22.0 instance during INTENTIUS/choudoufu#1027 and found
|
|
38
|
+
* false — the failure was `gh api` resolving a *relative* path against
|
|
39
|
+
* `/api/v3`, which Forgejo does not serve, not an unreachable forge. See
|
|
40
|
+
* `reconcilePr`'s `postOrUpdateComment` (`packages/core/src/op/activities/reconcile.ts`)
|
|
41
|
+
* for the fix.
|
|
42
|
+
*
|
|
43
|
+
* `issue` (`gh issue create`/`postOrUpdateGithubIssue`) does not cross over,
|
|
44
|
+
* and chant #2315 is why: it remained un-refused-but-unverified after #2304
|
|
45
|
+
* lifted `comment`'s refusal, and settling that gap the same way — against a
|
|
46
|
+
* real instance rather than a mock — found a real failure, so the refusal
|
|
47
|
+
* below is reinstated rather than lifted. Tested against the same
|
|
48
|
+
* 12.0.4+gitea-1.22.0 image (`codeberg.org/forgejo/forgejo:12`) with a repo,
|
|
49
|
+
* an issue, and a pull request created on it: the *read* half of
|
|
50
|
+
* `postOrUpdateGithubIssue` checks out fine on Forgejo — plain paginated
|
|
51
|
+
* `GET .../issues?state=open` (no `/search/issues`, confirmed absent from a
|
|
52
|
+
* live Forgejo's own OpenAPI spec) returns pull requests interleaved with
|
|
53
|
+
* issues exactly as GitHub's endpoint does, and the `.pull_request == null`
|
|
54
|
+
* filter and the marker `startswith` match both behave identically to
|
|
55
|
+
* GitHub. The *write* half does not: `postOrUpdateGithubIssue`'s POST and
|
|
56
|
+
* PATCH calls carry only `GH_TOKEN` (in fact, on the `issue` path, not even
|
|
57
|
+
* that — `reconcilePr` hands it `execAsync` with no `env` override at all,
|
|
58
|
+
* chant #2320), and `gh`'s own documented environment variables (`gh help
|
|
59
|
+
* environment`) scope `GH_TOKEN`/`GITHUB_TOKEN` to "github.com or a subdomain
|
|
60
|
+
* of ghe.com" — never a self-hosted Forgejo. A `GH_DEBUG=api` POST against
|
|
61
|
+
* the live instance, with `GH_TOKEN` and `GITHUB_API_URL` set exactly as the
|
|
62
|
+
* generated workflow sets them, sent no `Authorization` header at all and
|
|
63
|
+
* Forgejo answered `{"message":"token is required"}` (HTTP 401); adding
|
|
64
|
+
* `GH_HOST` alongside `GH_TOKEN` made no difference. Only `GH_ENTERPRISE_TOKEN`
|
|
65
|
+
* paired with a matching `GH_HOST` authenticated the same call. Since neither
|
|
66
|
+
* `postOrUpdateGithubIssue` nor its caller sets either, every write this mode
|
|
67
|
+
* makes on Forgejo fails — not a slow search, a 401 on every run. The same
|
|
68
|
+
* shape (`GH_TOKEN` only, no `GH_HOST`/`GH_ENTERPRISE_TOKEN`) is what
|
|
69
|
+
* `postOrUpdateComment`'s writes carry too, which means #2304's "verified
|
|
70
|
+
* against a real Forgejo instance" could not have gone through the generated
|
|
71
|
+
* workflow's own credential path — either that session had a `gh auth login`
|
|
72
|
+
* already stored for the test instance, which a real Actions job's fresh
|
|
73
|
+
* checkout never has, or a different `gh` build was in play. That is
|
|
74
|
+
* `comment` mode's problem to re-verify, not `issue` mode's, and out of
|
|
75
|
+
* scope for chant #2315; flagged here because finding it is what closes that
|
|
76
|
+
* issue's question of whether to lift or reinstate this refusal.
|
|
37
77
|
*
|
|
38
78
|
* A spec's `environment` (#2257) is dropped on the same terms as
|
|
39
79
|
* `permissions:`, and for a stronger reason: Forgejo Actions has no
|
|
@@ -56,6 +96,24 @@
|
|
|
56
96
|
* Forgejo apply that stops at its gate is a green run rather than a red one,
|
|
57
97
|
* and `chant run` still writes the gate and the approve command to
|
|
58
98
|
* `GITHUB_STEP_SUMMARY`, which Forgejo Actions sets like GitHub does.
|
|
99
|
+
*
|
|
100
|
+
* The gate-notice job's own reason for being — `outputs: { gated, op, gate,
|
|
101
|
+
* approve }` on the Op's job, and the `node -e` step that writes them to
|
|
102
|
+
* `$GITHUB_OUTPUT` — does not survive it either (#2294). Both existed only for
|
|
103
|
+
* that job to read via `needs.<job>.outputs`; with the job gone, so is every
|
|
104
|
+
* reader, and `buildGithubOpPipelineDocs` is asked for neither
|
|
105
|
+
* (`emitGatedOutputs: false` below) rather than emitting them here and
|
|
106
|
+
* stripping them back out. Should the Forgejo posting work in #2291 grow a
|
|
107
|
+
* notice job of its own, this is the one flag that brings both back.
|
|
108
|
+
*
|
|
109
|
+
* A spec's own `variables` (#2290) crosses over unchanged: forgejo reuses the
|
|
110
|
+
* same job-level `env:` github's builder emits, and the dialect transform
|
|
111
|
+
* touches only `permissions:`/`environment:`/action refs, never a job's `env:`
|
|
112
|
+
* mapping. That is what makes a per-Op credential expressible here at all —
|
|
113
|
+
* `ComponentPipelineOptions.variables` is workflow-scoped and, on Forgejo,
|
|
114
|
+
* Actions mints no OIDC token to put in `setup` instead, so a job-level
|
|
115
|
+
* `variables` entry is the only way one Op's job can hold a credential no
|
|
116
|
+
* sibling Op's job receives.
|
|
59
117
|
*/
|
|
60
118
|
import type { ComponentPipelineOptions, OpPipelineResult, ScheduledOpSpec } from "@intentius/chant/lexicon";
|
|
61
119
|
import { type ForgejoDialectOptions } from "../dialect.js";
|
|
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoHG;AAOH,OAAO,KAAK,EACV,wBAAwB,EACxB,gBAAgB,EAChB,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAA2B,KAAK,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAmCjF;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,eAAe,EAAE,EACtB,OAAO,GAAE,wBAA6B,EACtC,cAAc,GAAE,qBAA0B,GACzC,gBAAgB,CA4ClB"}
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "320a18e182b02a18de130ee2975fae9e5b3c163c87d571f26fa74ac18e4f0f2b",
|
|
5
5
|
"meta.json": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
|
|
6
6
|
"types/index.d.ts": "66204549b2a864ab5489e3f02c1878e73ab4b3c9d938cfb480b66f71543a43de",
|
|
7
7
|
"rules/delegate-to-github.ts": "1060cda40f4b73d6cca3ba3fa13b80b4886147d4034c88584c2d80c5510dbe5e",
|
|
@@ -9,5 +9,5 @@
|
|
|
9
9
|
"rules/wfj011.ts": "fcb8bf6685d744af20ad804ce929488257995c74b9a3353b2eacae3768d1c83e",
|
|
10
10
|
"skills/chant-forgejo.md": "a1a560429db736c187e0b34cf8dd9efc6ad771afefcbf6f802d160d3d5274257"
|
|
11
11
|
},
|
|
12
|
-
"composite": "
|
|
12
|
+
"composite": "52d7d99cea1db89b3e91637f18abf541193279984fdbf6cf6156e7c24378706c"
|
|
13
13
|
}
|
package/dist/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-forgejo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.63.0",
|
|
4
4
|
"description": "Forgejo / Codeberg / Gitea Actions lexicon for chant — a thin GitHub Actions dialect",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://intentius.io/chant",
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
56
|
"zod": "^4.3.6",
|
|
57
|
-
"@intentius/chant": "^0.
|
|
58
|
-
"@intentius/chant-lexicon-github": "^0.
|
|
57
|
+
"@intentius/chant": "^0.63.0",
|
|
58
|
+
"@intentius/chant-lexicon-github": "^0.63.0"
|
|
59
59
|
},
|
|
60
60
|
"scripts": {
|
|
61
61
|
"generate": "tsx src/codegen/generate-cli.ts",
|
|
@@ -14,9 +14,11 @@ import type { ScheduledOpSpec } from "@intentius/chant/lexicon";
|
|
|
14
14
|
|
|
15
15
|
interface ParsedJob {
|
|
16
16
|
"runs-on"?: string;
|
|
17
|
+
container?: string;
|
|
17
18
|
environment?: Record<string, string>;
|
|
18
19
|
outputs?: Record<string, string>;
|
|
19
|
-
|
|
20
|
+
env?: Record<string, string>;
|
|
21
|
+
steps: Array<{ id?: string; uses?: string; run?: string; shell?: string }>;
|
|
20
22
|
}
|
|
21
23
|
interface ParsedDoc {
|
|
22
24
|
on?: Record<string, unknown>;
|
|
@@ -31,7 +33,7 @@ describe("generateForgejoOpPipeline: structure (github-shaped)", () => {
|
|
|
31
33
|
test("one file per scheduled Op, each a valid workflow with one job", () => {
|
|
32
34
|
const specs: ScheduledOpSpec[] = [
|
|
33
35
|
{ name: "actions-audit", schedule: "0 6 * * *" },
|
|
34
|
-
{ name: "prod-reconcile", schedule: "0 * * * *", findingMode: "
|
|
36
|
+
{ name: "prod-reconcile", schedule: "0 * * * *", findingMode: "pull-request" },
|
|
35
37
|
];
|
|
36
38
|
const result = generateForgejoOpPipeline(specs);
|
|
37
39
|
|
|
@@ -76,7 +78,7 @@ describe("generateForgejoOpPipeline: dialect applied", () => {
|
|
|
76
78
|
describe("generateForgejoOpPipeline: non-cron trigger survives the dialect transform (#2084)", () => {
|
|
77
79
|
test("a pull_request trigger round-trips through the Forgejo dialect, with permissions: still dropped", () => {
|
|
78
80
|
const specs: ScheduledOpSpec[] = [
|
|
79
|
-
{ name: "tf-plan", trigger: { kind: "pull_request", branches: ["main"] }, findingMode: "
|
|
81
|
+
{ name: "tf-plan", trigger: { kind: "pull_request", branches: ["main"] }, findingMode: "pull-request" },
|
|
80
82
|
];
|
|
81
83
|
const fj = generateForgejoOpPipeline(specs);
|
|
82
84
|
const gh = generateGithubOpPipeline(specs);
|
|
@@ -105,27 +107,74 @@ describe("generateForgejoOpPipeline: non-cron trigger survives the dialect trans
|
|
|
105
107
|
});
|
|
106
108
|
});
|
|
107
109
|
|
|
108
|
-
describe("generateForgejoOpPipeline:
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
110
|
+
describe("generateForgejoOpPipeline: comment finding mode crosses over (#2291)", () => {
|
|
111
|
+
// Used to be refused by name here (#2231), on the premise that chant had no
|
|
112
|
+
// way to point `gh` at a Forgejo instance. Checked against a real Forgejo
|
|
113
|
+
// 12.0.4+gitea-1.22.0 instance during INTENTIUS/choudoufu#1027 and found
|
|
114
|
+
// false: Forgejo's own `/api/v1` takes the same GET/POST/PATCH calls
|
|
115
|
+
// `reconcilePr`'s `postOrUpdateComment` makes, once it targets the right
|
|
116
|
+
// URL. See `packages/core/src/op/activities/reconcile.test.ts` for the
|
|
117
|
+
// mocked-transport proof of that fix; this file only proves the generator
|
|
118
|
+
// no longer refuses the mode and produces the same job shape github does.
|
|
119
|
+
test("findingMode comment generates cleanly on the pull_request trigger it needs", () => {
|
|
120
|
+
const specs: ScheduledOpSpec[] = [
|
|
121
|
+
{ name: "app-plan", trigger: { kind: "pull_request", branches: ["main"] }, findingMode: "comment" },
|
|
122
|
+
];
|
|
123
|
+
expect(() => generateForgejoOpPipeline(specs)).not.toThrow();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("job/trigger/findingMode parity with the github generator (same input, same job list)", () => {
|
|
127
|
+
const specs: ScheduledOpSpec[] = [
|
|
128
|
+
{ name: "app-plan", trigger: { kind: "pull_request", branches: ["main"] }, findingMode: "comment" },
|
|
129
|
+
];
|
|
130
|
+
const fj = generateForgejoOpPipeline(specs);
|
|
131
|
+
const gh = generateGithubOpPipeline(specs);
|
|
132
|
+
expect(fj.jobs).toEqual(gh.jobs);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("the run step carries GH_TOKEN/GITHUB_TOKEN, same as github's — permissions: still dropped by the dialect", () => {
|
|
115
136
|
const specs: ScheduledOpSpec[] = [
|
|
116
137
|
{ name: "app-plan", trigger: { kind: "pull_request", branches: ["main"] }, findingMode: "comment" },
|
|
117
138
|
];
|
|
139
|
+
const fjYaml = generateForgejoOpPipeline(specs).files[0].yaml;
|
|
140
|
+
const ghDoc = parseFile(generateGithubOpPipeline(specs).files[0].yaml);
|
|
141
|
+
|
|
142
|
+
expect(fjYaml).toContain("GH_TOKEN: '${{ github.token }}'");
|
|
143
|
+
expect(fjYaml).toContain("GITHUB_TOKEN: '${{ github.token }}'");
|
|
144
|
+
expect(fjYaml).not.toMatch(/^permissions:/m);
|
|
145
|
+
// github still declares the scope the mode needs — proving the omission
|
|
146
|
+
// above is the Forgejo dialect dropping `permissions:` wholesale, not a
|
|
147
|
+
// sign the mode generated with no token.
|
|
148
|
+
expect(ghDoc.permissions).toEqual({ contents: "read", "pull-requests": "write" });
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("generateForgejoOpPipeline: issue finding mode is refused by name (#2315)", () => {
|
|
153
|
+
// `comment` (above) was checked against a real Forgejo instance and cleared
|
|
154
|
+
// (#2291, #2304). `issue` stayed un-refused-but-unverified after that —
|
|
155
|
+
// this is the same settling, and it came out the other way: a real
|
|
156
|
+
// instance showed the write half of `postOrUpdateGithubIssue` cannot
|
|
157
|
+
// authenticate (`gh`'s GH_TOKEN never reaches a self-hosted Forgejo), so
|
|
158
|
+
// the refusal this mode used to carry (pre-#2304, for a different reason)
|
|
159
|
+
// is reinstated rather than left lifted. See the module doc above for the
|
|
160
|
+
// reproduction.
|
|
161
|
+
test("findingMode issue is refused, naming the Op and the real-instance failure", () => {
|
|
162
|
+
const specs: ScheduledOpSpec[] = [{ name: "prod-watch", schedule: "0 6 * * *", findingMode: "issue" }];
|
|
118
163
|
expect(() => generateForgejoOpPipeline(specs)).toThrow(
|
|
119
|
-
/Scheduled Op "
|
|
164
|
+
/Scheduled Op "prod-watch".*findingMode "issue".*token is required/s,
|
|
120
165
|
);
|
|
121
166
|
});
|
|
122
167
|
|
|
123
|
-
test("github generates the same spec, so the refusal is forgejo's and not the shared builder's", () => {
|
|
168
|
+
test("github generates the same spec cleanly, so the refusal is forgejo's and not the shared builder's", () => {
|
|
169
|
+
const specs: ScheduledOpSpec[] = [{ name: "prod-watch", schedule: "0 6 * * *", findingMode: "issue" }];
|
|
170
|
+
expect(() => generateGithubOpPipeline(specs)).not.toThrow();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("a pull_request-triggered Op is refused the same way — issue mode needs no trigger to fail on", () => {
|
|
124
174
|
const specs: ScheduledOpSpec[] = [
|
|
125
|
-
{ name: "
|
|
175
|
+
{ name: "tf-plan", trigger: { kind: "pull_request", branches: ["main"] }, findingMode: "issue" },
|
|
126
176
|
];
|
|
127
|
-
|
|
128
|
-
expect(gh.permissions).toEqual({ contents: "read", "pull-requests": "write" });
|
|
177
|
+
expect(() => generateForgejoOpPipeline(specs)).toThrow(/findingMode "issue"/);
|
|
129
178
|
});
|
|
130
179
|
});
|
|
131
180
|
|
|
@@ -198,6 +247,42 @@ describe("generateForgejoOpPipeline: the gated apply on push (#2243)", () => {
|
|
|
198
247
|
expect(Object.keys(fj.jobs ?? {})).toEqual(["app-apply"]);
|
|
199
248
|
expect(gh.jobs).toHaveProperty("app-apply-gate-notice");
|
|
200
249
|
});
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* chant #2299 — Forgejo Actions runs the same act_runner semantics as
|
|
253
|
+
* GitHub Actions: a job with `container:` (which this generator's Op job
|
|
254
|
+
* always carries, unconditionally) defaults to `sh`, and `sh` rejects `set
|
|
255
|
+
* -o pipefail`. This generator reuses github's `buildGithubOpPipelineDocs`
|
|
256
|
+
* to build the job, then the dialect transform in ../dialect.ts to cross
|
|
257
|
+
* it, so the fix has to survive both: the exit-code capture is plain POSIX
|
|
258
|
+
* `sh` (chant #2321 — no `shell: bash` to drop or rename in the first
|
|
259
|
+
* place, unlike before #2321, when an unconditional `shell: bash` broke
|
|
260
|
+
* any consumer image without bash — see the sibling test below).
|
|
261
|
+
*/
|
|
262
|
+
test("the exit-code capture crosses the Forgejo dialect with no shell override — its job runs in a container, whose default shell is sh", () => {
|
|
263
|
+
const doc = parseFile(generateForgejoOpPipeline([pushSpec]).files[0].yaml);
|
|
264
|
+
const job = doc.jobs!["app-apply"];
|
|
265
|
+
expect(job.container).toBe("node:22-slim");
|
|
266
|
+
const step = job.steps.find((s) => s.id === "chant-run");
|
|
267
|
+
expect(step?.shell).toBeUndefined();
|
|
268
|
+
expect(step?.run).not.toContain("pipefail");
|
|
269
|
+
expect(step?.run).toContain('[ "$code" -eq 0 ] || exit "$code"');
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* chant #2321 — same regression as the github generator's, since this one
|
|
274
|
+
* reuses `buildGithubOpPipelineDocs` to build the job before crossing the
|
|
275
|
+
* dialect: a consumer's non-bash `options.image` (alpine, distroless, …)
|
|
276
|
+
* must not get an unconditional `shell: bash` it cannot run.
|
|
277
|
+
*/
|
|
278
|
+
test("a consumer-set non-bash image's gated job can still start on Forgejo (#2321)", () => {
|
|
279
|
+
const doc = parseFile(generateForgejoOpPipeline([pushSpec], { image: "alpine:3.20" }).files[0].yaml);
|
|
280
|
+
const job = doc.jobs!["app-apply"];
|
|
281
|
+
expect(job.container).toBe("alpine:3.20");
|
|
282
|
+
const step = job.steps.find((s) => s.id === "chant-run");
|
|
283
|
+
expect(step?.shell).toBeUndefined();
|
|
284
|
+
expect(step?.run).not.toContain("pipefail");
|
|
285
|
+
});
|
|
201
286
|
});
|
|
202
287
|
|
|
203
288
|
/**
|
|
@@ -239,7 +324,7 @@ describe("generateForgejoOpPipeline: a dropped deployment environment (#2257)",
|
|
|
239
324
|
|
|
240
325
|
test("a spec with no environment emits the bytes it emitted before the option existed", () => {
|
|
241
326
|
const yaml = generateForgejoOpPipeline([
|
|
242
|
-
{ name: "actions-audit", schedule: "0 6 * * *", findingMode: "
|
|
327
|
+
{ name: "actions-audit", schedule: "0 6 * * *", findingMode: "pull-request" },
|
|
243
328
|
]).files[0].yaml;
|
|
244
329
|
expect(yaml).toBe(AUDIT_YAML_BEFORE_2257);
|
|
245
330
|
});
|
|
@@ -277,3 +362,129 @@ describe("generateForgejoOpPipeline: a dropped deployment environment (#2257)",
|
|
|
277
362
|
).toThrow(/neither an absolute http\(s\) URL nor a/);
|
|
278
363
|
});
|
|
279
364
|
});
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* chant #2290 — per-Op credentials, so a pull-request job need not hold the
|
|
368
|
+
* apply credential. The consuming project's own shape: `live-check` (no
|
|
369
|
+
* `variables`) carries no credential while `live-apply` (`variables` set)
|
|
370
|
+
* carries one, both generated from forge-wide `options.variables` that name
|
|
371
|
+
* no credential at all.
|
|
372
|
+
*/
|
|
373
|
+
describe("generateForgejoOpPipeline: per-Op variables land on the job, not the workflow (#2290)", () => {
|
|
374
|
+
const FORGE_WIDE = { CHANT_FORGE: "forgejo", AWS_REGION: "${{ vars.AWS_REGION }}" };
|
|
375
|
+
const CREDENTIAL = { AWS_ACCESS_KEY_ID: "${{ secrets.AWS_ACCESS_KEY_ID }}" };
|
|
376
|
+
|
|
377
|
+
test("a job whose spec declares no variables carries none, even when the forge-wide options do", () => {
|
|
378
|
+
const doc = parseFile(
|
|
379
|
+
generateForgejoOpPipeline([{ name: "live-check", trigger: { kind: "pull_request", branches: ["main"] } }], {
|
|
380
|
+
variables: FORGE_WIDE,
|
|
381
|
+
}).files[0].yaml,
|
|
382
|
+
);
|
|
383
|
+
expect(doc.jobs!["live-check"].env).toBeUndefined();
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test("a job whose spec declares variables carries them as the job's own env:, beside the workflow env:", () => {
|
|
387
|
+
const doc = parseFile(
|
|
388
|
+
generateForgejoOpPipeline(
|
|
389
|
+
[{ name: "live-apply", trigger: { kind: "push", branches: ["main"] }, variables: CREDENTIAL }],
|
|
390
|
+
{ variables: FORGE_WIDE },
|
|
391
|
+
).files[0].yaml,
|
|
392
|
+
);
|
|
393
|
+
expect(doc.jobs!["live-apply"].env).toEqual(CREDENTIAL);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* The proof the issue names: a generated workflow whose `live-check` job
|
|
398
|
+
* carries no credential while its `live-apply` job does, from one forge-wide
|
|
399
|
+
* options object that itself names no credential — the property the
|
|
400
|
+
* consuming project (`examples/ci-pipelines`) needs to be able to state.
|
|
401
|
+
*/
|
|
402
|
+
test("live-check carries no credential while live-apply does, from the same forge-wide options", () => {
|
|
403
|
+
const files = generateForgejoOpPipeline(
|
|
404
|
+
[
|
|
405
|
+
{ name: "live-check", trigger: { kind: "pull_request", branches: ["main"] } },
|
|
406
|
+
{ name: "live-apply", trigger: { kind: "push", branches: ["main"] }, variables: CREDENTIAL },
|
|
407
|
+
],
|
|
408
|
+
{ variables: FORGE_WIDE },
|
|
409
|
+
).files;
|
|
410
|
+
const check = parseFile(files.find((f) => f.name === "live-check.yml")!.yaml);
|
|
411
|
+
const apply = parseFile(files.find((f) => f.name === "live-apply.yml")!.yaml);
|
|
412
|
+
|
|
413
|
+
expect(JSON.stringify(check.jobs!["live-check"])).not.toContain("AWS_ACCESS_KEY_ID");
|
|
414
|
+
expect(apply.jobs!["live-apply"].env).toEqual(CREDENTIAL);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
test("a per-Op key wins over a same-named forge-wide one — the job is more specific", () => {
|
|
418
|
+
const doc = parseFile(
|
|
419
|
+
generateForgejoOpPipeline(
|
|
420
|
+
[
|
|
421
|
+
{
|
|
422
|
+
name: "live-apply",
|
|
423
|
+
trigger: { kind: "push", branches: ["main"] },
|
|
424
|
+
variables: { CHANT_FORGE: "overridden" },
|
|
425
|
+
},
|
|
426
|
+
],
|
|
427
|
+
{ variables: FORGE_WIDE },
|
|
428
|
+
).files[0].yaml,
|
|
429
|
+
);
|
|
430
|
+
expect(doc.jobs!["live-apply"].env).toEqual({ CHANT_FORGE: "overridden" });
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test("a spec declaring variables changes exactly the job's env: block and nothing else", () => {
|
|
434
|
+
const spec: ScheduledOpSpec = { name: "live-apply", trigger: { kind: "push", branches: ["main"] } };
|
|
435
|
+
const before = generateForgejoOpPipeline([spec]).files[0].yaml;
|
|
436
|
+
const after = generateForgejoOpPipeline([{ ...spec, variables: CREDENTIAL }]).files[0].yaml;
|
|
437
|
+
expect(after).toContain(" env:\n AWS_ACCESS_KEY_ID: '${{ secrets.AWS_ACCESS_KEY_ID }}'\n");
|
|
438
|
+
expect(
|
|
439
|
+
after.replace(" env:\n AWS_ACCESS_KEY_ID: '${{ secrets.AWS_ACCESS_KEY_ID }}'\n", ""),
|
|
440
|
+
).toBe(before);
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* chant #2294 — the gated-apply outputs survive the notice job that consumed
|
|
446
|
+
* them. Asserted directly against forgejo's own output: no `outputs:` on the
|
|
447
|
+
* push job, no `node -e` line writing to `$GITHUB_OUTPUT`, while github's
|
|
448
|
+
* generator (same spec) still carries both because its notice job still reads
|
|
449
|
+
* them.
|
|
450
|
+
*/
|
|
451
|
+
describe("generateForgejoOpPipeline: no unconsumed outputs (#2294)", () => {
|
|
452
|
+
const pushSpec: ScheduledOpSpec = { name: "live-apply", trigger: { kind: "push", branches: ["main"] } };
|
|
453
|
+
|
|
454
|
+
test("the push job carries no outputs: block", () => {
|
|
455
|
+
const doc = parseFile(generateForgejoOpPipeline([pushSpec]).files[0].yaml);
|
|
456
|
+
expect(doc.jobs!["live-apply"].outputs).toBeUndefined();
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
test("the run step carries no node -e line writing to $GITHUB_OUTPUT", () => {
|
|
460
|
+
const doc = parseFile(generateForgejoOpPipeline([pushSpec]).files[0].yaml);
|
|
461
|
+
const step = doc.jobs!["live-apply"].steps.find((s) => s.id === "chant-run");
|
|
462
|
+
expect(step?.run).not.toContain("node -e");
|
|
463
|
+
expect(step?.run).not.toContain("GITHUB_OUTPUT");
|
|
464
|
+
// The tee'd invocation survives — it's what puts the run's own record in
|
|
465
|
+
// the log, gate-notice job or not.
|
|
466
|
+
expect(step?.run).toContain("| tee");
|
|
467
|
+
expect(step?.run).toContain("chant run live-apply --gated-exit 0 --json");
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
test("github, generated from the same spec, still carries both — the drop is forgejo's dialect, not the shared builder", () => {
|
|
471
|
+
const gh = parseFile(generateGithubOpPipeline([pushSpec]).files[0].yaml);
|
|
472
|
+
const ghStep = gh.jobs!["live-apply"].steps.find((s) => s.id === "chant-run");
|
|
473
|
+
expect(gh.jobs!["live-apply"].outputs).toEqual({
|
|
474
|
+
gated: "${{ steps.chant-run.outputs.gated }}",
|
|
475
|
+
op: "${{ steps.chant-run.outputs.op }}",
|
|
476
|
+
gate: "${{ steps.chant-run.outputs.gate }}",
|
|
477
|
+
approve: "${{ steps.chant-run.outputs.approve }}",
|
|
478
|
+
});
|
|
479
|
+
expect(ghStep?.run).toContain("node -e");
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
test("a non-push (cron) job carries no outputs and no gate script either way", () => {
|
|
483
|
+
const doc = parseFile(
|
|
484
|
+
generateForgejoOpPipeline([{ name: "live-discover", schedule: "0 6 * * *" }]).files[0].yaml,
|
|
485
|
+
);
|
|
486
|
+
expect(doc.jobs!["live-discover"].outputs).toBeUndefined();
|
|
487
|
+
const step = doc.jobs!["live-discover"].steps.find((s) => typeof s.run === "string");
|
|
488
|
+
expect(step?.run).not.toContain("node -e");
|
|
489
|
+
});
|
|
490
|
+
});
|
|
@@ -28,12 +28,52 @@
|
|
|
28
28
|
* no mapping in ../actions.ts passes through verbatim and resolves only if
|
|
29
29
|
* the runner can fetch it.
|
|
30
30
|
*
|
|
31
|
-
*
|
|
32
|
-
* triggering pull request by shelling to `gh` against
|
|
33
|
-
*
|
|
34
|
-
* GitHub
|
|
35
|
-
*
|
|
36
|
-
*
|
|
31
|
+
* `comment` (#2231) crosses over, since chant #2291: it posts onto the
|
|
32
|
+
* triggering pull request by shelling to `gh` against `${GITHUB_API_URL}`,
|
|
33
|
+
* and a Forgejo Actions job already sets that (and `github.token`) the same
|
|
34
|
+
* way a GitHub Actions job does. This generator used to refuse the mode by
|
|
35
|
+
* name here, on the premise that chant had no way to point `gh` at a Forgejo
|
|
36
|
+
* instance; that premise was checked against a real Forgejo
|
|
37
|
+
* 12.0.4+gitea-1.22.0 instance during INTENTIUS/choudoufu#1027 and found
|
|
38
|
+
* false — the failure was `gh api` resolving a *relative* path against
|
|
39
|
+
* `/api/v3`, which Forgejo does not serve, not an unreachable forge. See
|
|
40
|
+
* `reconcilePr`'s `postOrUpdateComment` (`packages/core/src/op/activities/reconcile.ts`)
|
|
41
|
+
* for the fix.
|
|
42
|
+
*
|
|
43
|
+
* `issue` (`gh issue create`/`postOrUpdateGithubIssue`) does not cross over,
|
|
44
|
+
* and chant #2315 is why: it remained un-refused-but-unverified after #2304
|
|
45
|
+
* lifted `comment`'s refusal, and settling that gap the same way — against a
|
|
46
|
+
* real instance rather than a mock — found a real failure, so the refusal
|
|
47
|
+
* below is reinstated rather than lifted. Tested against the same
|
|
48
|
+
* 12.0.4+gitea-1.22.0 image (`codeberg.org/forgejo/forgejo:12`) with a repo,
|
|
49
|
+
* an issue, and a pull request created on it: the *read* half of
|
|
50
|
+
* `postOrUpdateGithubIssue` checks out fine on Forgejo — plain paginated
|
|
51
|
+
* `GET .../issues?state=open` (no `/search/issues`, confirmed absent from a
|
|
52
|
+
* live Forgejo's own OpenAPI spec) returns pull requests interleaved with
|
|
53
|
+
* issues exactly as GitHub's endpoint does, and the `.pull_request == null`
|
|
54
|
+
* filter and the marker `startswith` match both behave identically to
|
|
55
|
+
* GitHub. The *write* half does not: `postOrUpdateGithubIssue`'s POST and
|
|
56
|
+
* PATCH calls carry only `GH_TOKEN` (in fact, on the `issue` path, not even
|
|
57
|
+
* that — `reconcilePr` hands it `execAsync` with no `env` override at all,
|
|
58
|
+
* chant #2320), and `gh`'s own documented environment variables (`gh help
|
|
59
|
+
* environment`) scope `GH_TOKEN`/`GITHUB_TOKEN` to "github.com or a subdomain
|
|
60
|
+
* of ghe.com" — never a self-hosted Forgejo. A `GH_DEBUG=api` POST against
|
|
61
|
+
* the live instance, with `GH_TOKEN` and `GITHUB_API_URL` set exactly as the
|
|
62
|
+
* generated workflow sets them, sent no `Authorization` header at all and
|
|
63
|
+
* Forgejo answered `{"message":"token is required"}` (HTTP 401); adding
|
|
64
|
+
* `GH_HOST` alongside `GH_TOKEN` made no difference. Only `GH_ENTERPRISE_TOKEN`
|
|
65
|
+
* paired with a matching `GH_HOST` authenticated the same call. Since neither
|
|
66
|
+
* `postOrUpdateGithubIssue` nor its caller sets either, every write this mode
|
|
67
|
+
* makes on Forgejo fails — not a slow search, a 401 on every run. The same
|
|
68
|
+
* shape (`GH_TOKEN` only, no `GH_HOST`/`GH_ENTERPRISE_TOKEN`) is what
|
|
69
|
+
* `postOrUpdateComment`'s writes carry too, which means #2304's "verified
|
|
70
|
+
* against a real Forgejo instance" could not have gone through the generated
|
|
71
|
+
* workflow's own credential path — either that session had a `gh auth login`
|
|
72
|
+
* already stored for the test instance, which a real Actions job's fresh
|
|
73
|
+
* checkout never has, or a different `gh` build was in play. That is
|
|
74
|
+
* `comment` mode's problem to re-verify, not `issue` mode's, and out of
|
|
75
|
+
* scope for chant #2315; flagged here because finding it is what closes that
|
|
76
|
+
* issue's question of whether to lift or reinstate this refusal.
|
|
37
77
|
*
|
|
38
78
|
* A spec's `environment` (#2257) is dropped on the same terms as
|
|
39
79
|
* `permissions:`, and for a stronger reason: Forgejo Actions has no
|
|
@@ -56,6 +96,24 @@
|
|
|
56
96
|
* Forgejo apply that stops at its gate is a green run rather than a red one,
|
|
57
97
|
* and `chant run` still writes the gate and the approve command to
|
|
58
98
|
* `GITHUB_STEP_SUMMARY`, which Forgejo Actions sets like GitHub does.
|
|
99
|
+
*
|
|
100
|
+
* The gate-notice job's own reason for being — `outputs: { gated, op, gate,
|
|
101
|
+
* approve }` on the Op's job, and the `node -e` step that writes them to
|
|
102
|
+
* `$GITHUB_OUTPUT` — does not survive it either (#2294). Both existed only for
|
|
103
|
+
* that job to read via `needs.<job>.outputs`; with the job gone, so is every
|
|
104
|
+
* reader, and `buildGithubOpPipelineDocs` is asked for neither
|
|
105
|
+
* (`emitGatedOutputs: false` below) rather than emitting them here and
|
|
106
|
+
* stripping them back out. Should the Forgejo posting work in #2291 grow a
|
|
107
|
+
* notice job of its own, this is the one flag that brings both back.
|
|
108
|
+
*
|
|
109
|
+
* A spec's own `variables` (#2290) crosses over unchanged: forgejo reuses the
|
|
110
|
+
* same job-level `env:` github's builder emits, and the dialect transform
|
|
111
|
+
* touches only `permissions:`/`environment:`/action refs, never a job's `env:`
|
|
112
|
+
* mapping. That is what makes a per-Op credential expressible here at all —
|
|
113
|
+
* `ComponentPipelineOptions.variables` is workflow-scoped and, on Forgejo,
|
|
114
|
+
* Actions mints no OIDC token to put in `setup` instead, so a job-level
|
|
115
|
+
* `variables` entry is the only way one Op's job can hold a credential no
|
|
116
|
+
* sibling Op's job receives.
|
|
59
117
|
*/
|
|
60
118
|
|
|
61
119
|
import {
|
|
@@ -117,18 +175,31 @@ export function generateForgejoOpPipeline(
|
|
|
117
175
|
options: ComponentPipelineOptions = {},
|
|
118
176
|
dialectOptions: ForgejoDialectOptions = {},
|
|
119
177
|
): OpPipelineResult {
|
|
178
|
+
// `findingMode: "comment"` used to be refused by name here (#2231); lifted
|
|
179
|
+
// in #2291 once a real Forgejo instance showed the forge itself was never
|
|
180
|
+
// the obstacle — see the module doc above.
|
|
181
|
+
|
|
182
|
+
// `findingMode: "issue"` is refused by name here (#2315): unlike `comment`,
|
|
183
|
+
// a real Forgejo instance did not clear it — see the module doc above for
|
|
184
|
+
// the reproduction.
|
|
120
185
|
for (const spec of ops) {
|
|
121
|
-
if (spec.findingMode === "
|
|
186
|
+
if (spec.findingMode === "issue") {
|
|
122
187
|
throw new Error(
|
|
123
|
-
`Scheduled Op "${spec.name}" has findingMode "
|
|
124
|
-
`
|
|
125
|
-
`
|
|
126
|
-
`
|
|
188
|
+
`Scheduled Op "${spec.name}" has findingMode "issue", which opens or edits a GitHub-shaped issue by ` +
|
|
189
|
+
`shelling to \`gh\`. Checked against a real Forgejo 12.0.4+gitea-1.22.0 instance (chant #2315): the ` +
|
|
190
|
+
`search this mode does works there, but the POST/PATCH it writes with does not — \`gh\`'s own ` +
|
|
191
|
+
`GH_TOKEN/GITHUB_TOKEN only authenticate a request to github.com or a ghe.com subdomain, never a ` +
|
|
192
|
+
`self-hosted Forgejo, and neither this mode nor its caller sets GH_HOST or GH_ENTERPRISE_TOKEN, so ` +
|
|
193
|
+
`every write fails with Forgejo's "token is required" (HTTP 401). Use findingMode "comment" on a ` +
|
|
194
|
+
`pull_request trigger here, or generate this Op for github.`,
|
|
127
195
|
);
|
|
128
196
|
}
|
|
129
197
|
}
|
|
130
198
|
|
|
131
|
-
|
|
199
|
+
// `emitGatedOutputs: false` (#2294): forgejo never carries a gate-notice job
|
|
200
|
+
// (`gatedNoticeDoc` never crosses the dialect, below), so the job outputs
|
|
201
|
+
// and the `node -e` step that populate them would have no reader.
|
|
202
|
+
const { files, jobs } = buildGithubOpPipelineDocs(ops, options, { emitGatedOutputs: false });
|
|
132
203
|
|
|
133
204
|
return {
|
|
134
205
|
// One file per spec, in spec order, which is what lets the header below
|
package/src/serializer.test.ts
CHANGED
|
@@ -128,10 +128,27 @@ describe("forgejoSerializer — multi-workflow output", () => {
|
|
|
128
128
|
// "@intentius/chant-lexicon-github"`); nothing forgejo-specific is written for
|
|
129
129
|
// it. This is the functional check that inheritance actually holds, not just
|
|
130
130
|
// that the type is importable: the dialect still drops the job's
|
|
131
|
-
// `permissions` and remaps its runner label,
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
131
|
+
// `permissions` and remaps its runner label, and the sticky-comment step — a
|
|
132
|
+
// plain `gh api` script with no `uses:` at all — passes through the dialect
|
|
133
|
+
// transform untouched, because the dialect only ever rewrites job-level
|
|
134
|
+
// `permissions:`/`environment:`/action refs and never looks at step content.
|
|
135
|
+
//
|
|
136
|
+
// That passing-through is a fact about serialization, not about whether the
|
|
137
|
+
// script would work if run: chant #2291 settled, against a real Forgejo
|
|
138
|
+
// 12.0.4+gitea-1.22.0 instance, that Forgejo's `/api/v1` does accept the same
|
|
139
|
+
// GET/POST/PATCH calls this script makes — but only when they target a full
|
|
140
|
+
// URL. Until chant #2305, this script's `gh api "repos/$REPO/issues/…"` took
|
|
141
|
+
// a bare relative path, which `gh` resolves against `/api/v3` on any host but
|
|
142
|
+
// github.com, and Forgejo does not serve `/api/v3`. `reconcilePr`'s
|
|
143
|
+
// `postOrUpdateComment` got the URL fix #2291 made
|
|
144
|
+
// (`packages/core/src/op/activities/reconcile.ts`), and `PrPlanReport`'s own
|
|
145
|
+
// script (`lexicons/github/src/composites/pr-plan-report.ts`) — out of
|
|
146
|
+
// #2291's scope, being the component composite rather than the Op generator —
|
|
147
|
+
// got the equivalent fix in #2305: every call now targets `$api_base`, built
|
|
148
|
+
// from `$GITHUB_API_URL` inline in the shell script. The dialect still leaves
|
|
149
|
+
// step content untouched, so that base-URL construction crosses over exactly
|
|
150
|
+
// as written, which is what makes the untouched script below a working
|
|
151
|
+
// Forgejo comment rather than just a passed-through one.
|
|
135
152
|
describe("forgejoSerializer — inherits PrPlanReport from github (#1983)", () => {
|
|
136
153
|
test("dialect still applies to a github-lexicon composite's job", () => {
|
|
137
154
|
const workflow = new Workflow({
|
|
@@ -154,6 +171,10 @@ describe("forgejoSerializer — inherits PrPlanReport from github (#1983)", () =
|
|
|
154
171
|
// Including the `-F` that reads the body from plan.md — forgejo renders
|
|
155
172
|
// the same script, so the #2236 regression would show up here as well.
|
|
156
173
|
expect(result.primary).toContain("-F body=@plan.md");
|
|
174
|
+
// And the #2305 fix: the resolved-base construction and every `gh api`
|
|
175
|
+
// call built from it cross the dialect untouched too.
|
|
176
|
+
expect(result.primary).toContain('api_base="${GITHUB_API_URL%/}"');
|
|
177
|
+
expect(result.primary).not.toMatch(/gh api[^\n]*"repos\//);
|
|
157
178
|
expect(result.primary).not.toContain("-f body=@");
|
|
158
179
|
});
|
|
159
180
|
});
|