@intentius/chant-lexicon-github 0.124.41 → 0.124.42

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.
@@ -1 +1 @@
1
- {"version":3,"file":"generate-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AAEH,OAAO,EAAyB,KAAK,eAAe,EAAE,MAAM,oCAAoC,CAAC;AA2BjG,OAAO,KAAK,EACV,oBAAoB,IAAI,YAAY,EACpC,wBAAwB,IAAI,qBAAqB,EACjD,uBAAuB,IAAI,oBAAoB,EAEhD,MAAM,0BAA0B,CAAC;AAIlC,YAAY,EAAE,YAAY,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,CAAC;AAE1E;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,6FAA6F;IAC7F,WAAW,EAAE,MAAM,CAAC;IACpB,8DAA8D;IAC9D,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC5B,mIAAmI;IACnI,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC;;;;;;OAMG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,qDAAqD;IACrD,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,uEAAuE;IACvE,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,0DAA0D;IAC1D,IAAI,EAAE,YAAY,EAAE,CAAC;CACtB;AAsaD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CAWpD;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,eAAe,EAAE,EAC7B,OAAO,GAAE,qBAA0B,GAClC,iBAAiB,CA+JnB;AA4CD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,iBAAiB,GAAG,MAAM,CAE/D;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,eAAe,EAAE,EAC7B,OAAO,GAAE,qBAA0B,GAClC,oBAAoB,CAGtB"}
1
+ {"version":3,"file":"generate-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AAEH,OAAO,EAAyB,KAAK,eAAe,EAAE,MAAM,oCAAoC,CAAC;AA4BjG,OAAO,KAAK,EACV,oBAAoB,IAAI,YAAY,EACpC,wBAAwB,IAAI,qBAAqB,EACjD,uBAAuB,IAAI,oBAAoB,EAEhD,MAAM,0BAA0B,CAAC;AAIlC,YAAY,EAAE,YAAY,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,CAAC;AAE1E;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,6FAA6F;IAC7F,WAAW,EAAE,MAAM,CAAC;IACpB,8DAA8D;IAC9D,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC5B,mIAAmI;IACnI,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC;;;;;;OAMG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,qDAAqD;IACrD,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,uEAAuE;IACvE,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,0DAA0D;IAC1D,IAAI,EAAE,YAAY,EAAE,CAAC;CACtB;AA2bD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CAWpD;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,eAAe,EAAE,EAC7B,OAAO,GAAE,qBAA0B,GAClC,iBAAiB,CA+JnB;AA4CD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,iBAAiB,GAAG,MAAM,CAE/D;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,eAAe,EAAE,EAC7B,OAAO,GAAE,qBAA0B,GAClC,oBAAoB,CAGtB"}
@@ -61,7 +61,7 @@ import { GATED_WAVE_RECORD, gatedWaveJobs } from "@intentius/chant/components/ga
61
61
  import { CI_IMAGE, ciJobName, dirnameOf, fillCommand, GITHUB_JOB_TOKEN, GITHUB_RUNNER } from "@intentius/chant/ci-jobs";
62
62
  import { CHECKOUT_TOKEN_ENV, checkoutScript, dropForgeTokens, handoverOutputs, handoverTrap, takeOverScript } from "@intentius/chant/ci-handover";
63
63
  import { OIDC_REQUEST_VARS, TRADE_SPEC_ENV, tradeLine } from "@intentius/chant/ci-identity";
64
- import { GREEN_TAG_PATTERN, GREEN_TICK_WORKFLOW, PR_APPLY_RECORD, PR_LOOP_REPORT_DIR, prApplyGroup, prRecordGroup, prApplyRecordKey, prApplyWaveJobName, prLoopJobs, prLoopPostJob, prLoopSetup, PR_PLAN_NOTE_JOB, prLoopWaveJobs, prLoopWithWaves, prEnvJobs, PR_ENV_REPORT_DIR, PR_ENV_SWEEP_CRON, PR_ENV_POST_JOB, } from "@intentius/chant/components/pr-pipeline";
64
+ import { APPLY_COMMENT, GREEN_TAG_PATTERN, GREEN_TICK_WORKFLOW, PR_APPLY_RECORD, PR_LOOP_REPORT_DIR, prApplyGroup, prRecordGroup, prApplyRecordKey, prApplyWaveJobName, prLoopJobs, prLoopPostJob, prLoopSetup, PR_PLAN_NOTE_JOB, prLoopWaveJobs, prLoopWithWaves, prEnvJobs, PR_ENV_REPORT_DIR, PR_ENV_SWEEP_CRON, PR_ENV_POST_JOB, } from "@intentius/chant/components/pr-pipeline";
65
65
  import { memberRepoPath } from "@intentius/chant/lexicon";
66
66
  import { afterEarlierWave, checkoutStep, concurrency, downloadArtifactStep, emitWorkflowYAML, keepRecordStep, runSteps, UPLOAD_ARTIFACT } from "./ci-jobs.js";
67
67
  import { actionRef } from "../action-pins.js";
@@ -160,6 +160,15 @@ function gatedWaveGithubDoc(components, env, image, beforeScript, extraScript, o
160
160
  * variables a token is requested with, so the pull request's code holds no
161
161
  * OIDC token and cannot request one, and the plan job names no apply
162
162
  * identity.
163
+ *
164
+ * With `applyBeforeMerge` (#4301, ws-106) the apply job runs on a
165
+ * `/chant apply` comment on a pull request (`issue_comment`), from the
166
+ * default branch's workflow file, so a pull request cannot change the job
167
+ * that applies it. It checks out the pull request's head after the setup
168
+ * and `beforeScript` ran at the default branch's commit, and runs `pr-apply
169
+ * --before-merge`, which asks the forge the requires checks before any of
170
+ * the head's code loads. On GitHub the commenter must be an owner, member
171
+ * or collaborator. A push still records.
163
172
  */
164
173
  function prLoopGithubDoc(components, env, image, beforeScript, extraScript, options) {
165
174
  const loop = options.prLoop;
@@ -226,6 +235,14 @@ function prLoopGithubDoc(components, env, image, beforeScript, extraScript, opti
226
235
  const forgejo = loop.forge === "forgejo";
227
236
  const onGreen = forgejo ? "startsWith(github.ref, 'refs/tags/ci/green/')" : "github.event_name == 'workflow_run' || github.event_name == 'workflow_dispatch'";
228
237
  const onPush = loop.applyOnMerge && forgejo ? "github.event_name == 'push' && startsWith(github.ref, 'refs/heads/')" : "github.event_name == 'push'";
238
+ // #4301: a `/chant apply` comment on a pull request starts the apply before merge. GitHub also asks for a commenter who can write.
239
+ const onComment = [
240
+ "github.event_name == 'issue_comment'",
241
+ // Forgejo's payload says so with is_pull (terragucci comment-apply.ts:129 at e6cb431).
242
+ forgejo ? "(github.event.issue.pull_request || github.event.is_pull)" : "github.event.issue.pull_request",
243
+ `startsWith(github.event.comment.body, '${APPLY_COMMENT}')`,
244
+ ...(forgejo ? [] : ["contains(fromJSON('[\"OWNER\",\"MEMBER\",\"COLLABORATOR\"]'), github.event.comment.author_association)"]),
245
+ ].join(" && ");
229
246
  const applyJob = (job, needs, name) => ({
230
247
  // #4291: a wave after the first runs after one that failed, and blocks only what depends on the failure.
231
248
  if: job.jobName === "record" ? onPush : needs.length > 0 && job.wave ? afterEarlierWave(needs[0], onGreen) : onGreen,
@@ -246,7 +263,9 @@ function prLoopGithubDoc(components, env, image, beforeScript, extraScript, opti
246
263
  ])
247
264
  : loop.applyOnMerge
248
265
  ? [["apply", applyJob(apply, [], "Apply the newest green commit under the merged changes' approvals")]]
249
- : [];
266
+ : loop.applyBeforeMerge
267
+ ? [["apply", { ...applyJob(apply, [], "Apply the pull request's head under its gate, once it is approved, mergeable, undiverged and green"), if: onComment }]]
268
+ : [];
250
269
  const applyJobs = [recordJob, ...greenJobs];
251
270
  // #4295: the plan job runs the pull request's code and holds no forge token while it does.
252
271
  const forgeName = loop.forge ?? "github";
@@ -315,6 +334,9 @@ function prLoopGithubDoc(components, env, image, beforeScript, extraScript, opti
315
334
  on.pull_request = { branches: [branch], types: ["opened", "synchronize", "reopened", "closed"] };
316
335
  on.schedule = [{ cron: PR_ENV_SWEEP_CRON }];
317
336
  }
337
+ // #4301: a /chant apply comment starts the apply before merge.
338
+ if (loop.applyBeforeMerge)
339
+ on.issue_comment = { types: ["created"] };
318
340
  return {
319
341
  name: member ? `chant-pr.${member.name}.${env}` : `chant-pr-${env}`,
320
342
  environment: env,
@@ -328,7 +350,7 @@ function prLoopGithubDoc(components, env, image, beforeScript, extraScript, opti
328
350
  { jobName: PR_PLAN_NOTE_JOB, component: "pull request plan note", stage: PR_PLAN_NOTE_JOB, needs: ["plan"] },
329
351
  ...applyNames.map((name, i) => ({
330
352
  jobName: name,
331
- component: name === "record" ? "merge record" : loop.waveGate ? `green apply, wave ${i}` : "green apply",
353
+ component: name === "record" ? "merge record" : loop.applyBeforeMerge ? "apply before merge" : loop.waveGate ? `green apply, wave ${i}` : "green apply",
332
354
  stage: name,
333
355
  // The record runs on a push and the apply on a green commit, so only the waves need each other.
334
356
  needs: i > 1 ? [applyNames[i - 1]] : [],
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "91be5cd758d335f713fe5eee9fdde779edfb2baf9ed1d38bc248487dfeb4301d",
4
+ "manifest.json": "f43fcee2c55ab1b71df96a4010da7ae13b50257d1f5f287ae9eaf4582a4403b9",
5
5
  "meta.json": "c50e0ef3845c370c3ad2210b8bba455980417c0f818aa6ed46e06d4fb8b1b9f9",
6
6
  "types/index.d.ts": "0ead95536e947ddfb98a5acf0ddb88547913b5a53a342df67711c59266f84b3e",
7
7
  "rules/deprecated-action-version.ts": "a9ba7cc924ff1c7055110416d8989273690415bfb5e8dcc32dc9431b9351f5ca",
@@ -78,5 +78,5 @@
78
78
  "skills/chant-github-patterns.md": "bb3abef289a8fdfcf07d6bb2d7289dcb2f38bc0cb0321ea320b78b45a6f548c0",
79
79
  "skills/chant-github-security.md": "aab111cb0871cad30281ce48d7da23663689619351029219e2be019a1a61e394"
80
80
  },
81
- "composite": "5c0703f8385fcd34f6d37731e92126fa3390127f40f3f829cb1eb2d33c011bee"
81
+ "composite": "20903f358de58d2aa87ad426ed158a792c906737d6064d904af4ff0da466c121"
82
82
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github",
3
- "version": "0.124.41",
3
+ "version": "0.124.42",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "GitHub",
6
6
  "intrinsics": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-github",
3
- "version": "0.124.41",
3
+ "version": "0.124.42",
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,6 +61,6 @@
61
61
  "typescript": "^5.9.3"
62
62
  },
63
63
  "peerDependencies": {
64
- "@intentius/chant": "^0.124.41"
64
+ "@intentius/chant": "^0.124.42"
65
65
  }
66
66
  }
@@ -63,6 +63,7 @@ import { CI_IMAGE, ciJobName, dirnameOf, fillCommand, GITHUB_JOB_TOKEN, GITHUB_R
63
63
  import { CHECKOUT_TOKEN_ENV, checkoutScript, dropForgeTokens, handoverOutputs, handoverTrap, takeOverScript } from "@intentius/chant/ci-handover";
64
64
  import { OIDC_REQUEST_VARS, TRADE_SPEC_ENV, tradeLine, type CloudTradeSpec } from "@intentius/chant/ci-identity";
65
65
  import {
66
+ APPLY_COMMENT,
66
67
  GREEN_TAG_PATTERN,
67
68
  GREEN_TICK_WORKFLOW,
68
69
  PR_APPLY_RECORD,
@@ -237,6 +238,15 @@ function gatedWaveGithubDoc(
237
238
  * variables a token is requested with, so the pull request's code holds no
238
239
  * OIDC token and cannot request one, and the plan job names no apply
239
240
  * identity.
241
+ *
242
+ * With `applyBeforeMerge` (#4301, ws-106) the apply job runs on a
243
+ * `/chant apply` comment on a pull request (`issue_comment`), from the
244
+ * default branch's workflow file, so a pull request cannot change the job
245
+ * that applies it. It checks out the pull request's head after the setup
246
+ * and `beforeScript` ran at the default branch's commit, and runs `pr-apply
247
+ * --before-merge`, which asks the forge the requires checks before any of
248
+ * the head's code loads. On GitHub the commenter must be an owner, member
249
+ * or collaborator. A push still records.
240
250
  */
241
251
  function prLoopGithubDoc(
242
252
  components: DriverComponent[],
@@ -310,6 +320,14 @@ function prLoopGithubDoc(
310
320
  const forgejo = loop.forge === "forgejo";
311
321
  const onGreen = forgejo ? "startsWith(github.ref, 'refs/tags/ci/green/')" : "github.event_name == 'workflow_run' || github.event_name == 'workflow_dispatch'";
312
322
  const onPush = loop.applyOnMerge && forgejo ? "github.event_name == 'push' && startsWith(github.ref, 'refs/heads/')" : "github.event_name == 'push'";
323
+ // #4301: a `/chant apply` comment on a pull request starts the apply before merge. GitHub also asks for a commenter who can write.
324
+ const onComment = [
325
+ "github.event_name == 'issue_comment'",
326
+ // Forgejo's payload says so with is_pull (terragucci comment-apply.ts:129 at e6cb431).
327
+ forgejo ? "(github.event.issue.pull_request || github.event.is_pull)" : "github.event.issue.pull_request",
328
+ `startsWith(github.event.comment.body, '${APPLY_COMMENT}')`,
329
+ ...(forgejo ? [] : ["contains(fromJSON('[\"OWNER\",\"MEMBER\",\"COLLABORATOR\"]'), github.event.comment.author_association)"]),
330
+ ].join(" && ");
313
331
  const applyJob = (job: typeof plan, needs: string[], name: string) => ({
314
332
  // #4291: a wave after the first runs after one that failed, and blocks only what depends on the failure.
315
333
  if: job.jobName === "record" ? onPush : needs.length > 0 && job.wave ? afterEarlierWave(needs[0]!, onGreen) : onGreen,
@@ -330,7 +348,9 @@ function prLoopGithubDoc(
330
348
  ])
331
349
  : loop.applyOnMerge
332
350
  ? [["apply", applyJob(apply, [], "Apply the newest green commit under the merged changes' approvals")]]
333
- : [];
351
+ : loop.applyBeforeMerge
352
+ ? [["apply", { ...applyJob(apply, [], "Apply the pull request's head under its gate, once it is approved, mergeable, undiverged and green"), if: onComment }]]
353
+ : [];
334
354
  const applyJobs: Array<[string, Record<string, unknown>]> = [recordJob, ...greenJobs];
335
355
  // #4295: the plan job runs the pull request's code and holds no forge token while it does.
336
356
  const forgeName = loop.forge ?? "github";
@@ -399,6 +419,8 @@ function prLoopGithubDoc(
399
419
  on.pull_request = { branches: [branch], types: ["opened", "synchronize", "reopened", "closed"] };
400
420
  on.schedule = [{ cron: PR_ENV_SWEEP_CRON }];
401
421
  }
422
+ // #4301: a /chant apply comment starts the apply before merge.
423
+ if (loop.applyBeforeMerge) on.issue_comment = { types: ["created"] };
402
424
  return {
403
425
  name: member ? `chant-pr.${member.name}.${env}` : `chant-pr-${env}`,
404
426
  environment: env,
@@ -412,7 +434,7 @@ function prLoopGithubDoc(
412
434
  { jobName: PR_PLAN_NOTE_JOB, component: "pull request plan note", stage: PR_PLAN_NOTE_JOB, needs: ["plan"] },
413
435
  ...applyNames.map((name, i) => ({
414
436
  jobName: name,
415
- component: name === "record" ? "merge record" : loop.waveGate ? `green apply, wave ${i}` : "green apply",
437
+ component: name === "record" ? "merge record" : loop.applyBeforeMerge ? "apply before merge" : loop.waveGate ? `green apply, wave ${i}` : "green apply",
416
438
  stage: name,
417
439
  // The record runs on a push and the apply on a green commit, so only the waves need each other.
418
440
  needs: i > 1 ? [applyNames[i - 1]] : [],
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The pull-request workflow with an apply before merge (#4301, ws-106): a
3
+ * `/chant apply` comment starts the apply job from the default branch's
4
+ * workflow, which checks out the pull request's head and runs `pr-apply
5
+ * --before-merge` through `chant ci run`. A push still records.
6
+ */
7
+
8
+ import { describe, expect, test } from "vitest";
9
+ import { parseYAML } from "@intentius/chant/yaml";
10
+ import type { DriverComponent } from "@intentius/chant/components/driver";
11
+ import { prLoopJobs, pullRequestCheckout } from "@intentius/chant/components/pr-pipeline";
12
+ import { generateGithubPipeline } from "./generate-pipeline";
13
+
14
+ const ESTATE: DriverComponent[] = [
15
+ { name: "net", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "terraform-apply", root: "net" }] }] },
16
+ ];
17
+
18
+ interface Step { name?: string; run?: string; env?: Record<string, string> }
19
+ interface Job { if: string; permissions: Record<string, string>; steps: Step[] }
20
+
21
+ describe("the GitHub workflow with --apply-before-merge", () => {
22
+ const doc = parseYAML(generateGithubPipeline(ESTATE, { env: "prod", prLoop: { applyBeforeMerge: true } }).yaml) as { on: Record<string, unknown>; jobs: Record<string, Job> };
23
+
24
+ test("listens to new comments beside pull requests and pushes, and keeps the record job on a push", () => {
25
+ expect(doc.on).toEqual({ pull_request: { branches: ["main"] }, push: { branches: ["main"] }, issue_comment: { types: ["created"] } });
26
+ expect(Object.keys(doc.jobs)).toEqual(["plan", "plan-note", "record", "apply"]);
27
+ expect(doc.jobs.record!.if).toBe("github.event_name == 'push'");
28
+ });
29
+
30
+ test("the apply runs on a /chant apply comment on a pull request from someone who can write", () => {
31
+ const cond = doc.jobs.apply!.if;
32
+ expect(cond).toContain("github.event_name == 'issue_comment'");
33
+ expect(cond).toContain("github.event.issue.pull_request");
34
+ expect(cond).toContain("startsWith(github.event.comment.body, '/chant apply')");
35
+ expect(cond).toContain("github.event.comment.author_association");
36
+ });
37
+
38
+ test("it checks out the pull request's head in its run step and applies before merge with the target branch's commit as the base", () => {
39
+ const step = doc.jobs.apply!.steps.find((s) => s.run?.includes("chant ci run apply"))!;
40
+ const lines = step.run!.trimEnd().split("\n");
41
+ expect(lines[0]).toBe(pullRequestCheckout("github"));
42
+ expect(lines.at(-1)).toBe('chant ci run apply --in-place --base "$BASE_SHA" --pr "$PR_NUMBER" --apply-before-merge --env prod --gate pr-apply --forge github');
43
+ expect(step.env).toMatchObject({ BASE_SHA: "${{ github.sha }}", PR_NUMBER: "${{ github.event.issue.number }}" });
44
+ });
45
+
46
+ test("the command chant ci run rebuilds asks pr-apply for the apply before merge", () => {
47
+ const apply = prLoopJobs("github", "prod", { applyBeforeMerge: true }).find((j) => j.jobName === "apply")!;
48
+ expect(apply.command).toBe('chant components pr-apply --before-merge --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --output .chant/pr --forge github --resume .chant/pr-resume/pr-apply.json');
49
+ });
50
+
51
+ test("it takes no apply on merge, no wave gate and no build", () => {
52
+ expect(() => prLoopJobs("github", "prod", { applyBeforeMerge: true, applyOnMerge: true })).toThrow(/--apply-on-merge/);
53
+ expect(() => prLoopJobs("github", "prod", { applyBeforeMerge: true, waveGate: "w" })).toThrow(/--wave-gate/);
54
+ expect(() => prLoopJobs("github", "prod", { applyBeforeMerge: true, build: true })).toThrow(/--pr-build/);
55
+ });
56
+
57
+ test("the pull request checkout refuses a number that is not one", () => {
58
+ expect(pullRequestCheckout("github")).toMatch(/^echo "\$PR_NUMBER" \| grep -Eq '\^\[0-9\]\+\$' && git fetch --quiet origin "\+refs\/pull\/\$PR_NUMBER\/head:/);
59
+ expect(pullRequestCheckout("gitlab")).toContain("refs/merge-requests/$PR_NUMBER/head");
60
+ });
61
+
62
+ test("on Forgejo the comment may also say so with is_pull, and there is no author association", () => {
63
+ const forgejo = parseYAML(generateGithubPipeline(ESTATE, { env: "prod", prLoop: { applyBeforeMerge: true, forge: "forgejo" } }).yaml) as { jobs: Record<string, Job> };
64
+ expect(forgejo.jobs.apply!.if).toContain("(github.event.issue.pull_request || github.event.is_pull)");
65
+ expect(forgejo.jobs.apply!.if).not.toContain("author_association");
66
+ });
67
+ });