@intentius/chant-lexicon-github 0.124.57 → 0.124.58
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-pipeline.d.ts.map +1 -1
- package/dist/components/generate-pipeline.js +47 -4
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/package.json +2 -2
- package/src/components/__golden__/pr-loop-action.github.yml +228 -0
- package/src/components/generate-pipeline.ts +48 -3
- package/src/components/generate-pr-pipeline.action.test.ts +67 -0
|
@@ -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;
|
|
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;AA6CjG,OAAO,KAAK,EACV,oBAAoB,IAAI,YAAY,EACpC,wBAAwB,IAAI,qBAAqB,EACjD,uBAAuB,IAAI,oBAAoB,EAEhD,MAAM,0BAA0B,CAAC;AAGlC,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;AA6qBD;;;;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, githubRunsOn } 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 { prApplyComment, GREEN_TAG_PATTERN, GREEN_TICK_WORKFLOW, PR_APPLY_RECORD, PR_LOOP_REPORT_DIR, prApplyGroup, prRecordGroup, prApplyRecordKey, prApplyWaveJobName, prLoopJobs, prLoopPostJob, prLoopSetup, PR_PLAN_NOTE_JOB, PR_LOCK_JOB, PR_UNLOCK_JOB, prUnlockCommand, prLockCommand, prCommentLockJobs, prCommentPreviewJob, PREVIEW_REF_ENV, PREVIEW_HEAD_OUTPUT, prLoopWaveJobs, prLoopWithWaves, prEnvJobs, prLoopJobStage, prStaleJob, PR_STALE_JOB, prLoopRunner, PR_ENV_REPORT_DIR, PR_ENV_SWEEP_CRON, PR_ENV_POST_JOB, } from "@intentius/chant/components/pr-pipeline";
|
|
64
|
+
import { prApplyComment, GREEN_TAG_PATTERN, GREEN_TICK_WORKFLOW, PR_APPLY_RECORD, PR_LOOP_REPORT_DIR, prApplyGroup, prRecordGroup, prApplyRecordKey, prApplyWaveJobName, prLoopJobs, prLoopPostJob, prLoopSetup, PR_PLAN_NOTE_JOB, PR_LOCK_JOB, PR_UNLOCK_JOB, prUnlockCommand, prLockCommand, prCommentLockJobs, prCommentPreviewJob, prActionJob, prLoopPassedEnv, PREVIEW_REF_ENV, PREVIEW_HEAD_OUTPUT, prLoopWaveJobs, prLoopWithWaves, prEnvJobs, prLoopJobStage, prStaleJob, PR_STALE_JOB, prLoopRunner, PR_ENV_REPORT_DIR, PR_ENV_SWEEP_CRON, PR_ENV_POST_JOB, } from "@intentius/chant/components/pr-pipeline";
|
|
65
65
|
import { forgejoRepoLockLines, REPO_APPLY_GROUP } from "@intentius/chant/components/repo-lock";
|
|
66
66
|
import { memberRepoPath } from "@intentius/chant/lexicon";
|
|
67
67
|
import { productStatusContext, resolveProductWords } from "@intentius/chant/ci-product-words";
|
|
@@ -239,7 +239,10 @@ function prLoopGithubDoc(components, env, image, beforeScript, extraScript, opti
|
|
|
239
239
|
];
|
|
240
240
|
// #4168 step 9: under apply on merge the apply runs on a green signal, and a push to the branch records.
|
|
241
241
|
const forgejo = loop.forge === "forgejo";
|
|
242
|
-
|
|
242
|
+
// #4654: the action job answers its comment and a dispatch that names an action; such a dispatch starts no apply.
|
|
243
|
+
const action = prActionJob(loop.forge ?? "github", loop);
|
|
244
|
+
const plainDispatch = action ? `(github.event_name == 'workflow_dispatch' && github.event.inputs.${action.inputs.action} == '')` : "github.event_name == 'workflow_dispatch'";
|
|
245
|
+
const onGreen = forgejo ? "startsWith(github.ref, 'refs/tags/ci/green/')" : `github.event_name == 'workflow_run' || ${plainDispatch}`;
|
|
243
246
|
const onPush = loop.applyOnMerge && forgejo ? "github.event_name == 'push' && startsWith(github.ref, 'refs/heads/')" : "github.event_name == 'push'";
|
|
244
247
|
// #4301: a `/chant apply` comment on a pull request starts the apply before merge. GitHub also asks for a commenter who can write.
|
|
245
248
|
const onComment = [
|
|
@@ -467,6 +470,34 @@ function prLoopGithubDoc(components, env, image, beforeScript, extraScript, opti
|
|
|
467
470
|
},
|
|
468
471
|
}
|
|
469
472
|
: {};
|
|
473
|
+
// #4654: `/<product> <action word> <member> <action>`, or a dispatch naming both, runs the product's action verb at the target branch's commit.
|
|
474
|
+
const actionJobs = action
|
|
475
|
+
? {
|
|
476
|
+
[action.jobName]: {
|
|
477
|
+
if: [
|
|
478
|
+
`(github.event_name == 'workflow_dispatch' && github.event.inputs.${action.inputs.action} != '')`,
|
|
479
|
+
`(${[
|
|
480
|
+
"github.event_name == 'issue_comment'",
|
|
481
|
+
`startsWith(github.event.comment.body, '${action.comment.replace(/'/g, "''")} ')`,
|
|
482
|
+
...(forgejo ? [] : ["contains(fromJSON('[\"OWNER\",\"MEMBER\",\"COLLABORATOR\"]'), github.event.comment.author_association)"]),
|
|
483
|
+
].join(" && ")})`,
|
|
484
|
+
].join(" || "),
|
|
485
|
+
"runs-on": GITHUB_RUNNER,
|
|
486
|
+
container: image,
|
|
487
|
+
// One action of a pipeline at a time, and never two runs of one request.
|
|
488
|
+
concurrency: concurrency(`chant-action-${env}${member ? `-${member.name}` : ""}`),
|
|
489
|
+
permissions: withIdToken({ contents: "write", issues: "write", "pull-requests": "write" }),
|
|
490
|
+
steps: [
|
|
491
|
+
...(identity ? [tradeStep(identity.apply, "apply")] : []),
|
|
492
|
+
// An issue_comment run checks out the target branch's commit, a dispatch the ref it names: never a pull request's head.
|
|
493
|
+
{ name: "Check out the target branch's commit", uses: CHECKOUT, with: { "fetch-depth": 0 } },
|
|
494
|
+
...runSteps(prLoopSetup(image, loop.engineSetup)),
|
|
495
|
+
...runSteps(beforeScript),
|
|
496
|
+
{ name: "Run the requested action under its gated request", env: { ...prLoopPassedEnv(forgeName, loop), ...action.env }, run: action.script },
|
|
497
|
+
],
|
|
498
|
+
},
|
|
499
|
+
}
|
|
500
|
+
: {};
|
|
470
501
|
const jobsDoc = {
|
|
471
502
|
plan: {
|
|
472
503
|
if: closes ? "github.event_name == 'pull_request' && github.event.action != 'closed'" : "github.event_name == 'pull_request'",
|
|
@@ -488,6 +519,7 @@ function prLoopGithubDoc(components, env, image, beforeScript, extraScript, opti
|
|
|
488
519
|
...lockJobs,
|
|
489
520
|
...commentLockJobs,
|
|
490
521
|
...previewJobs,
|
|
522
|
+
...actionJobs,
|
|
491
523
|
...staleJobs,
|
|
492
524
|
...Object.fromEntries(applyJobs),
|
|
493
525
|
...Object.fromEntries(controlJobs),
|
|
@@ -506,8 +538,18 @@ function prLoopGithubDoc(components, env, image, beforeScript, extraScript, opti
|
|
|
506
538
|
on.schedule = [{ cron: PR_ENV_SWEEP_CRON }];
|
|
507
539
|
}
|
|
508
540
|
// #4301: a /chant apply comment starts the apply before merge. #4588: and a lock or unlock comment its job.
|
|
509
|
-
if (loop.applyBeforeMerge || commentLocks.length > 0 || preview)
|
|
541
|
+
if (loop.applyBeforeMerge || commentLocks.length > 0 || preview || action)
|
|
510
542
|
on.issue_comment = { types: ["created"] };
|
|
543
|
+
// #4654: a dispatch names a member and an action; both empty, it is the dispatch it was before.
|
|
544
|
+
if (action) {
|
|
545
|
+
const input = (description) => ({ description, required: false, type: "string", default: "" });
|
|
546
|
+
on.workflow_dispatch = {
|
|
547
|
+
inputs: {
|
|
548
|
+
[action.inputs.member]: input(`The ${resolveProductWords(loop.words).noun[0]} the action runs on`),
|
|
549
|
+
[action.inputs.action]: input("The action to run, by its name"),
|
|
550
|
+
},
|
|
551
|
+
};
|
|
552
|
+
}
|
|
511
553
|
// #4625: each job on its stage's runner, the plan side's, the apply side's, or the default.
|
|
512
554
|
if (loop.runners) {
|
|
513
555
|
for (const [name, job] of Object.entries(jobsDoc)) {
|
|
@@ -521,7 +563,7 @@ function prLoopGithubDoc(components, env, image, beforeScript, extraScript, opti
|
|
|
521
563
|
...(rooted ? {} : { defaults: { run: { "working-directory": member.dir } } }),
|
|
522
564
|
env: { ...options.variables, CHANT_ENV: env },
|
|
523
565
|
jobsDoc,
|
|
524
|
-
stages: ["plan", PR_PLAN_NOTE_JOB, ...(locks ? [PR_LOCK_JOB, PR_UNLOCK_JOB] : []), ...commentLocks.map((j) => j.jobName), ...(preview ? [preview.jobName, preview.noteJobName] : []), ...(stale ? [PR_STALE_JOB] : []), ...applyNames, ...controlJobs.map(([name]) => name), ...(prEnv ? ["env-up", PR_ENV_POST_JOB, "env-down", "env-sweep"] : [])],
|
|
566
|
+
stages: ["plan", PR_PLAN_NOTE_JOB, ...(locks ? [PR_LOCK_JOB, PR_UNLOCK_JOB] : []), ...commentLocks.map((j) => j.jobName), ...(preview ? [preview.jobName, preview.noteJobName] : []), ...(action ? [action.jobName] : []), ...(stale ? [PR_STALE_JOB] : []), ...applyNames, ...controlJobs.map(([name]) => name), ...(prEnv ? ["env-up", PR_ENV_POST_JOB, "env-down", "env-sweep"] : [])],
|
|
525
567
|
jobs: [
|
|
526
568
|
{ jobName: "plan", component: "pull request plan", stage: "plan", needs: [] },
|
|
527
569
|
{ jobName: PR_PLAN_NOTE_JOB, component: "pull request plan note", stage: PR_PLAN_NOTE_JOB, needs: ["plan"] },
|
|
@@ -539,6 +581,7 @@ function prLoopGithubDoc(components, env, image, beforeScript, extraScript, opti
|
|
|
539
581
|
{ jobName: preview.noteJobName, component: "pull request preview comment note", stage: preview.noteJobName, needs: [preview.jobName] },
|
|
540
582
|
]
|
|
541
583
|
: []),
|
|
584
|
+
...(action ? [{ jobName: action.jobName, component: "day-2 action", stage: action.jobName, needs: [] }] : []),
|
|
542
585
|
...applyNames.map((name, i) => ({
|
|
543
586
|
jobName: name,
|
|
544
587
|
component: name === "record" ? "merge record" : loop.applyBeforeMerge ? "apply before merge" : loop.waveGate ? `green apply, wave ${i}` : "green apply",
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "418be40e957e0634548ba848e3b607febd9850185be3dfd8d278b7cb356aa61a",
|
|
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": "
|
|
81
|
+
"composite": "f8d68a8078ebf75ac45adc4efce5ab4b263bc8c4808cdee6c07380458a1f0390"
|
|
82
82
|
}
|
package/dist/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-github",
|
|
3
|
-
"version": "0.124.
|
|
3
|
+
"version": "0.124.58",
|
|
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.
|
|
64
|
+
"@intentius/chant": "^0.124.58"
|
|
65
65
|
}
|
|
66
66
|
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
name: chant-pr-prod
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
push:
|
|
8
|
+
branches:
|
|
9
|
+
- main
|
|
10
|
+
workflow_run:
|
|
11
|
+
workflows:
|
|
12
|
+
- chant-ci-green
|
|
13
|
+
types:
|
|
14
|
+
- completed
|
|
15
|
+
workflow_dispatch:
|
|
16
|
+
inputs:
|
|
17
|
+
stack:
|
|
18
|
+
description: The stack the action runs on
|
|
19
|
+
required: false
|
|
20
|
+
type: string
|
|
21
|
+
default: ''
|
|
22
|
+
action:
|
|
23
|
+
description: The action to run, by its name
|
|
24
|
+
required: false
|
|
25
|
+
type: string
|
|
26
|
+
default: ''
|
|
27
|
+
issue_comment:
|
|
28
|
+
types:
|
|
29
|
+
- created
|
|
30
|
+
|
|
31
|
+
env:
|
|
32
|
+
CHANT_ENV: prod
|
|
33
|
+
|
|
34
|
+
jobs:
|
|
35
|
+
plan:
|
|
36
|
+
if: github.event_name == 'pull_request'
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
container: node:22
|
|
39
|
+
permissions:
|
|
40
|
+
contents: read
|
|
41
|
+
outputs:
|
|
42
|
+
report_0: '${{ steps.plan.outputs.report_0 }}'
|
|
43
|
+
steps:
|
|
44
|
+
- name: Check out the change
|
|
45
|
+
env:
|
|
46
|
+
CHANT_CHECKOUT_TOKEN: '${{ github.token }}'
|
|
47
|
+
run: |
|
|
48
|
+
set -eu
|
|
49
|
+
git config --global --add safe.directory "$PWD" 2>/dev/null || true
|
|
50
|
+
[ -d .git ] || git init -q .
|
|
51
|
+
git remote get-url origin >/dev/null 2>&1 || git remote add origin "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY"
|
|
52
|
+
if [ -n "${CHANT_CHECKOUT_TOKEN:-}" ]; then
|
|
53
|
+
chant_auth="$(printf 'x-access-token:%s' "$CHANT_CHECKOUT_TOKEN" | base64 | tr -d '\n')"
|
|
54
|
+
export GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=http.extraheader GIT_CONFIG_VALUE_0="AUTHORIZATION: basic $chant_auth"
|
|
55
|
+
unset chant_auth
|
|
56
|
+
fi
|
|
57
|
+
ref="$GITHUB_REF"
|
|
58
|
+
want="$GITHUB_SHA"
|
|
59
|
+
case "$ref" in
|
|
60
|
+
refs/heads/*) target="refs/remotes/origin/${ref#refs/heads/}" ;;
|
|
61
|
+
refs/tags/*) target="$ref" ;;
|
|
62
|
+
refs/*) target="refs/remotes/${ref#refs/}" ;;
|
|
63
|
+
*) target="refs/remotes/origin/chant-head" ;;
|
|
64
|
+
esac
|
|
65
|
+
if git rev-parse -q --verify HEAD >/dev/null; then git clean -ffdxq; fi
|
|
66
|
+
git -c protocol.version=2 fetch -q --prune --no-recurse-submodules origin '+refs/heads/*:refs/remotes/origin/*' '+refs/tags/*:refs/tags/*' "+$want:$target"
|
|
67
|
+
git -c protocol.version=2 fetch -q --no-tags origin '+refs/heads/chant/lifecycle:refs/heads/chant/lifecycle' 2>/dev/null || true
|
|
68
|
+
case "$ref" in
|
|
69
|
+
refs/heads/*) git checkout -q --force -B "${ref#refs/heads/}" "$target" ;;
|
|
70
|
+
*) git checkout -q --force --detach "$target" ;;
|
|
71
|
+
esac
|
|
72
|
+
- run: |
|
|
73
|
+
if [ -z "${CHANT_NO_FORGE_TOKEN:-}" ] && [ "${0#*/}" != "$0" ] && [ -f "$0" ] && ! { [ "${0##*/}" = sh ] || [ "${0##*/}" = bash ] || [ "${0##*/}" = dash ]; } && command -v env >/dev/null 2>&1 && command -v sh >/dev/null 2>&1; then exec env -u CHANT_FORGE_TOKEN -u CHANT_GITHUB_TOKEN -u CHANT_FORGEJO_TOKEN -u CHANT_GITLAB_TOKEN -u CHANT_CHECKOUT_TOKEN -u GITHUB_TOKEN -u GH_TOKEN -u FORGEJO_TOKEN -u GITEA_TOKEN -u GITLAB_TOKEN -u CI_JOB_TOKEN -u ACTIONS_RUNTIME_TOKEN CHANT_NO_FORGE_TOKEN=1 sh -e "$0"; fi; unset CHANT_FORGE_TOKEN CHANT_GITHUB_TOKEN CHANT_FORGEJO_TOKEN CHANT_GITLAB_TOKEN CHANT_CHECKOUT_TOKEN GITHUB_TOKEN GH_TOKEN FORGEJO_TOKEN GITEA_TOKEN GITLAB_TOKEN CI_JOB_TOKEN ACTIONS_RUNTIME_TOKEN
|
|
74
|
+
command -v tofu >/dev/null 2>&1 || (curl -fsSL https://get.opentofu.org/install-opentofu.sh -o /tmp/install-opentofu.sh && sh /tmp/install-opentofu.sh --install-method standalone)
|
|
75
|
+
- name: Plan the members this pull request reaches
|
|
76
|
+
id: plan
|
|
77
|
+
env:
|
|
78
|
+
BASE_SHA: '${{ github.event.pull_request.base.sha }}'
|
|
79
|
+
PR_NUMBER: '${{ github.event.pull_request.number }}'
|
|
80
|
+
run: |
|
|
81
|
+
if [ -z "${CHANT_NO_FORGE_TOKEN:-}" ] && [ "${0#*/}" != "$0" ] && [ -f "$0" ] && ! { [ "${0##*/}" = sh ] || [ "${0##*/}" = bash ] || [ "${0##*/}" = dash ]; } && command -v env >/dev/null 2>&1 && command -v sh >/dev/null 2>&1; then exec env -u CHANT_FORGE_TOKEN -u CHANT_GITHUB_TOKEN -u CHANT_FORGEJO_TOKEN -u CHANT_GITLAB_TOKEN -u CHANT_CHECKOUT_TOKEN -u GITHUB_TOKEN -u GH_TOKEN -u FORGEJO_TOKEN -u GITEA_TOKEN -u GITLAB_TOKEN -u CI_JOB_TOKEN -u ACTIONS_RUNTIME_TOKEN CHANT_NO_FORGE_TOKEN=1 sh -e "$0"; fi; unset CHANT_FORGE_TOKEN CHANT_GITHUB_TOKEN CHANT_FORGEJO_TOKEN CHANT_GITLAB_TOKEN CHANT_CHECKOUT_TOKEN GITHUB_TOKEN GH_TOKEN FORGEJO_TOKEN GITEA_TOKEN GITLAB_TOKEN CI_JOB_TOKEN ACTIONS_RUNTIME_TOKEN
|
|
82
|
+
chant_report="$PWD/.chant/pr"
|
|
83
|
+
chant_handover() { node -e 'const fs=require("fs"),p=require("path"),z=require("zlib");const [dir,out]=process.argv.slice(1);const CHUNK=450000,PARTS=1,NOTE=["pr-plan.json","pr-note.md"];const all={};const walk=(d,r)=>{let n=[];try{n=fs.readdirSync(d).sort()}catch{return}for(const x of n){const f=p.join(d,x),k=r?r+"/"+x:x;let s;try{s=fs.lstatSync(f)}catch{continue}if(s.isDirectory())walk(f,k);else if(s.isFile())all[k]=fs.readFileSync(f).toString("base64")}};walk(dir,"");const pack=(o)=>z.gzipSync(JSON.stringify(o)).toString("base64");let b=pack(all);if(b.length>CHUNK*PARTS){b=pack(Object.fromEntries(Object.entries(all).filter(([k])=>NOTE.includes(k))));if(b.length>CHUNK*PARTS){b=pack({});console.error("chant: the plan report is too large to hand to the note job, even cut to the report and the note; it hands on none of it")}else console.error("chant: the plan report is too large to hand to the note job whole; it hands on pr-plan.json and pr-note.md")}let o="";for(let i=0;i*CHUNK<b.length;i++)o+="report_"+i+"="+b.slice(i*CHUNK,(i+1)*CHUNK)+"\n";fs.appendFileSync(out,o);' "$chant_report" "${GITHUB_OUTPUT:-/dev/null}" || echo "chant: the plan report could not be handed to the note job" >&2; }
|
|
84
|
+
trap chant_handover EXIT
|
|
85
|
+
npx --no-install plywood ci run plan --chant "npx --no-install plywood" --in-place --no-push --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --forge github
|
|
86
|
+
plan-note:
|
|
87
|
+
needs:
|
|
88
|
+
- plan
|
|
89
|
+
if: always() && github.event_name == 'pull_request' && (needs.plan.result == 'success' || needs.plan.result == 'failure')
|
|
90
|
+
runs-on: ubuntu-latest
|
|
91
|
+
container: node:22
|
|
92
|
+
permissions:
|
|
93
|
+
actions: read
|
|
94
|
+
contents: read
|
|
95
|
+
pull-requests: write
|
|
96
|
+
statuses: write
|
|
97
|
+
steps:
|
|
98
|
+
- name: Check out the target branch's commit
|
|
99
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
100
|
+
with:
|
|
101
|
+
ref: '${{ github.event.pull_request.base.sha }}'
|
|
102
|
+
- name: Take the plan report from the plan job
|
|
103
|
+
run: |
|
|
104
|
+
set -u
|
|
105
|
+
node -e 'const fs=require("fs"),p=require("path"),z=require("zlib");const dir=process.argv[1];let b="";for(const l of fs.readFileSync(0,"utf8").split("\n")){if(!l.trim())continue;let s="";try{s=JSON.parse(l)}catch{}if(typeof s==="string")b+=s}if(!b){console.log("chant: the plan job handed on no report");process.exit(0)}let files;try{files=JSON.parse(z.gunzipSync(Buffer.from(b,"base64"),{maxOutputLength:268435456}).toString("utf8"))}catch(e){console.log("chant: the report the plan job handed on does not read: "+e.message);process.exit(0)}if(!files||typeof files!=="object"||Array.isArray(files)){console.log("chant: the report the plan job handed on is not a set of files");process.exit(0)}let n=0;for(const [k,v] of Object.entries(files)){const parts=k.split("/");if(typeof v!=="string"||k.length>1024||parts.some((x)=>!x||x==="."||x===".."||/[\\\x00-\x1f]/.test(x))){console.log("chant: left out "+JSON.stringify(k));continue}try{const f=p.join(dir,...parts);fs.mkdirSync(p.dirname(f),{recursive:true});fs.writeFileSync(f,Buffer.from(v,"base64"));n++}catch(e){console.log("chant: could not write "+JSON.stringify(k)+": "+e.message)}}console.log("chant: took "+n+" report files from the plan job")' .chant/pr <<'END_OF_REPORT'
|
|
106
|
+
${{ toJSON(needs.plan.outputs.report_0) }}
|
|
107
|
+
END_OF_REPORT
|
|
108
|
+
- name: Keep the plan report
|
|
109
|
+
if: always()
|
|
110
|
+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
|
111
|
+
with:
|
|
112
|
+
name: chant-pr-plan
|
|
113
|
+
path: .chant/pr
|
|
114
|
+
if-no-files-found: ignore
|
|
115
|
+
include-hidden-files: true
|
|
116
|
+
- name: Post the plan note and plywood/plan
|
|
117
|
+
if: always()
|
|
118
|
+
env:
|
|
119
|
+
PR_NUMBER: '${{ github.event.pull_request.number }}'
|
|
120
|
+
HEAD_SHA: '${{ github.sha }}'
|
|
121
|
+
PLAN_RESULT: '${{ needs.plan.result }}'
|
|
122
|
+
GITHUB_TOKEN: '${{ github.token }}'
|
|
123
|
+
run: npx --no-install plywood components pr-post --pr "$PR_NUMBER" --head "$HEAD_SHA" --env prod --output .chant/pr --forge github --result "$PLAN_RESULT"
|
|
124
|
+
action:
|
|
125
|
+
if: (github.event_name == 'workflow_dispatch' && github.event.inputs.action != '') || (github.event_name == 'issue_comment' && startsWith(github.event.comment.body, '/plywood action ') && contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association))
|
|
126
|
+
runs-on: ubuntu-latest
|
|
127
|
+
container: node:22
|
|
128
|
+
concurrency:
|
|
129
|
+
group: chant-action-prod
|
|
130
|
+
cancel-in-progress: false
|
|
131
|
+
permissions:
|
|
132
|
+
contents: write
|
|
133
|
+
issues: write
|
|
134
|
+
pull-requests: write
|
|
135
|
+
steps:
|
|
136
|
+
- name: Check out the target branch's commit
|
|
137
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
138
|
+
with:
|
|
139
|
+
fetch-depth: 0
|
|
140
|
+
- run: command -v tofu >/dev/null 2>&1 || (curl -fsSL https://get.opentofu.org/install-opentofu.sh -o /tmp/install-opentofu.sh && sh /tmp/install-opentofu.sh --install-method standalone)
|
|
141
|
+
- name: Run the requested action under its gated request
|
|
142
|
+
env:
|
|
143
|
+
CHANT_ACTION_COMMENT: '${{ github.event.comment.body }}'
|
|
144
|
+
CHANT_ACTION_MEMBER: '${{ github.event.inputs.stack }}'
|
|
145
|
+
CHANT_ACTION: '${{ github.event.inputs.action }}'
|
|
146
|
+
PR_NUMBER: '${{ github.event.issue.number }}'
|
|
147
|
+
GITHUB_TOKEN: '${{ github.token }}'
|
|
148
|
+
run: |
|
|
149
|
+
set +e
|
|
150
|
+
npx --no-install plywood action --from-event
|
|
151
|
+
code="$?"
|
|
152
|
+
if [ "$code" -eq 3 ]; then echo "The action waits for an approval."; exit 0; fi
|
|
153
|
+
exit "$code"
|
|
154
|
+
record:
|
|
155
|
+
if: github.event_name == 'push'
|
|
156
|
+
runs-on: ubuntu-latest
|
|
157
|
+
container: node:22
|
|
158
|
+
concurrency:
|
|
159
|
+
group: chant-record-prod
|
|
160
|
+
cancel-in-progress: false
|
|
161
|
+
permissions:
|
|
162
|
+
contents: write
|
|
163
|
+
pull-requests: write
|
|
164
|
+
statuses: write
|
|
165
|
+
steps:
|
|
166
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
167
|
+
with:
|
|
168
|
+
fetch-depth: 0
|
|
169
|
+
- run: command -v tofu >/dev/null 2>&1 || (curl -fsSL https://get.opentofu.org/install-opentofu.sh -o /tmp/install-opentofu.sh && sh /tmp/install-opentofu.sh --install-method standalone)
|
|
170
|
+
- name: Record the plan on the merged commit; nothing applies
|
|
171
|
+
env:
|
|
172
|
+
BASE_SHA: '${{ github.event.before }}'
|
|
173
|
+
GITHUB_TOKEN: '${{ github.token }}'
|
|
174
|
+
CHANT_RUNNER_KEY: '${{ secrets.CHANT_RUNNER_KEY }}'
|
|
175
|
+
run: |
|
|
176
|
+
if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then git fetch origin "main" && BASE_SHA="$(git merge-base FETCH_HEAD "$GITHUB_SHA")"; if [ "$BASE_SHA" = "$GITHUB_SHA" ]; then BASE_SHA="$(git rev-parse --verify --quiet "$GITHUB_SHA^" || echo 4b825dc642cb6eb9a060e54bf8d69288fbee4904)"; fi; fi; export BASE_SHA
|
|
177
|
+
npx --no-install plywood ci run record --chant "npx --no-install plywood" --in-place --base "$BASE_SHA" --env prod --gate pr-apply --forge github
|
|
178
|
+
- name: Keep the record report
|
|
179
|
+
if: always()
|
|
180
|
+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
|
181
|
+
with:
|
|
182
|
+
name: chant-pr-record
|
|
183
|
+
path: .chant/pr
|
|
184
|
+
if-no-files-found: ignore
|
|
185
|
+
include-hidden-files: true
|
|
186
|
+
apply:
|
|
187
|
+
if: github.event_name == 'workflow_run' || (github.event_name == 'workflow_dispatch' && github.event.inputs.action == '')
|
|
188
|
+
runs-on: ubuntu-latest
|
|
189
|
+
container: node:22
|
|
190
|
+
concurrency:
|
|
191
|
+
group: chant-apply-prod
|
|
192
|
+
cancel-in-progress: false
|
|
193
|
+
permissions:
|
|
194
|
+
contents: write
|
|
195
|
+
pull-requests: write
|
|
196
|
+
statuses: write
|
|
197
|
+
steps:
|
|
198
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
199
|
+
with:
|
|
200
|
+
fetch-depth: 0
|
|
201
|
+
- run: command -v tofu >/dev/null 2>&1 || (curl -fsSL https://get.opentofu.org/install-opentofu.sh -o /tmp/install-opentofu.sh && sh /tmp/install-opentofu.sh --install-method standalone)
|
|
202
|
+
- name: Restore the record of an earlier attempt at this commit
|
|
203
|
+
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
|
204
|
+
with:
|
|
205
|
+
path: .chant/pr-resume/pr-apply.json
|
|
206
|
+
key: chant-apply-prod-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
|
|
207
|
+
restore-keys: chant-apply-prod-${{ github.sha }}-
|
|
208
|
+
- name: Apply the newest green commit under the merged changes' approvals
|
|
209
|
+
env:
|
|
210
|
+
GITHUB_TOKEN: '${{ github.token }}'
|
|
211
|
+
CHANT_RUNNER_KEY: '${{ secrets.CHANT_RUNNER_KEY }}'
|
|
212
|
+
run: |
|
|
213
|
+
git fetch --quiet --tags origin "+refs/heads/main:refs/remotes/origin/main" && GREEN_SHA="$(npx --no-install plywood ci last-green)" && git checkout --quiet --detach "$GREEN_SHA"
|
|
214
|
+
npx --no-install plywood ci run apply --chant "npx --no-install plywood" --in-place --env prod --gate pr-apply --forge github
|
|
215
|
+
- name: Keep the record for a re-run
|
|
216
|
+
if: always()
|
|
217
|
+
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
|
218
|
+
with:
|
|
219
|
+
path: .chant/pr-resume/pr-apply.json
|
|
220
|
+
key: chant-apply-prod-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
|
|
221
|
+
- name: Keep the apply report
|
|
222
|
+
if: always()
|
|
223
|
+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
|
224
|
+
with:
|
|
225
|
+
name: chant-pr-apply
|
|
226
|
+
path: .chant/pr
|
|
227
|
+
if-no-files-found: ignore
|
|
228
|
+
include-hidden-files: true
|
|
@@ -82,6 +82,8 @@ import {
|
|
|
82
82
|
prLockCommand,
|
|
83
83
|
prCommentLockJobs,
|
|
84
84
|
prCommentPreviewJob,
|
|
85
|
+
prActionJob,
|
|
86
|
+
prLoopPassedEnv,
|
|
85
87
|
PREVIEW_REF_ENV,
|
|
86
88
|
PREVIEW_HEAD_OUTPUT,
|
|
87
89
|
prLoopWaveJobs,
|
|
@@ -336,7 +338,10 @@ function prLoopGithubDoc(
|
|
|
336
338
|
];
|
|
337
339
|
// #4168 step 9: under apply on merge the apply runs on a green signal, and a push to the branch records.
|
|
338
340
|
const forgejo = loop.forge === "forgejo";
|
|
339
|
-
|
|
341
|
+
// #4654: the action job answers its comment and a dispatch that names an action; such a dispatch starts no apply.
|
|
342
|
+
const action = prActionJob(loop.forge ?? "github", loop);
|
|
343
|
+
const plainDispatch = action ? `(github.event_name == 'workflow_dispatch' && github.event.inputs.${action.inputs.action} == '')` : "github.event_name == 'workflow_dispatch'";
|
|
344
|
+
const onGreen = forgejo ? "startsWith(github.ref, 'refs/tags/ci/green/')" : `github.event_name == 'workflow_run' || ${plainDispatch}`;
|
|
340
345
|
const onPush = loop.applyOnMerge && forgejo ? "github.event_name == 'push' && startsWith(github.ref, 'refs/heads/')" : "github.event_name == 'push'";
|
|
341
346
|
// #4301: a `/chant apply` comment on a pull request starts the apply before merge. GitHub also asks for a commenter who can write.
|
|
342
347
|
const onComment = [
|
|
@@ -566,6 +571,34 @@ function prLoopGithubDoc(
|
|
|
566
571
|
},
|
|
567
572
|
}
|
|
568
573
|
: {};
|
|
574
|
+
// #4654: `/<product> <action word> <member> <action>`, or a dispatch naming both, runs the product's action verb at the target branch's commit.
|
|
575
|
+
const actionJobs: Record<string, unknown> = action
|
|
576
|
+
? {
|
|
577
|
+
[action.jobName]: {
|
|
578
|
+
if: [
|
|
579
|
+
`(github.event_name == 'workflow_dispatch' && github.event.inputs.${action.inputs.action} != '')`,
|
|
580
|
+
`(${[
|
|
581
|
+
"github.event_name == 'issue_comment'",
|
|
582
|
+
`startsWith(github.event.comment.body, '${action.comment.replace(/'/g, "''")} ')`,
|
|
583
|
+
...(forgejo ? [] : ["contains(fromJSON('[\"OWNER\",\"MEMBER\",\"COLLABORATOR\"]'), github.event.comment.author_association)"]),
|
|
584
|
+
].join(" && ")})`,
|
|
585
|
+
].join(" || "),
|
|
586
|
+
"runs-on": GITHUB_RUNNER,
|
|
587
|
+
container: image,
|
|
588
|
+
// One action of a pipeline at a time, and never two runs of one request.
|
|
589
|
+
concurrency: concurrency(`chant-action-${env}${member ? `-${member.name}` : ""}`),
|
|
590
|
+
permissions: withIdToken({ contents: "write", issues: "write", "pull-requests": "write" }),
|
|
591
|
+
steps: [
|
|
592
|
+
...(identity ? [tradeStep(identity.apply, "apply")] : []),
|
|
593
|
+
// An issue_comment run checks out the target branch's commit, a dispatch the ref it names: never a pull request's head.
|
|
594
|
+
{ name: "Check out the target branch's commit", uses: CHECKOUT, with: { "fetch-depth": 0 } },
|
|
595
|
+
...runSteps(prLoopSetup(image, loop.engineSetup)),
|
|
596
|
+
...runSteps(beforeScript),
|
|
597
|
+
{ name: "Run the requested action under its gated request", env: { ...prLoopPassedEnv(forgeName, loop), ...action.env }, run: action.script },
|
|
598
|
+
],
|
|
599
|
+
},
|
|
600
|
+
}
|
|
601
|
+
: {};
|
|
569
602
|
const jobsDoc: Record<string, unknown> = {
|
|
570
603
|
plan: {
|
|
571
604
|
if: closes ? "github.event_name == 'pull_request' && github.event.action != 'closed'" : "github.event_name == 'pull_request'",
|
|
@@ -587,6 +620,7 @@ function prLoopGithubDoc(
|
|
|
587
620
|
...lockJobs,
|
|
588
621
|
...commentLockJobs,
|
|
589
622
|
...previewJobs,
|
|
623
|
+
...actionJobs,
|
|
590
624
|
...staleJobs,
|
|
591
625
|
...Object.fromEntries(applyJobs),
|
|
592
626
|
...Object.fromEntries(controlJobs),
|
|
@@ -604,7 +638,17 @@ function prLoopGithubDoc(
|
|
|
604
638
|
on.schedule = [{ cron: PR_ENV_SWEEP_CRON }];
|
|
605
639
|
}
|
|
606
640
|
// #4301: a /chant apply comment starts the apply before merge. #4588: and a lock or unlock comment its job.
|
|
607
|
-
if (loop.applyBeforeMerge || commentLocks.length > 0 || preview) on.issue_comment = { types: ["created"] };
|
|
641
|
+
if (loop.applyBeforeMerge || commentLocks.length > 0 || preview || action) on.issue_comment = { types: ["created"] };
|
|
642
|
+
// #4654: a dispatch names a member and an action; both empty, it is the dispatch it was before.
|
|
643
|
+
if (action) {
|
|
644
|
+
const input = (description: string) => ({ description, required: false, type: "string", default: "" });
|
|
645
|
+
on.workflow_dispatch = {
|
|
646
|
+
inputs: {
|
|
647
|
+
[action.inputs.member]: input(`The ${resolveProductWords(loop.words).noun[0]} the action runs on`),
|
|
648
|
+
[action.inputs.action]: input("The action to run, by its name"),
|
|
649
|
+
},
|
|
650
|
+
};
|
|
651
|
+
}
|
|
608
652
|
// #4625: each job on its stage's runner, the plan side's, the apply side's, or the default.
|
|
609
653
|
if (loop.runners) {
|
|
610
654
|
for (const [name, job] of Object.entries(jobsDoc) as Array<[string, Record<string, unknown>]>) {
|
|
@@ -618,7 +662,7 @@ function prLoopGithubDoc(
|
|
|
618
662
|
...(rooted ? {} : { defaults: { run: { "working-directory": member!.dir } } }),
|
|
619
663
|
env: { ...options.variables, CHANT_ENV: env },
|
|
620
664
|
jobsDoc,
|
|
621
|
-
stages: ["plan", PR_PLAN_NOTE_JOB, ...(locks ? [PR_LOCK_JOB, PR_UNLOCK_JOB] : []), ...commentLocks.map((j) => j.jobName), ...(preview ? [preview.jobName, preview.noteJobName] : []), ...(stale ? [PR_STALE_JOB] : []), ...applyNames, ...controlJobs.map(([name]) => name), ...(prEnv ? ["env-up", PR_ENV_POST_JOB, "env-down", "env-sweep"] : [])],
|
|
665
|
+
stages: ["plan", PR_PLAN_NOTE_JOB, ...(locks ? [PR_LOCK_JOB, PR_UNLOCK_JOB] : []), ...commentLocks.map((j) => j.jobName), ...(preview ? [preview.jobName, preview.noteJobName] : []), ...(action ? [action.jobName] : []), ...(stale ? [PR_STALE_JOB] : []), ...applyNames, ...controlJobs.map(([name]) => name), ...(prEnv ? ["env-up", PR_ENV_POST_JOB, "env-down", "env-sweep"] : [])],
|
|
622
666
|
jobs: [
|
|
623
667
|
{ jobName: "plan", component: "pull request plan", stage: "plan", needs: [] },
|
|
624
668
|
{ jobName: PR_PLAN_NOTE_JOB, component: "pull request plan note", stage: PR_PLAN_NOTE_JOB, needs: ["plan"] },
|
|
@@ -636,6 +680,7 @@ function prLoopGithubDoc(
|
|
|
636
680
|
{ jobName: preview.noteJobName, component: "pull request preview comment note", stage: preview.noteJobName, needs: [preview.jobName] },
|
|
637
681
|
]
|
|
638
682
|
: []),
|
|
683
|
+
...(action ? [{ jobName: action.jobName, component: "day-2 action", stage: action.jobName, needs: [] }] : []),
|
|
639
684
|
...applyNames.map((name, i) => ({
|
|
640
685
|
jobName: name,
|
|
641
686
|
component: name === "record" ? "merge record" : loop.applyBeforeMerge ? "apply before merge" : loop.waveGate ? `green apply, wave ${i}` : "green apply",
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The action job (#4654): with `actionJob`, the pull request loop's
|
|
3
|
+
* workflow answers `/<product> <action word> <member> <action>` and a
|
|
4
|
+
* dispatch with the member and action inputs, at the target branch's
|
|
5
|
+
* commit, through the product's action verb. A dispatch that names an
|
|
6
|
+
* action starts no apply.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, expect, test } from "vitest";
|
|
10
|
+
import { parseYAML } from "@intentius/chant/yaml";
|
|
11
|
+
import type { DriverComponent } from "@intentius/chant/components/driver";
|
|
12
|
+
import { generateGithubPipeline } from "./generate-pipeline";
|
|
13
|
+
|
|
14
|
+
const ESTATE: DriverComponent[] = [
|
|
15
|
+
{ name: "shop", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "pulumi-up", stack: "shop/prod-us" }] }] },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const WORDS = { product: "plywood", noun: ["stack", "stacks"] as [string, string], commands: { preview: "plan" as const, action: "action" as const }, bin: "npx --no-install plywood" };
|
|
19
|
+
|
|
20
|
+
interface Step { name?: string; uses?: string; run?: string; env?: Record<string, string>; with?: Record<string, unknown> }
|
|
21
|
+
interface Job { if?: string; needs?: string[]; permissions: Record<string, string>; steps: Step[] }
|
|
22
|
+
type Doc = { on: Record<string, unknown>; jobs: Record<string, Job> };
|
|
23
|
+
|
|
24
|
+
const gen = (prLoop: Record<string, unknown>) => generateGithubPipeline(ESTATE, { env: "prod", prLoop }).yaml;
|
|
25
|
+
|
|
26
|
+
describe("the GitHub workflow's action job", () => {
|
|
27
|
+
const yaml = gen({ words: WORDS, actionJob: true, applyOnMerge: true });
|
|
28
|
+
const doc = parseYAML(yaml) as Doc;
|
|
29
|
+
|
|
30
|
+
test("a dispatch takes the stack and action inputs, and a comment starts the job too", () => {
|
|
31
|
+
expect(doc.on.workflow_dispatch).toEqual({
|
|
32
|
+
inputs: {
|
|
33
|
+
stack: { description: "The stack the action runs on", required: false, type: "string", default: "" },
|
|
34
|
+
action: { description: "The action to run, by its name", required: false, type: "string", default: "" },
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
expect(doc.on.issue_comment).toEqual({ types: ["created"] });
|
|
38
|
+
const job = doc.jobs.action!;
|
|
39
|
+
expect(job.if).toContain("github.event_name == 'workflow_dispatch' && github.event.inputs.action != ''");
|
|
40
|
+
expect(job.if).toContain("startsWith(github.event.comment.body, '/plywood action ')");
|
|
41
|
+
expect(job.if).toContain("github.event.comment.author_association");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("the job runs the product's verb at the target branch's commit, with the comment and inputs as variables", () => {
|
|
45
|
+
const job = doc.jobs.action!;
|
|
46
|
+
expect(job.steps[0]!.uses).toMatch(/^actions\/checkout@/);
|
|
47
|
+
expect(job.steps[0]!.with).toEqual({ "fetch-depth": 0 });
|
|
48
|
+
const run = job.steps.at(-1)!;
|
|
49
|
+
expect(run.run).toContain("npx --no-install plywood action --from-event");
|
|
50
|
+
expect(run.env).toMatchObject({ CHANT_ACTION_COMMENT: "${{ github.event.comment.body }}", CHANT_ACTION_MEMBER: "${{ github.event.inputs.stack }}", CHANT_ACTION: "${{ github.event.inputs.action }}" });
|
|
51
|
+
expect(job.permissions).toEqual({ contents: "write", issues: "write", "pull-requests": "write" });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("a dispatch that names an action starts no apply", () => {
|
|
55
|
+
expect(doc.jobs.apply!.if).toContain("(github.event_name == 'workflow_dispatch' && github.event.inputs.action == '')");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("without actionJob nothing changes", () => {
|
|
59
|
+
const plain = parseYAML(gen({ words: { ...WORDS, commands: { preview: "plan" as const } }, applyOnMerge: true })) as Doc;
|
|
60
|
+
expect(plain.jobs.action).toBeUndefined();
|
|
61
|
+
expect(plain.on.workflow_dispatch).toEqual({});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("golden", async () => {
|
|
65
|
+
await expect(yaml).toMatchFileSnapshot("./__golden__/pr-loop-action.github.yml");
|
|
66
|
+
});
|
|
67
|
+
});
|