@intentius/chant-lexicon-github 0.124.54 → 0.124.55
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/agent-jobs.d.ts +67 -0
- package/dist/components/agent-jobs.d.ts.map +1 -0
- package/dist/components/agent-jobs.js +139 -0
- package/dist/components/control-pipeline.d.ts +34 -0
- package/dist/components/control-pipeline.d.ts.map +1 -0
- package/dist/components/control-pipeline.js +45 -0
- package/dist/components/generate-op-pipeline.d.ts +8 -1
- package/dist/components/generate-op-pipeline.d.ts.map +1 -1
- package/dist/components/generate-op-pipeline.js +75 -12
- package/dist/components/generate-op-waves-pipeline.d.ts +13 -0
- package/dist/components/generate-op-waves-pipeline.d.ts.map +1 -1
- package/dist/components/generate-op-waves-pipeline.js +62 -4
- package/dist/components/generate-pipeline.d.ts.map +1 -1
- package/dist/components/generate-pipeline.js +133 -14
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/package.json +2 -2
- package/src/components/agent-jobs.ts +177 -0
- package/src/components/control-pipeline.ts +65 -0
- package/src/components/generate-op-pipeline.agent.test.ts +147 -0
- package/src/components/generate-op-pipeline.notice.test.ts +2 -1
- package/src/components/generate-op-pipeline.ts +82 -11
- package/src/components/generate-op-waves-pipeline.control.test.ts +64 -0
- package/src/components/generate-op-waves-pipeline.test.ts +17 -0
- package/src/components/generate-op-waves-pipeline.ts +81 -4
- package/src/components/generate-pipeline.ts +139 -14
- package/src/components/generate-pr-pipeline.comment-locks.test.ts +53 -0
- package/src/components/generate-pr-pipeline.comment-preview.test.ts +64 -0
- package/src/components/generate-pr-pipeline.control.test.ts +94 -0
- package/src/components/generate-pr-pipeline.pulumi-identity.test.ts +43 -0
|
@@ -63,7 +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
|
-
|
|
66
|
+
prApplyComment,
|
|
67
67
|
GREEN_TAG_PATTERN,
|
|
68
68
|
GREEN_TICK_WORKFLOW,
|
|
69
69
|
PR_APPLY_RECORD,
|
|
@@ -80,6 +80,10 @@ import {
|
|
|
80
80
|
PR_UNLOCK_JOB,
|
|
81
81
|
PR_UNLOCK_COMMAND,
|
|
82
82
|
prLockCommand,
|
|
83
|
+
prCommentLockJobs,
|
|
84
|
+
prCommentPreviewJob,
|
|
85
|
+
PREVIEW_REF_ENV,
|
|
86
|
+
PREVIEW_HEAD_OUTPUT,
|
|
83
87
|
prLoopWaveJobs,
|
|
84
88
|
prLoopWithWaves,
|
|
85
89
|
prEnvJobs,
|
|
@@ -87,7 +91,10 @@ import {
|
|
|
87
91
|
PR_ENV_SWEEP_CRON,
|
|
88
92
|
PR_ENV_POST_JOB,
|
|
89
93
|
} from "@intentius/chant/components/pr-pipeline";
|
|
94
|
+
import { forgejoRepoLockLines, REPO_APPLY_GROUP } from "@intentius/chant/components/repo-lock";
|
|
90
95
|
import { memberRepoPath } from "@intentius/chant/lexicon";
|
|
96
|
+
import { productStatusContext, resolveProductWords } from "@intentius/chant/ci-product-words";
|
|
97
|
+
import { controlDispatchJob, controlNoteCheckout } from "@intentius/chant/control/dispatch";
|
|
91
98
|
import type {
|
|
92
99
|
ComponentPipelineJob as GeneratedJob,
|
|
93
100
|
ComponentPipelineOptions as GenerateGithubOptions,
|
|
@@ -137,6 +144,8 @@ export interface GithubPipelineDoc {
|
|
|
137
144
|
|
|
138
145
|
/** The plan job's last step, whose outputs carry its report to the plan-note job (#4295). */
|
|
139
146
|
const PLAN_STEP_ID = "plan";
|
|
147
|
+
/** The comment preview's checkout step (#4620), whose output names the head it checked out. */
|
|
148
|
+
const PREVIEW_HEAD_STEP_ID = "head";
|
|
140
149
|
|
|
141
150
|
/** The output a gated apply wave hands the green commit it applies through, and the step that sets it (#4221). */
|
|
142
151
|
const WAVE_GREEN_OUTPUT = "green-sha";
|
|
@@ -292,10 +301,12 @@ function prLoopGithubDoc(
|
|
|
292
301
|
});
|
|
293
302
|
const withIdToken = (permissions: Record<string, string>): Record<string, string> => (identity ? { ...permissions, "id-token": "write" } : permissions);
|
|
294
303
|
const reportName = (job: typeof plan) => `chant-pr-${job.wave ? prApplyWaveJobName(job.wave) : job.jobName}`;
|
|
304
|
+
// #4552: on the repo grain a Forgejo apply takes the lock tag in its own step, before the command, since the runner does not queue on a concurrency group.
|
|
305
|
+
const repoLock = (job: typeof plan): string[] => (loop.lockGrain === "repo" && loop.forge === "forgejo" && job.jobName !== "record" ? forgejoRepoLockLines() : []);
|
|
295
306
|
const steps = (job: typeof plan, name: string): Array<Record<string, unknown>> => [
|
|
296
307
|
...(identity ? [tradeStep(job.jobName === "record" ? identity.plan : identity.apply, job.jobName === "record" ? "plan" : "apply")] : []),
|
|
297
308
|
checkoutStep(true),
|
|
298
|
-
...runSteps(prLoopSetup(image)),
|
|
309
|
+
...runSteps(prLoopSetup(image, loop.engineSetup)),
|
|
299
310
|
...runSteps(beforeScript),
|
|
300
311
|
...(job.build ? [{ name: "Build the templates", run: job.build[0] }, { name: "Lint the project", run: job.build[1] }] : []),
|
|
301
312
|
...(job.jobName === "apply" ? [restoreRecord(job.wave)] : []),
|
|
@@ -307,9 +318,9 @@ function prLoopGithubDoc(
|
|
|
307
318
|
name,
|
|
308
319
|
id: WAVE_STEP_ID,
|
|
309
320
|
env: { ...job.run.env, ...(job.wave > 1 ? { GREEN_SHA: `\${{ needs.${prApplyWaveJobName(job.wave - 1)}.outputs.${WAVE_GREEN_OUTPUT} }}` } : {}) },
|
|
310
|
-
run: [...(job.checkout ?? []), `echo "${WAVE_GREEN_OUTPUT}=$GREEN_SHA" >> "$GITHUB_OUTPUT"`, ...(job.setup ?? []), job.run.command].join("\n"),
|
|
321
|
+
run: [...(job.checkout ?? []), `echo "${WAVE_GREEN_OUTPUT}=$GREEN_SHA" >> "$GITHUB_OUTPUT"`, ...(job.setup ?? []), ...repoLock(job), job.run.command].join("\n"),
|
|
311
322
|
}
|
|
312
|
-
: { name, env: job.run.env, run: [...(job.checkout ?? []), ...(job.setup ?? []), job.run.command].join("\n") },
|
|
323
|
+
: { name, env: job.run.env, run: [...(job.checkout ?? []), ...(job.setup ?? []), ...repoLock(job), job.run.command].join("\n") },
|
|
313
324
|
...runSteps(extraScript),
|
|
314
325
|
...(job.jobName === "apply" ? [saveRecord(job.wave)] : []),
|
|
315
326
|
{
|
|
@@ -328,7 +339,7 @@ function prLoopGithubDoc(
|
|
|
328
339
|
"github.event_name == 'issue_comment'",
|
|
329
340
|
// Forgejo's payload says so with is_pull (terragucci comment-apply.ts:129 at e6cb431).
|
|
330
341
|
forgejo ? "(github.event.issue.pull_request || github.event.is_pull)" : "github.event.issue.pull_request",
|
|
331
|
-
`startsWith(github.event.comment.body, '${
|
|
342
|
+
`startsWith(github.event.comment.body, '${prApplyComment(loop)}')`,
|
|
332
343
|
...(forgejo ? [] : ["contains(fromJSON('[\"OWNER\",\"MEMBER\",\"COLLABORATOR\"]'), github.event.comment.author_association)"]),
|
|
333
344
|
].join(" && ");
|
|
334
345
|
const applyJob = (job: typeof plan, needs: string[], name: string) => ({
|
|
@@ -337,7 +348,8 @@ function prLoopGithubDoc(
|
|
|
337
348
|
"runs-on": GITHUB_RUNNER,
|
|
338
349
|
...(needs.length > 0 ? { needs } : {}),
|
|
339
350
|
container: image,
|
|
340
|
-
|
|
351
|
+
// #4552: on the repo grain every apply of the repository shares one group.
|
|
352
|
+
concurrency: concurrency(loop.lockGrain === "repo" ? REPO_APPLY_GROUP : prApplyGroup(env, member?.name)),
|
|
341
353
|
permissions: withIdToken({ contents: "write", "pull-requests": "write", statuses: "write" }),
|
|
342
354
|
...(job.wave ? { outputs: { [WAVE_GREEN_OUTPUT]: `\${{ steps.${WAVE_STEP_ID}.outputs.${WAVE_GREEN_OUTPUT} }}` } } : {}),
|
|
343
355
|
steps: steps(job, name),
|
|
@@ -370,7 +382,7 @@ function prLoopGithubDoc(
|
|
|
370
382
|
env: { [CHECKOUT_TOKEN_ENV]: GITHUB_JOB_TOKEN },
|
|
371
383
|
run: checkoutScript(true, true),
|
|
372
384
|
},
|
|
373
|
-
...[...prLoopSetup(image), ...beforeScript].map((line) => headStep(line)),
|
|
385
|
+
...[...prLoopSetup(image, loop.engineSetup), ...beforeScript].map((line) => headStep(line)),
|
|
374
386
|
...(plan.build ? [headStep(plan.build[0]!, "Build the templates"), headStep(plan.build[1]!, "Lint the project")] : []),
|
|
375
387
|
// The job's last step: a step after it would start beside whatever the change's code left running.
|
|
376
388
|
{
|
|
@@ -380,14 +392,40 @@ function prLoopGithubDoc(
|
|
|
380
392
|
run: [drop, ...handoverTrap(PR_LOOP_REPORT_DIR, forgeName), ...(plan.checkout ?? []), ...(plan.setup ?? []), plan.run.command, ...extraScript].join("\n"),
|
|
381
393
|
},
|
|
382
394
|
];
|
|
383
|
-
|
|
395
|
+
// #4568: once the last apply on merge succeeded, one job tells the control repo this member applied.
|
|
396
|
+
const lastGreen = loop.control && !loop.applyBeforeMerge && greenJobs.length > 0 ? greenJobs[greenJobs.length - 1]![0] : undefined;
|
|
397
|
+
const controlJobs: Array<[string, Record<string, unknown>]> = [];
|
|
398
|
+
if (loop.control && lastGreen) {
|
|
399
|
+
// A wave job hands on the commit it applied; a single apply job applies the green commit its trigger names.
|
|
400
|
+
const sha = loop.waveGate ? `\${{ needs.${lastGreen}.outputs.${WAVE_GREEN_OUTPUT} }}` : forgejo ? "${{ github.sha }}" : "${{ github.event.workflow_run.head_sha || github.sha }}";
|
|
401
|
+
const dispatch = controlDispatchJob(forgeName, loop.control.target, sha, {
|
|
402
|
+
...(loop.words ? { words: loop.words } : {}),
|
|
403
|
+
...(loop.control.sentCheck ? { sentCheck: loop.control.sentCheck } : {}),
|
|
404
|
+
...(loop.control.markSent ? { markSent: loop.control.markSent } : {}),
|
|
405
|
+
});
|
|
406
|
+
controlJobs.push([
|
|
407
|
+
dispatch.name,
|
|
408
|
+
{
|
|
409
|
+
needs: [lastGreen],
|
|
410
|
+
if: `success() && (${onGreen})`,
|
|
411
|
+
"runs-on": GITHUB_RUNNER,
|
|
412
|
+
container: image,
|
|
413
|
+
permissions: loop.control.markSent ? { contents: "read", statuses: "write" } : { contents: "read" },
|
|
414
|
+
steps: [{ name: dispatch.stepName, ...(rooted ? {} : { "working-directory": "." }), env: { ...dispatch.env, ...(loop.control.sentCheck || loop.control.markSent ? { GITHUB_TOKEN: GITHUB_JOB_TOKEN } : {}) }, run: dispatch.script }],
|
|
415
|
+
},
|
|
416
|
+
]);
|
|
417
|
+
}
|
|
418
|
+
const post = prLoopPostJob(forgeName, env, member?.name, loop.words ? resolveProductWords(loop.words) : undefined);
|
|
419
|
+
// #4619: with a control repo named, the note job reads its chant/lifecycle, read-only, so pr-post marks the cross-stack edges.
|
|
420
|
+
const controlRead = loop.control ? controlNoteCheckout(forgeName, loop.control.target, loop.words ?? {}) : undefined;
|
|
384
421
|
const noteSteps: Array<Record<string, unknown>> = [
|
|
385
422
|
// The target branch's commit: the chant this job runs is the target branch's, and none of the change's code is here.
|
|
386
423
|
{ name: "Check out the target branch's commit", uses: CHECKOUT, with: { ref: "${{ github.event.pull_request.base.sha }}" } },
|
|
387
424
|
...runSteps(beforeScript),
|
|
388
425
|
{ name: "Take the plan report from the plan job", run: takeOverScript("plan", PR_LOOP_REPORT_DIR, forgeName) },
|
|
389
426
|
{ name: "Keep the plan report", if: "always()", uses: UPLOAD_ARTIFACT, with: { name: reportName(plan), path: reportPath, "if-no-files-found": "ignore", "include-hidden-files": true } },
|
|
390
|
-
{ name:
|
|
427
|
+
...(controlRead ? [{ name: controlRead.stepName, if: "always()", env: controlRead.env, run: controlRead.script }] : []),
|
|
428
|
+
{ name: `Post the plan note and ${productStatusContext(resolveProductWords(loop.words), "plan")}`, if: "always()", env: post.env, run: post.command },
|
|
391
429
|
];
|
|
392
430
|
// #4298: with a pull request environment the pull_request trigger fires on close too, and the plan skips that event.
|
|
393
431
|
const prEnv = loop.prEnvironment ? prEnvGithubJobs(loop.forge ?? "github", image, beforeScript, extraScript, loop, member) : undefined;
|
|
@@ -428,6 +466,82 @@ function prLoopGithubDoc(
|
|
|
428
466
|
},
|
|
429
467
|
}
|
|
430
468
|
: {};
|
|
469
|
+
// #4588: `/<product> lock` and `/<product> unlock` comments, at the target branch's commit, so none of the change's code runs.
|
|
470
|
+
const commentLocks = prCommentLockJobs(forgeName, loop);
|
|
471
|
+
const commentLockJobs: Record<string, unknown> = Object.fromEntries(
|
|
472
|
+
commentLocks.map((job) => [
|
|
473
|
+
job.jobName,
|
|
474
|
+
{
|
|
475
|
+
if: [
|
|
476
|
+
"github.event_name == 'issue_comment'",
|
|
477
|
+
forgejo ? "(github.event.issue.pull_request || github.event.is_pull)" : "github.event.issue.pull_request",
|
|
478
|
+
`startsWith(github.event.comment.body, '${job.comment}')`,
|
|
479
|
+
...(forgejo ? [] : ["contains(fromJSON('[\"OWNER\",\"MEMBER\",\"COLLABORATOR\"]'), github.event.comment.author_association)"]),
|
|
480
|
+
].join(" && "),
|
|
481
|
+
"runs-on": GITHUB_RUNNER,
|
|
482
|
+
container: image,
|
|
483
|
+
permissions: { contents: "write", "pull-requests": "read", statuses: "write" },
|
|
484
|
+
steps: [
|
|
485
|
+
{ name: "Check out the target branch's commit", uses: CHECKOUT, with: { "fetch-depth": 0 } },
|
|
486
|
+
...runSteps(beforeScript),
|
|
487
|
+
{ name: job.command === "lock" ? "Keep the locks the pull request's plan took" : "Release the pull request's locks", env: job.env, run: job.line },
|
|
488
|
+
],
|
|
489
|
+
},
|
|
490
|
+
]),
|
|
491
|
+
);
|
|
492
|
+
// #4620: `/<product> <plan word>` plans the pull request's head again with a read token; the job after it posts the note.
|
|
493
|
+
const preview = prCommentPreviewJob(forgeName, env, loop, member?.name);
|
|
494
|
+
const previewJobs: Record<string, unknown> = preview
|
|
495
|
+
? {
|
|
496
|
+
[preview.jobName]: {
|
|
497
|
+
if: [
|
|
498
|
+
"github.event_name == 'issue_comment'",
|
|
499
|
+
forgejo ? "(github.event.issue.pull_request || github.event.is_pull)" : "github.event.issue.pull_request",
|
|
500
|
+
`startsWith(github.event.comment.body, '${preview.comment}')`,
|
|
501
|
+
...(forgejo ? [] : ["contains(fromJSON('[\"OWNER\",\"MEMBER\",\"COLLABORATOR\"]'), github.event.comment.author_association)"]),
|
|
502
|
+
].join(" && "),
|
|
503
|
+
"runs-on": GITHUB_RUNNER,
|
|
504
|
+
container: image,
|
|
505
|
+
permissions: withIdToken({ contents: "read" }),
|
|
506
|
+
outputs: { ...handoverOutputs(PLAN_STEP_ID, forgeName), [PREVIEW_HEAD_OUTPUT]: `\${{ steps.${PREVIEW_HEAD_STEP_ID}.outputs.${PREVIEW_HEAD_OUTPUT} }}` },
|
|
507
|
+
steps: [
|
|
508
|
+
...(identity ? [tradeStep(identity.plan, "plan")] : []),
|
|
509
|
+
// The head is read here, before any of the change's code runs, so the post job names the commit this job planned.
|
|
510
|
+
{
|
|
511
|
+
name: "Check out the pull request's head",
|
|
512
|
+
id: PREVIEW_HEAD_STEP_ID,
|
|
513
|
+
...(rooted ? {} : { "working-directory": "." }),
|
|
514
|
+
env: { [CHECKOUT_TOKEN_ENV]: GITHUB_JOB_TOKEN, [PREVIEW_REF_ENV]: "ref" in preview.head ? preview.head.ref : "" },
|
|
515
|
+
run: [checkoutScript(true, true, { refEnv: PREVIEW_REF_ENV }), `echo "${PREVIEW_HEAD_OUTPUT}=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"`].join("\n"),
|
|
516
|
+
},
|
|
517
|
+
...[...prLoopSetup(image, loop.engineSetup), ...beforeScript].map((line) => headStep(line)),
|
|
518
|
+
...(preview.plan.build ? [headStep(preview.plan.build[0]!, "Build the templates"), headStep(preview.plan.build[1]!, "Lint the project")] : []),
|
|
519
|
+
{
|
|
520
|
+
name: "Preview the members this pull request reaches",
|
|
521
|
+
id: PLAN_STEP_ID,
|
|
522
|
+
env: preview.plan.run.env,
|
|
523
|
+
run: [drop, ...handoverTrap(PR_LOOP_REPORT_DIR, forgeName), preview.base, ...(preview.plan.setup ?? []), preview.plan.run.command, ...extraScript].join("\n"),
|
|
524
|
+
},
|
|
525
|
+
],
|
|
526
|
+
},
|
|
527
|
+
[preview.noteJobName]: {
|
|
528
|
+
needs: [preview.jobName],
|
|
529
|
+
if: `always() && github.event_name == 'issue_comment' && (needs.${preview.jobName}.result == 'success' || needs.${preview.jobName}.result == 'failure')`,
|
|
530
|
+
"runs-on": GITHUB_RUNNER,
|
|
531
|
+
container: image,
|
|
532
|
+
permissions: { actions: "read", contents: "read", "pull-requests": "write", statuses: "write" },
|
|
533
|
+
steps: [
|
|
534
|
+
// The target branch's commit, which an issue_comment run checks out: none of the change's code is here.
|
|
535
|
+
{ name: "Check out the target branch's commit", uses: CHECKOUT },
|
|
536
|
+
...runSteps(beforeScript),
|
|
537
|
+
{ name: "Take the preview report from the preview job", run: takeOverScript(preview.jobName, PR_LOOP_REPORT_DIR, forgeName) },
|
|
538
|
+
{ name: "Keep the preview report", if: "always()", uses: UPLOAD_ARTIFACT, with: { name: `chant-pr-${preview.jobName}`, path: reportPath, "if-no-files-found": "ignore", "include-hidden-files": true } },
|
|
539
|
+
...(controlRead ? [{ name: controlRead.stepName, if: "always()", env: controlRead.env, run: controlRead.script }] : []),
|
|
540
|
+
{ name: `Post the preview note and ${productStatusContext(resolveProductWords(loop.words), "plan")}`, if: "always()", env: preview.post.env, run: preview.post.command },
|
|
541
|
+
],
|
|
542
|
+
},
|
|
543
|
+
}
|
|
544
|
+
: {};
|
|
431
545
|
const jobsDoc: Record<string, unknown> = {
|
|
432
546
|
plan: {
|
|
433
547
|
if: closes ? "github.event_name == 'pull_request' && github.event.action != 'closed'" : "github.event_name == 'pull_request'",
|
|
@@ -447,7 +561,10 @@ function prLoopGithubDoc(
|
|
|
447
561
|
steps: noteSteps,
|
|
448
562
|
},
|
|
449
563
|
...lockJobs,
|
|
564
|
+
...commentLockJobs,
|
|
565
|
+
...previewJobs,
|
|
450
566
|
...Object.fromEntries(applyJobs),
|
|
567
|
+
...Object.fromEntries(controlJobs),
|
|
451
568
|
...(prEnv ? prEnv.jobs : {}),
|
|
452
569
|
};
|
|
453
570
|
const applyNames = applyJobs.map(([name]) => name);
|
|
@@ -461,8 +578,8 @@ function prLoopGithubDoc(
|
|
|
461
578
|
on.pull_request = { branches: [branch], types: ["opened", "synchronize", "reopened", "closed"] };
|
|
462
579
|
on.schedule = [{ cron: PR_ENV_SWEEP_CRON }];
|
|
463
580
|
}
|
|
464
|
-
// #4301: a /chant apply comment starts the apply before merge.
|
|
465
|
-
if (loop.applyBeforeMerge) on.issue_comment = { types: ["created"] };
|
|
581
|
+
// #4301: a /chant apply comment starts the apply before merge. #4588: and a lock or unlock comment its job.
|
|
582
|
+
if (loop.applyBeforeMerge || commentLocks.length > 0 || preview) on.issue_comment = { types: ["created"] };
|
|
466
583
|
return {
|
|
467
584
|
name: member ? `chant-pr.${member.name}.${env}` : `chant-pr-${env}`,
|
|
468
585
|
environment: env,
|
|
@@ -470,7 +587,7 @@ function prLoopGithubDoc(
|
|
|
470
587
|
...(rooted ? {} : { defaults: { run: { "working-directory": member!.dir } } }),
|
|
471
588
|
env: { ...options.variables, CHANT_ENV: env },
|
|
472
589
|
jobsDoc,
|
|
473
|
-
stages: ["plan", PR_PLAN_NOTE_JOB, ...(locks ? [PR_LOCK_JOB, PR_UNLOCK_JOB] : []), ...applyNames, ...(prEnv ? ["env-up", PR_ENV_POST_JOB, "env-down", "env-sweep"] : [])],
|
|
590
|
+
stages: ["plan", PR_PLAN_NOTE_JOB, ...(locks ? [PR_LOCK_JOB, PR_UNLOCK_JOB] : []), ...commentLocks.map((j) => j.jobName), ...(preview ? [preview.jobName, preview.noteJobName] : []), ...applyNames, ...controlJobs.map(([name]) => name), ...(prEnv ? ["env-up", PR_ENV_POST_JOB, "env-down", "env-sweep"] : [])],
|
|
474
591
|
jobs: [
|
|
475
592
|
{ jobName: "plan", component: "pull request plan", stage: "plan", needs: [] },
|
|
476
593
|
{ jobName: PR_PLAN_NOTE_JOB, component: "pull request plan note", stage: PR_PLAN_NOTE_JOB, needs: ["plan"] },
|
|
@@ -480,6 +597,13 @@ function prLoopGithubDoc(
|
|
|
480
597
|
{ jobName: PR_UNLOCK_JOB, component: "pull request unlock", stage: PR_UNLOCK_JOB, needs: [] },
|
|
481
598
|
]
|
|
482
599
|
: []),
|
|
600
|
+
...commentLocks.map((j) => ({ jobName: j.jobName, component: `pull request ${j.command} comment`, stage: j.jobName, needs: [] })),
|
|
601
|
+
...(preview
|
|
602
|
+
? [
|
|
603
|
+
{ jobName: preview.jobName, component: "pull request preview comment", stage: preview.jobName, needs: [] },
|
|
604
|
+
{ jobName: preview.noteJobName, component: "pull request preview comment note", stage: preview.noteJobName, needs: [preview.jobName] },
|
|
605
|
+
]
|
|
606
|
+
: []),
|
|
483
607
|
...applyNames.map((name, i) => ({
|
|
484
608
|
jobName: name,
|
|
485
609
|
component: name === "record" ? "merge record" : loop.applyBeforeMerge ? "apply before merge" : loop.waveGate ? `green apply, wave ${i}` : "green apply",
|
|
@@ -487,6 +611,7 @@ function prLoopGithubDoc(
|
|
|
487
611
|
// The record runs on a push and the apply on a green commit, so only the waves need each other.
|
|
488
612
|
needs: i > 1 ? [applyNames[i - 1]] : [],
|
|
489
613
|
})),
|
|
614
|
+
...controlJobs.map(([name]) => ({ jobName: name, component: "control repo dispatch", stage: name, needs: [lastGreen!] })),
|
|
490
615
|
...(prEnv ? prEnv.listed : []),
|
|
491
616
|
],
|
|
492
617
|
};
|
|
@@ -547,7 +672,7 @@ function prEnvGithubJobs(
|
|
|
547
672
|
env: { [CHECKOUT_TOKEN_ENV]: GITHUB_JOB_TOKEN },
|
|
548
673
|
run: checkoutScript(true, true),
|
|
549
674
|
},
|
|
550
|
-
...[...prLoopSetup(image), ...beforeScript].map((line) => headStep(line)),
|
|
675
|
+
...[...prLoopSetup(image, loop.engineSetup), ...beforeScript].map((line) => headStep(line)),
|
|
551
676
|
...(up.build ? [headStep(up.build[0]!, "Build the templates", { CHANT_ENV: up.env.CHANT_ENV! }), headStep(up.build[1]!, "Lint the project")] : []),
|
|
552
677
|
// The job's last step: a step after it would start beside whatever the change's code left running.
|
|
553
678
|
{
|
|
@@ -583,7 +708,7 @@ function prEnvGithubJobs(
|
|
|
583
708
|
steps: [
|
|
584
709
|
...tradeStep,
|
|
585
710
|
{ uses: CHECKOUT, with: { "fetch-depth": 0, ...(ref ? { ref } : {}) } },
|
|
586
|
-
...runSteps(prLoopSetup(image)),
|
|
711
|
+
...runSteps(prLoopSetup(image, loop.engineSetup)),
|
|
587
712
|
...runSteps(beforeScript),
|
|
588
713
|
{ name, env: j.env, run: j.command },
|
|
589
714
|
...runSteps(extraScript),
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/<product> lock` and `/<product> unlock` (#4588): under `ci.locks: plan`,
|
|
3
|
+
* when the words name the commands, a job answers each comment at the
|
|
4
|
+
* target branch's commit. The lock job keeps the locks the plan took; the
|
|
5
|
+
* unlock job releases them.
|
|
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 { generateGithubPipeline } from "./generate-pipeline";
|
|
12
|
+
|
|
13
|
+
const ESTATE: DriverComponent[] = [
|
|
14
|
+
{ name: "net", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "terraform-apply", root: "net" }] }] },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const WORDS = { product: "plywood", commands: { lock: "lock" as const, unlock: "unlock" as const }, bin: "npx --no-install plywood" };
|
|
18
|
+
|
|
19
|
+
interface Step { name?: string; uses?: string; run?: string; env?: Record<string, string>; with?: Record<string, unknown> }
|
|
20
|
+
interface Job { if?: string; needs?: string[]; permissions: Record<string, string>; steps: Step[] }
|
|
21
|
+
type Doc = { on: Record<string, unknown>; jobs: Record<string, Job> };
|
|
22
|
+
|
|
23
|
+
const gen = (prLoop: Record<string, unknown>) => parseYAML(generateGithubPipeline(ESTATE, { env: "prod", prLoop }).yaml) as Doc;
|
|
24
|
+
|
|
25
|
+
describe("the GitHub workflow's lock and unlock comments", () => {
|
|
26
|
+
const doc = gen({ locks: "plan", words: WORDS });
|
|
27
|
+
|
|
28
|
+
test("a job per comment, started by issue_comment from a commenter who can write", () => {
|
|
29
|
+
expect(Object.keys(doc.jobs)).toEqual(expect.arrayContaining(["comment-lock", "comment-unlock"]));
|
|
30
|
+
expect(doc.on.issue_comment).toEqual({ types: ["created"] });
|
|
31
|
+
const lock = doc.jobs["comment-lock"]!;
|
|
32
|
+
expect(lock.if).toContain("github.event_name == 'issue_comment'");
|
|
33
|
+
expect(lock.if).toContain("startsWith(github.event.comment.body, '/plywood lock')");
|
|
34
|
+
expect(lock.if).toContain("github.event.comment.author_association");
|
|
35
|
+
expect(doc.jobs["comment-unlock"]!.if).toContain("startsWith(github.event.comment.body, '/plywood unlock')");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("the lock keeps the plan's locks through the product's bin, the unlock releases them, at the target branch's commit", () => {
|
|
39
|
+
const lock = doc.jobs["comment-lock"]!;
|
|
40
|
+
expect(lock.steps[0]!.with).toEqual({ "fetch-depth": 0 });
|
|
41
|
+
expect(lock.steps.at(-1)!.run).toBe('npx --no-install plywood lock --keep --pr "$PR_NUMBER" --forge github');
|
|
42
|
+
expect(lock.steps.at(-1)!.env!.PR_NUMBER).toBe("${{ github.event.issue.number }}");
|
|
43
|
+
expect(doc.jobs["comment-unlock"]!.steps.at(-1)!.run).toBe('npx --no-install plywood unlock --pr "$PR_NUMBER"');
|
|
44
|
+
expect(lock.permissions).toEqual({ contents: "write", "pull-requests": "read", statuses: "write" });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("neither job without ci.locks, nor without the words for the commands", () => {
|
|
48
|
+
expect(Object.keys(gen({ words: WORDS }).jobs)).not.toContain("comment-lock");
|
|
49
|
+
const chant = gen({ locks: "plan" });
|
|
50
|
+
expect(Object.keys(chant.jobs)).not.toContain("comment-lock");
|
|
51
|
+
expect(chant.on.issue_comment).toBeUndefined();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/<product> <plan word>` (#4620): with `previewComment` a job previews the
|
|
3
|
+
* pull request's head again with a read token, and the job after it posts
|
|
4
|
+
* the note from the target branch's commit, as plan and plan-note do.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from "vitest";
|
|
8
|
+
import { parseYAML } from "@intentius/chant/yaml";
|
|
9
|
+
import type { DriverComponent } from "@intentius/chant/components/driver";
|
|
10
|
+
import { generateGithubPipeline } from "./generate-pipeline";
|
|
11
|
+
|
|
12
|
+
const ESTATE: DriverComponent[] = [
|
|
13
|
+
{ name: "net", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "terraform-apply", root: "net" }] }] },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const WORDS = { product: "plywood", stages: { plan: "preview", apply: "up" }, commands: { preview: "plan" as const }, bin: "npx --no-install plywood" };
|
|
17
|
+
|
|
18
|
+
interface Step { id?: string; name?: string; uses?: string; run?: string; env?: Record<string, string>; with?: Record<string, unknown> }
|
|
19
|
+
interface Job { if?: string; needs?: string[]; permissions: Record<string, string>; outputs?: Record<string, string>; steps: Step[] }
|
|
20
|
+
type Doc = { on: Record<string, unknown>; jobs: Record<string, Job> };
|
|
21
|
+
|
|
22
|
+
const gen = (prLoop: Record<string, unknown>) => parseYAML(generateGithubPipeline(ESTATE, { env: "prod", prLoop }).yaml) as Doc;
|
|
23
|
+
|
|
24
|
+
describe("the GitHub workflow's preview comment", () => {
|
|
25
|
+
const doc = gen({ previewComment: true, words: WORDS });
|
|
26
|
+
const preview = doc.jobs["comment-preview"]!;
|
|
27
|
+
const note = doc.jobs["comment-preview-note"]!;
|
|
28
|
+
|
|
29
|
+
test("started by issue_comment with the plan word, from a commenter who can write", () => {
|
|
30
|
+
expect(doc.on.issue_comment).toEqual({ types: ["created"] });
|
|
31
|
+
expect(preview.if).toContain("github.event_name == 'issue_comment'");
|
|
32
|
+
expect(preview.if).toContain("startsWith(github.event.comment.body, '/plywood preview')");
|
|
33
|
+
expect(preview.if).toContain("github.event.comment.author_association");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("the preview job holds a read token only, and drops it before the change's code runs", () => {
|
|
37
|
+
expect(preview.permissions).toEqual({ contents: "read" });
|
|
38
|
+
const checkout = preview.steps[0]!;
|
|
39
|
+
expect(checkout.id).toBe("head");
|
|
40
|
+
expect(checkout.env!.CHANT_PREVIEW_REF).toBe("refs/pull/${{ github.event.issue.number }}/head");
|
|
41
|
+
expect(checkout.run).toContain('echo "head=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"');
|
|
42
|
+
const plan = preview.steps.at(-1)!;
|
|
43
|
+
expect(plan.run).toContain('BASE_SHA="$(git rev-parse --verify "refs/remotes/origin/main^{commit}")"');
|
|
44
|
+
expect(plan.run).toContain("npx --no-install plywood ci run plan");
|
|
45
|
+
expect(plan.env!.PR_NUMBER).toBe("${{ github.event.issue.number }}");
|
|
46
|
+
expect(plan.env!.GITHUB_TOKEN).toBeUndefined();
|
|
47
|
+
expect(preview.outputs!.head).toBe("${{ steps.head.outputs.head }}");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("the note job posts from the target branch's commit, for the head the preview job read", () => {
|
|
51
|
+
expect(note.needs).toEqual(["comment-preview"]);
|
|
52
|
+
expect(note.permissions).toEqual({ actions: "read", contents: "read", "pull-requests": "write", statuses: "write" });
|
|
53
|
+
const post = note.steps.at(-1)!;
|
|
54
|
+
expect(post.run).toBe('npx --no-install plywood components pr-post --pr "$PR_NUMBER" --head "$HEAD_SHA" --env prod --output .chant/pr --forge github --result "$PLAN_RESULT"');
|
|
55
|
+
expect(post.env!.HEAD_SHA).toBe("${{ needs.comment-preview.outputs.head }}");
|
|
56
|
+
expect(note.steps.some((s) => s.run?.includes("needs.comment-preview.outputs"))).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("no preview job without previewComment", () => {
|
|
60
|
+
const plain = gen({ words: WORDS });
|
|
61
|
+
expect(plain.jobs["comment-preview"]).toBeUndefined();
|
|
62
|
+
expect(plain.on.issue_comment).toBeUndefined();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #4568: with a control repo, the GitHub and Forgejo pull-request workflows
|
|
3
|
+
* add a `control-dispatch` job after the last apply on merge, which maps the
|
|
4
|
+
* product's secret into CHANT_CONTROL_TOKEN for its one step. The control
|
|
5
|
+
* repo's own workflow runs the member's dependents. Generated YAML only;
|
|
6
|
+
* the request itself is tested against fake forges in
|
|
7
|
+
* packages/core/src/control/control.test.ts.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, test } from "vitest";
|
|
11
|
+
import { parseYAML } from "@intentius/chant/yaml";
|
|
12
|
+
import type { DriverComponent } from "@intentius/chant/components/driver";
|
|
13
|
+
import { resolveControlTarget } from "@intentius/chant/control/dispatch";
|
|
14
|
+
import { generateGithubPipeline } from "./generate-pipeline";
|
|
15
|
+
import { generateControlPipeline } from "./control-pipeline";
|
|
16
|
+
|
|
17
|
+
const root = (name: string, dependsOn: string[] = []): DriverComponent => ({ name, dependsOn, deploy: [{ phase: "Apply", steps: [{ kind: "terraform-apply", root: name }] }] });
|
|
18
|
+
const ESTATE: DriverComponent[] = [root("net"), root("app", ["net"])];
|
|
19
|
+
const control = { target: resolveControlTarget({ repo: "github.com/acme/control", op: "deploy", tokenSecret: "PLYWOOD_CONTROL_TOKEN" }, "network") };
|
|
20
|
+
|
|
21
|
+
interface Job {
|
|
22
|
+
needs?: string[];
|
|
23
|
+
if?: string;
|
|
24
|
+
permissions?: Record<string, string>;
|
|
25
|
+
steps: Array<{ name?: string; run?: string; env?: Record<string, string> }>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
for (const forge of ["github", "forgejo"] as const) {
|
|
29
|
+
describe(`the ${forge} workflow's control dispatch (#4568)`, () => {
|
|
30
|
+
test("one job after the apply, reading the dispatch token from the product's secret", () => {
|
|
31
|
+
const { yaml } = generateGithubPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true, forge, control, words: { product: "plywood" } } });
|
|
32
|
+
const doc = parseYAML(yaml) as { jobs: Record<string, Job> };
|
|
33
|
+
const job = doc.jobs["control-dispatch"]!;
|
|
34
|
+
expect(job.needs).toEqual(["apply"]);
|
|
35
|
+
expect(job.permissions).toEqual({ contents: "read" });
|
|
36
|
+
expect(job.steps[0]!.env!.CHANT_CONTROL_TOKEN).toBe("${{ secrets.PLYWOOD_CONTROL_TOKEN }}");
|
|
37
|
+
expect(job.steps[0]!.run).toContain(forge === "github" ? "plywood-applied" : "/dispatches");
|
|
38
|
+
// No other job holds the dispatch token.
|
|
39
|
+
for (const [name, other] of Object.entries(doc.jobs)) if (name !== "control-dispatch") expect(JSON.stringify(other)).not.toContain("PLYWOOD_CONTROL_TOKEN");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("with a gate per wave, the job follows the last wave and reads the commit it applied", () => {
|
|
43
|
+
const { yaml } = generateGithubPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true, forge, control }, gatedWaves: { gate: "release" } });
|
|
44
|
+
const doc = parseYAML(yaml) as { jobs: Record<string, Job> };
|
|
45
|
+
const job = doc.jobs["control-dispatch"]!;
|
|
46
|
+
expect(job.needs).toEqual(["apply-wave-2"]);
|
|
47
|
+
expect(job.steps[0]!.env!.CHANT_CONTROL_SHA).toBe("${{ needs.apply-wave-2.outputs.green-sha }}");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("no apply on merge, no dispatch", () => {
|
|
51
|
+
const { yaml } = generateGithubPipeline(ESTATE, { env: "prod", prLoop: { forge, control } });
|
|
52
|
+
expect((parseYAML(yaml) as { jobs: Record<string, Job> }).jobs["control-dispatch"]).toBeUndefined();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe("the control repo's workflow on GitHub (#4568)", () => {
|
|
58
|
+
test("a repository_dispatch of the product's event and a manual dispatch run the dependents", () => {
|
|
59
|
+
const file = generateControlPipeline({ words: { product: "terragucci" } });
|
|
60
|
+
expect(file.path).toBe(".github/workflows/terragucci-dependents.yml");
|
|
61
|
+
const doc = parseYAML(file.content) as { on: Record<string, any>; jobs: Record<string, Job> };
|
|
62
|
+
expect(doc.on.repository_dispatch.types).toEqual(["terragucci-applied"]);
|
|
63
|
+
// #4617: the outcome an op waves dispatch carries is an input too.
|
|
64
|
+
expect(Object.keys(doc.on.workflow_dispatch.inputs)).toEqual(["member", "op", "sha", "outcome"]);
|
|
65
|
+
expect(doc.jobs.dependents!.steps[1]!.run).toContain("workspace run --dependents-of");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// #4619: the note job reads the control repo, read-only, so the note marks the cross-stack edges.
|
|
70
|
+
for (const forge of ["github", "forgejo"] as const) {
|
|
71
|
+
describe(`the ${forge} plan-note job's read of the control repo (#4619)`, () => {
|
|
72
|
+
const withRead = { target: resolveControlTarget({ repo: "github.com/acme/control", op: "deploy", tokenSecret: "PLYWOOD_CONTROL_TOKEN", readTokenSecret: "PLYWOOD_CONTROL_READ" }, "network") };
|
|
73
|
+
|
|
74
|
+
test("fetches the control repo's chant/lifecycle with the read secret before it posts", () => {
|
|
75
|
+
const { yaml } = generateGithubPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true, forge, control: withRead, words: { product: "plywood" } } });
|
|
76
|
+
const doc = parseYAML(yaml) as { jobs: Record<string, Job> };
|
|
77
|
+
const steps = doc.jobs["plan-note"]!.steps;
|
|
78
|
+
const read = steps.findIndex((s) => s.run?.includes("refs/heads/chant/lifecycle"));
|
|
79
|
+
const post = steps.findIndex((s) => s.run?.includes("components pr-post"));
|
|
80
|
+
expect(read).toBeGreaterThan(-1);
|
|
81
|
+
expect(read).toBe(post - 1);
|
|
82
|
+
expect(steps[read]!.env).toEqual({ CHANT_CONTROL_READ_TOKEN: "${{ secrets.PLYWOOD_CONTROL_READ || github.token }}" });
|
|
83
|
+
expect(steps[read]!.run).toContain('echo "CHANT_CONTROL_DIR=$chant_control_dir" >>"$GITHUB_ENV"');
|
|
84
|
+
// The plan job runs the change's code and never holds the read secret; no job but the dispatch holds the dispatch token.
|
|
85
|
+
expect(JSON.stringify(doc.jobs.plan)).not.toContain("PLYWOOD_CONTROL_READ");
|
|
86
|
+
expect(JSON.stringify(doc.jobs["plan-note"])).not.toContain("PLYWOOD_CONTROL_TOKEN");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("without a control repo the note job reads none", () => {
|
|
90
|
+
const { yaml } = generateGithubPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true, forge } });
|
|
91
|
+
expect(yaml).not.toContain("CHANT_CONTROL_DIR");
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #4549: with a Pulumi Cloud identity, the GitHub and Forgejo pull-request
|
|
3
|
+
* workflows trade the job's OIDC token for a preview token in the plan and
|
|
4
|
+
* record jobs and an up token in the apply job. No job reads a Pulumi token
|
|
5
|
+
* from the forge's secrets, and the plan job names no up token. Generated
|
|
6
|
+
* YAML only; the trade itself is tested against the fake exchange in
|
|
7
|
+
* packages/core/src/ci-identity-pulumi.test.ts.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, test } from "vitest";
|
|
11
|
+
import { parseYAML } from "@intentius/chant/yaml";
|
|
12
|
+
import type { DriverComponent } from "@intentius/chant/components/driver";
|
|
13
|
+
import { resolveCloudIdentity, TRADE_SPEC_ENV } from "@intentius/chant/ci-identity";
|
|
14
|
+
import { generateGithubPipeline } from "./generate-pipeline";
|
|
15
|
+
|
|
16
|
+
const ESTATE: DriverComponent[] = [{ name: "app", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "terraform-apply", root: "app" }] }] }];
|
|
17
|
+
|
|
18
|
+
const IDENTITY = resolveCloudIdentity({ pulumi: { organization: "acme", rules: [{ plan: "team:preview", apply: "team:deploy" }] } }, "prod")!;
|
|
19
|
+
|
|
20
|
+
interface Job {
|
|
21
|
+
permissions?: Record<string, string>;
|
|
22
|
+
steps: Array<{ name?: string; run?: string; env?: Record<string, string> }>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
for (const forge of ["github", "forgejo"] as const) {
|
|
26
|
+
describe(`the ${forge} workflow's Pulumi Cloud tokens (#4549)`, () => {
|
|
27
|
+
const { yaml } = generateGithubPipeline(ESTATE, { env: "prod", image: "example/ci:1", prLoop: { applyOnMerge: true, forge, identity: IDENTITY } });
|
|
28
|
+
const doc = parseYAML(yaml) as { jobs: Record<string, Job> };
|
|
29
|
+
const tradeOf = (job: Job) => JSON.parse(job.steps[0]!.env![TRADE_SPEC_ENV]!) as Record<string, unknown>;
|
|
30
|
+
|
|
31
|
+
test("the plan and record jobs trade for the preview token first, the apply job for the up token", () => {
|
|
32
|
+
expect(tradeOf(doc.jobs.plan!)).toEqual({ pulumi: { token: "team:preview", organization: "acme", audience: "urn:pulumi:org:acme" } });
|
|
33
|
+
expect(tradeOf(doc.jobs.record!)).toEqual(IDENTITY.plan);
|
|
34
|
+
expect(tradeOf(doc.jobs.apply!)).toEqual(IDENTITY.apply);
|
|
35
|
+
for (const name of ["plan", "record", "apply"]) expect(doc.jobs[name]!.permissions!["id-token"], name).toBe("write");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("the plan job names no up token, and no job reads a Pulumi token from the forge's secrets", () => {
|
|
39
|
+
expect(JSON.stringify(doc.jobs.plan)).not.toContain("team:deploy");
|
|
40
|
+
expect(yaml).not.toMatch(/secrets\.PULUMI_ACCESS_TOKEN/);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|