@catladder/cli 5.6.0 → 5.7.1

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.
Files changed (65) hide show
  1. package/dist/apps/cli/src/catci.js +22 -0
  2. package/dist/apps/cli/src/catci.js.map +1 -1
  3. package/dist/apps/cli/src/commands/index.d.ts +1 -0
  4. package/dist/apps/cli/src/commands/index.js +4 -1
  5. package/dist/apps/cli/src/commands/index.js.map +1 -1
  6. package/dist/apps/cli/src/commands/project/commandMrPin.d.ts +9 -0
  7. package/dist/apps/cli/src/commands/project/commandMrPin.js +11 -13
  8. package/dist/apps/cli/src/commands/project/commandMrPin.js.map +1 -1
  9. package/dist/apps/cli/src/commands/project/commandMrReviewApp.d.ts +21 -0
  10. package/dist/apps/cli/src/commands/project/commandMrReviewApp.js +102 -0
  11. package/dist/apps/cli/src/commands/project/commandMrReviewApp.js.map +1 -0
  12. package/dist/apps/cli/src/reviewApps/github.d.ts +10 -0
  13. package/dist/apps/cli/src/reviewApps/github.js +240 -0
  14. package/dist/apps/cli/src/reviewApps/github.js.map +1 -0
  15. package/dist/bundles/catci/index.js +5 -5
  16. package/dist/bundles/catenv/index.js +897 -25
  17. package/dist/bundles/cli/index.js +1021 -39
  18. package/dist/bundles/skills/catladder-cli/references/commands.md +13 -0
  19. package/dist/bundles/skills/catladder-config/SKILL.md +4 -0
  20. package/dist/bundles/skills/catladder-deploys/SKILL.md +6 -0
  21. package/dist/bundles/skills/catladder-pipelines/SKILL.md +85 -1
  22. package/dist/packages/pipeline/src/backends/github/GithubBackend.js +60 -3
  23. package/dist/packages/pipeline/src/backends/github/GithubBackend.js.map +1 -1
  24. package/dist/packages/pipeline/src/backends/github/aggregateCheckJob.d.ts +1 -1
  25. package/dist/packages/pipeline/src/backends/github/aggregateCheckJob.js +22 -8
  26. package/dist/packages/pipeline/src/backends/github/aggregateCheckJob.js.map +1 -1
  27. package/dist/packages/pipeline/src/backends/github/reviewDeployWorkflow.d.ts +128 -0
  28. package/dist/packages/pipeline/src/backends/github/reviewDeployWorkflow.js +510 -0
  29. package/dist/packages/pipeline/src/backends/github/reviewDeployWorkflow.js.map +1 -0
  30. package/dist/packages/pipeline/src/backends/gitlab/GitlabBackend.js +29 -4
  31. package/dist/packages/pipeline/src/backends/gitlab/GitlabBackend.js.map +1 -1
  32. package/dist/packages/pipeline/src/backends/gitlab/createGitlabPipeline.d.ts +11 -1
  33. package/dist/packages/pipeline/src/backends/gitlab/createGitlabPipeline.js +17 -5
  34. package/dist/packages/pipeline/src/backends/gitlab/createGitlabPipeline.js.map +1 -1
  35. package/dist/packages/pipeline/src/backends/gitlab/reviewDeployGate.d.ts +26 -0
  36. package/dist/packages/pipeline/src/backends/gitlab/reviewDeployGate.js +94 -0
  37. package/dist/packages/pipeline/src/backends/gitlab/reviewDeployGate.js.map +1 -0
  38. package/dist/packages/pipeline/src/deploy/base/deploy.js +10 -3
  39. package/dist/packages/pipeline/src/deploy/base/deploy.js.map +1 -1
  40. package/dist/packages/pipeline/src/index.d.ts +1 -0
  41. package/dist/packages/pipeline/src/index.js +1 -0
  42. package/dist/packages/pipeline/src/index.js.map +1 -1
  43. package/dist/packages/pipeline/src/pipeline/resolveRequirements.d.ts +2 -0
  44. package/dist/packages/pipeline/src/pipeline/resolveRequirements.js +3 -2
  45. package/dist/packages/pipeline/src/pipeline/resolveRequirements.js.map +1 -1
  46. package/dist/packages/pipeline/src/reviewApps.d.ts +38 -0
  47. package/dist/packages/pipeline/src/reviewApps.js +86 -0
  48. package/dist/packages/pipeline/src/reviewApps.js.map +1 -0
  49. package/dist/packages/pipeline/src/reviewAppsPolicy.d.ts +46 -0
  50. package/dist/packages/pipeline/src/reviewAppsPolicy.js +37 -0
  51. package/dist/packages/pipeline/src/reviewAppsPolicy.js.map +1 -0
  52. package/dist/packages/pipeline/src/types/config.d.ts +55 -0
  53. package/dist/packages/pipeline/src/types/config.js.map +1 -1
  54. package/dist/skills/catladder-cli/references/commands.md +13 -0
  55. package/dist/skills/catladder-config/SKILL.md +4 -0
  56. package/dist/skills/catladder-deploys/SKILL.md +6 -0
  57. package/dist/skills/catladder-pipelines/SKILL.md +85 -1
  58. package/dist/tsconfig.tsbuildinfo +1 -1
  59. package/package.json +1 -1
  60. package/src/catci.ts +29 -0
  61. package/src/commands/index.ts +4 -0
  62. package/src/commands/project/commandMrPin.ts +15 -14
  63. package/src/commands/project/commandMrReviewApp.ts +143 -0
  64. package/src/reviewApps/__tests__/github.test.ts +118 -0
  65. package/src/reviewApps/github.ts +307 -0
@@ -2905,7 +2905,7 @@ exports.commandKubeStopPortforward = (0, defineCommand_1.defineCommand)({
2905
2905
  "use strict";
2906
2906
 
2907
2907
  Object.defineProperty(exports, "__esModule", ({ value: true }));
2908
- exports.commandStarwars = exports.commandDadjoke = exports.commandSecurityAuditCreate = exports.commandSecurityAuditEvaluate = exports.commandSecurityAuditCiJob = exports.commandRestoreDb = exports.commandMongoDestroyMember = exports.commandMongoPortForward = exports.commandMongoGetShell = exports.commandTriggerCronjobProject = exports.commandDeleteProject = exports.commandPauseProject = exports.commandDeletePods = exports.commandGetShell = exports.commandListPods = exports.commandNamespace = exports.commandMrUnpin = exports.commandMrPin = exports.commandSecurityEvaluate = exports.commandProjectRestoreDb = exports.commandCiJobLog = exports.commandCiJobOpen = exports.commandDoctor = exports.commandSetup = exports.commandGetMyTotalWorktime = exports.commandRenewToken = exports.commandTriggerCronjobGeneral = exports.commandOpenLogs = exports.commandSecretsSyncGithub = exports.commandSecretsClearBackups = exports.commandSecretsList = exports.commandSecretsSet = exports.commandSecretsPush = exports.commandSecretsPull = exports.commandConfigSecrets = exports.commandCloudSqlProxy = exports.commandOpenGit = exports.commandOpenEnv = exports.commandPortForward = exports.commandEnvVars = exports.commandKubePortForward = exports.commandKubeGetShell = exports.commandKubeStopPortforward = exports.commandKubeListPods = exports.commandKubeListSecrets = exports.commandKubeListNamespaces = exports.commandKubeCurrentContext = void 0;
2908
+ exports.commandStarwars = exports.commandDadjoke = exports.commandSecurityAuditCreate = exports.commandSecurityAuditEvaluate = exports.commandSecurityAuditCiJob = exports.commandRestoreDb = exports.commandMongoDestroyMember = exports.commandMongoPortForward = exports.commandMongoGetShell = exports.commandTriggerCronjobProject = exports.commandDeleteProject = exports.commandPauseProject = exports.commandDeletePods = exports.commandGetShell = exports.commandListPods = exports.commandNamespace = exports.commandMrReviewAppOff = exports.commandMrReviewAppOn = exports.commandMrUnpin = exports.commandMrPin = exports.commandSecurityEvaluate = exports.commandProjectRestoreDb = exports.commandCiJobLog = exports.commandCiJobOpen = exports.commandDoctor = exports.commandSetup = exports.commandGetMyTotalWorktime = exports.commandRenewToken = exports.commandTriggerCronjobGeneral = exports.commandOpenLogs = exports.commandSecretsSyncGithub = exports.commandSecretsClearBackups = exports.commandSecretsList = exports.commandSecretsSet = exports.commandSecretsPush = exports.commandSecretsPull = exports.commandConfigSecrets = exports.commandCloudSqlProxy = exports.commandOpenGit = exports.commandOpenEnv = exports.commandPortForward = exports.commandEnvVars = exports.commandKubePortForward = exports.commandKubeGetShell = exports.commandKubeStopPortforward = exports.commandKubeListPods = exports.commandKubeListSecrets = exports.commandKubeListNamespaces = exports.commandKubeCurrentContext = void 0;
2909
2909
  // General commands
2910
2910
  var commandKubeCurrentContext_1 = __nccwpck_require__(78034);
2911
2911
  Object.defineProperty(exports, "commandKubeCurrentContext", ({ enumerable: true, get: function () { return commandKubeCurrentContext_1.commandKubeCurrentContext; } }));
@@ -2968,6 +2968,9 @@ Object.defineProperty(exports, "commandSecurityEvaluate", ({ enumerable: true, g
2968
2968
  var commandMrPin_1 = __nccwpck_require__(31318);
2969
2969
  Object.defineProperty(exports, "commandMrPin", ({ enumerable: true, get: function () { return commandMrPin_1.commandMrPin; } }));
2970
2970
  Object.defineProperty(exports, "commandMrUnpin", ({ enumerable: true, get: function () { return commandMrPin_1.commandMrUnpin; } }));
2971
+ var commandMrReviewApp_1 = __nccwpck_require__(94390);
2972
+ Object.defineProperty(exports, "commandMrReviewAppOn", ({ enumerable: true, get: function () { return commandMrReviewApp_1.commandMrReviewAppOn; } }));
2973
+ Object.defineProperty(exports, "commandMrReviewAppOff", ({ enumerable: true, get: function () { return commandMrReviewApp_1.commandMrReviewAppOff; } }));
2971
2974
  // Project k8s commands
2972
2975
  var commandNamespace_1 = __nccwpck_require__(25918);
2973
2976
  Object.defineProperty(exports, "commandNamespace", ({ enumerable: true, get: function () { return commandNamespace_1.commandNamespace; } }));
@@ -3638,7 +3641,7 @@ exports.commandCiJobLog = (0, defineCommand_1.defineCommand)({
3638
3641
  "use strict";
3639
3642
 
3640
3643
  Object.defineProperty(exports, "__esModule", ({ value: true }));
3641
- exports.commandMrUnpin = exports.commandMrPin = void 0;
3644
+ exports.commandMrUnpin = exports.commandMrPin = exports.ensureLabelExists = exports.findMr = void 0;
3642
3645
  const child_process_promise_1 = __nccwpck_require__(11542);
3643
3646
  const pipeline_1 = __nccwpck_require__(53695);
3644
3647
  const getProjectConfig_1 = __nccwpck_require__(70478);
@@ -3674,18 +3677,16 @@ const findMr = async (io, projectId, mrIid) => {
3674
3677
  }
3675
3678
  return mrs[0];
3676
3679
  };
3677
- const ensurePinLabelExists = async (io, projectId, pinLabel) => {
3678
- const existing = await (0, gitlab_1.doGitlabRequest)(io, `projects/${projectId}/labels?search=${encodeURIComponent(pinLabel)}`);
3679
- if (existing === null || existing === void 0 ? void 0 : existing.some((label) => label.name === pinLabel)) {
3680
+ exports.findMr = findMr;
3681
+ const ensureLabelExists = async (io, projectId, name, description) => {
3682
+ const existing = await (0, gitlab_1.doGitlabRequest)(io, `projects/${projectId}/labels?search=${encodeURIComponent(name)}`);
3683
+ if (existing === null || existing === void 0 ? void 0 : existing.some((label) => label.name === name)) {
3680
3684
  return;
3681
3685
  }
3682
- await (0, gitlab_1.doGitlabRequest)(io, `projects/${projectId}/labels`, {
3683
- name: pinLabel,
3684
- color: PIN_LABEL_COLOR,
3685
- description: "review apps of this MR are pinned — deploys use auto_stop_in: never (managed by catladder)",
3686
- }, "POST");
3687
- io.log(`created the '${pinLabel}' label in the project`);
3686
+ await (0, gitlab_1.doGitlabRequest)(io, `projects/${projectId}/labels`, { name, color: PIN_LABEL_COLOR, description }, "POST");
3687
+ io.log(`created the '${name}' label in the project`);
3688
3688
  };
3689
+ exports.ensureLabelExists = ensureLabelExists;
3689
3690
  exports.commandMrPin = (0, defineCommand_1.defineCommand)({
3690
3691
  name: "mr pin",
3691
3692
  description: "pin the review apps of a merge request (disable their auto-stop) by adding the pin label; merging or closing the MR still stops them",
@@ -3707,8 +3708,8 @@ exports.commandMrPin = (0, defineCommand_1.defineCommand)({
3707
3708
  var _a, _b;
3708
3709
  const pinLabel = await getPinLabel();
3709
3710
  const { id: projectId } = await (0, gitlab_1.getProjectInfo)(ctx);
3710
- const mr = await findMr(ctx, projectId, await ctx.get("mr"));
3711
- await ensurePinLabelExists(ctx, projectId, pinLabel);
3711
+ const mr = await (0, exports.findMr)(ctx, projectId, await ctx.get("mr"));
3712
+ await (0, exports.ensureLabelExists)(ctx, projectId, pinLabel, "review apps of this MR are pinned — deploys use auto_stop_in: never (managed by catladder)");
3712
3713
  if ((_a = mr.labels) === null || _a === void 0 ? void 0 : _a.includes(pinLabel)) {
3713
3714
  ctx.log(`already pinned: mr!${mr.iid} carries the '${pinLabel}' label`);
3714
3715
  }
@@ -3744,7 +3745,7 @@ exports.commandMrUnpin = (0, defineCommand_1.defineCommand)({
3744
3745
  var _a;
3745
3746
  const pinLabel = await getPinLabel();
3746
3747
  const { id: projectId } = await (0, gitlab_1.getProjectInfo)(ctx);
3747
- const mr = await findMr(ctx, projectId, await ctx.get("mr"));
3748
+ const mr = await (0, exports.findMr)(ctx, projectId, await ctx.get("mr"));
3748
3749
  if (!((_a = mr.labels) === null || _a === void 0 ? void 0 : _a.includes(pinLabel))) {
3749
3750
  ctx.log(`mr!${mr.iid} is not pinned (no '${pinLabel}' label)`);
3750
3751
  return;
@@ -3758,6 +3759,115 @@ exports.commandMrUnpin = (0, defineCommand_1.defineCommand)({
3758
3759
 
3759
3760
  /***/ }),
3760
3761
 
3762
+ /***/ 94390:
3763
+ /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
3764
+
3765
+ "use strict";
3766
+
3767
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
3768
+ exports.commandMrReviewAppOff = exports.commandMrReviewAppOn = void 0;
3769
+ const pipeline_1 = __nccwpck_require__(53695);
3770
+ const getProjectConfig_1 = __nccwpck_require__(70478);
3771
+ const defineCommand_1 = __nccwpck_require__(19096);
3772
+ const gitlab_1 = __nccwpck_require__(17697);
3773
+ const commandMrPin_1 = __nccwpck_require__(31318);
3774
+ /**
3775
+ * the label switch of the project's review apps — errors when the
3776
+ * project has none (github, or a policy without labels)
3777
+ */
3778
+ const getSwitch = async () => {
3779
+ const config = await (0, getProjectConfig_1.getProjectConfig)();
3780
+ if (!config) {
3781
+ throw new Error("no catladder config found");
3782
+ }
3783
+ const reviewApps = (0, pipeline_1.getReviewAppsConfig)(config);
3784
+ const switchLabel = (0, pipeline_1.getReviewAppsSwitchLabel)(reviewApps);
3785
+ if (!switchLabel) {
3786
+ throw new Error(reviewApps.deploy === "manual"
3787
+ ? "review apps deploy on request in this project (reviewApps.deploy: manual) — play the '🚀 deploy review' job of the MR pipeline"
3788
+ : "review apps of this project are not switched by label (set reviewApps.deploy to optIn or optOut)");
3789
+ }
3790
+ if (!(0, pipeline_1.getEnabledPipelineTypes)(config).includes("gitlab")) {
3791
+ throw new Error(`this command switches gitlab merge requests — on github, set the label on the pull request instead (gh pr edit --${switchLabel.deployOn === "labeled" ? "add" : "remove"}-label ${switchLabel.label}); the label change deploys by itself`);
3792
+ }
3793
+ return switchLabel;
3794
+ };
3795
+ const mrInput = {
3796
+ mr: {
3797
+ type: "number",
3798
+ message: "merge request IID (defaults to the open MR of the current branch)",
3799
+ positional: true,
3800
+ required: false,
3801
+ },
3802
+ };
3803
+ exports.commandMrReviewAppOn = (0, defineCommand_1.defineCommand)({
3804
+ name: "mr review-app-on",
3805
+ description: "deploy the review apps of a merge request: switches the reviewApps label on (opt-in: adds it, opt-out: removes the skip label) and triggers a pipeline, since gitlab doesn't start one for label changes",
3806
+ group: "mr",
3807
+ inputs: {
3808
+ ...mrInput,
3809
+ pipeline: {
3810
+ type: "boolean",
3811
+ message: "trigger a merge-request pipeline so the review apps deploy now",
3812
+ required: false,
3813
+ },
3814
+ },
3815
+ execute: async (ctx) => {
3816
+ var _a, _b;
3817
+ const switchLabel = await getSwitch();
3818
+ const { id: projectId } = await (0, gitlab_1.getProjectInfo)(ctx);
3819
+ const mr = await (0, commandMrPin_1.findMr)(ctx, projectId, await ctx.get("mr"));
3820
+ if (switchLabel.deployOn === "labeled") {
3821
+ await (0, commandMrPin_1.ensureLabelExists)(ctx, projectId, switchLabel.label, "review apps of this MR deploy (managed by catladder)");
3822
+ }
3823
+ const hasLabel = (_a = mr.labels) === null || _a === void 0 ? void 0 : _a.includes(switchLabel.label);
3824
+ if (hasLabel === (switchLabel.deployOn === "labeled")) {
3825
+ ctx.log(`review apps of mr!${mr.iid} are already switched on`);
3826
+ }
3827
+ else {
3828
+ await (0, gitlab_1.doGitlabRequest)(ctx, `projects/${projectId}/merge_requests/${mr.iid}`, switchLabel.deployOn === "labeled"
3829
+ ? { add_labels: switchLabel.label }
3830
+ : { remove_labels: switchLabel.label }, "PUT");
3831
+ ctx.log(`review apps of mr!${mr.iid} "${mr.title}" switched on — label '${switchLabel.label}' ${switchLabel.deployOn === "labeled" ? "added" : "removed"}`);
3832
+ }
3833
+ if ((_b = (await ctx.get("pipeline"))) !== null && _b !== void 0 ? _b : true) {
3834
+ const pipeline = await (0, gitlab_1.doGitlabRequest)(ctx, `projects/${projectId}/merge_requests/${mr.iid}/pipelines`, {}, "POST");
3835
+ ctx.log(`🚀 triggered a pipeline that deploys them: ${pipeline.web_url}`);
3836
+ }
3837
+ else {
3838
+ ctx.log("the review apps deploy with the next pipeline of the MR");
3839
+ }
3840
+ },
3841
+ });
3842
+ exports.commandMrReviewAppOff = (0, defineCommand_1.defineCommand)({
3843
+ name: "mr review-app-off",
3844
+ description: "stop deploying the review apps of a merge request: switches the reviewApps label off (opt-in: removes it, opt-out: adds the skip label). Running review apps stay until the MR closes or they auto-stop",
3845
+ group: "mr",
3846
+ inputs: mrInput,
3847
+ execute: async (ctx) => {
3848
+ var _a;
3849
+ const switchLabel = await getSwitch();
3850
+ const { id: projectId } = await (0, gitlab_1.getProjectInfo)(ctx);
3851
+ const mr = await (0, commandMrPin_1.findMr)(ctx, projectId, await ctx.get("mr"));
3852
+ if (switchLabel.stopOn === "labeled") {
3853
+ await (0, commandMrPin_1.ensureLabelExists)(ctx, projectId, switchLabel.label, "review apps of this MR don't deploy (managed by catladder)");
3854
+ }
3855
+ const hasLabel = (_a = mr.labels) === null || _a === void 0 ? void 0 : _a.includes(switchLabel.label);
3856
+ if (hasLabel === (switchLabel.stopOn === "labeled")) {
3857
+ ctx.log(`review apps of mr!${mr.iid} are already switched off`);
3858
+ return;
3859
+ }
3860
+ await (0, gitlab_1.doGitlabRequest)(ctx, `projects/${projectId}/merge_requests/${mr.iid}`, switchLabel.stopOn === "labeled"
3861
+ ? { add_labels: switchLabel.label }
3862
+ : { remove_labels: switchLabel.label }, "PUT");
3863
+ ctx.log(`review apps of mr!${mr.iid} switched off — label '${switchLabel.label}' ${switchLabel.stopOn === "labeled" ? "added" : "removed"}`);
3864
+ ctx.log("already running review apps stay until the MR is closed or they auto-stop — stop them earlier with the stop job of the MR pipeline");
3865
+ },
3866
+ });
3867
+ //# sourceMappingURL=commandMrReviewApp.js.map
3868
+
3869
+ /***/ }),
3870
+
3761
3871
  /***/ 30119:
3762
3872
  /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
3763
3873
 
@@ -9232,6 +9342,8 @@ const aggregateCheckJob_1 = __nccwpck_require__(82145);
9232
9342
  const shippedCatci_1 = __nccwpck_require__(27967);
9233
9343
  const githubReleaseJobs_1 = __nccwpck_require__(25073);
9234
9344
  const scriptFiles_1 = __nccwpck_require__(61071);
9345
+ const reviewApps_1 = __nccwpck_require__(67071);
9346
+ const reviewDeployWorkflow_1 = __nccwpck_require__(73671);
9235
9347
  const WORKFLOWS_FOLDER = ".github/workflows";
9236
9348
  /**
9237
9349
  * generated workflow files are prefixed so that cleanup never touches
@@ -9327,6 +9439,7 @@ class GithubBackend {
9327
9439
  ...(0, ciVariables_1.getGithubInjectedWorkflowEnv)(images.registryImageRef()),
9328
9440
  ...(0, index_1.getPipelineOptions)(config, this.type).runnerVariables,
9329
9441
  };
9442
+ const reviewApps = (0, reviewApps_1.getReviewAppsConfig)(config);
9330
9443
  const reviewStopJobs = {};
9331
9444
  // jobs routed to the per-kind workflow_dispatch workflows (deploy /
9332
9445
  // stop / rollback). A manual job and its build closure live there
@@ -9369,6 +9482,39 @@ class GithubBackend {
9369
9482
  triggerJobs.delete(id);
9370
9483
  }
9371
9484
  }
9485
+ // review delivery chains behind the per-PR deploy switch move to
9486
+ // their own workflow (reviewApps config, or manual review deploys)
9487
+ // When the label/draft policy already decides at the start of the
9488
+ // review run, the chains stay in it behind that condition (one
9489
+ // run, one app build); the review deploy workflow handles whatever
9490
+ // changes later. Manual review deploys only live there.
9491
+ let inRunDeliveryIds = [];
9492
+ const inRunCondition = (0, reviewApps_1.isReviewDeployGated)(reviewApps)
9493
+ ? (0, reviewDeployWorkflow_1.getInRunDeployCondition)(reviewApps)
9494
+ : undefined;
9495
+ if (trigger === "mr") {
9496
+ const deliveryIds = (0, reviewDeployWorkflow_1.getGatedReviewDeliveryJobIds)(triggerJobs, reviewApps);
9497
+ if (deliveryIds.length > 0) {
9498
+ workflows[reviewDeployWorkflow_1.REVIEW_DEPLOY_WORKFLOW_FILE] = (0, reviewDeployWorkflow_1.makeReviewDeployWorkflow)({
9499
+ deliveryIds,
9500
+ triggerJobs,
9501
+ reviewApps,
9502
+ images,
9503
+ workflowEnv,
9504
+ permissions: workflowPermissions(images).permissions,
9505
+ });
9506
+ if (inRunCondition) {
9507
+ inRunDeliveryIds = deliveryIds;
9508
+ for (const id of deliveryIds) {
9509
+ const entry = triggerJobs.get(id);
9510
+ entry.githubJob = (0, reviewDeployWorkflow_1.withInRunCondition)(entry.githubJob, inRunCondition);
9511
+ }
9512
+ }
9513
+ else {
9514
+ deliveryIds.forEach((id) => triggerJobs.delete(id));
9515
+ }
9516
+ }
9517
+ }
9372
9518
  // leaf manual jobs: an explicit manual gate (e.g. a prod deploy) or
9373
9519
  // a stop/rollback task with no automatic trigger. Each becomes a
9374
9520
  // dispatch choice.
@@ -9472,7 +9618,7 @@ class GithubBackend {
9472
9618
  }
9473
9619
  if (Object.keys(jobs).length > 0) {
9474
9620
  const workflowJobs = { ...(0, ensureImageJobs_1.makeEnsureImageGithubJobs)(images), ...jobs };
9475
- workflows[`${GENERATED_FILE_PREFIX}${workflowFileName(trigger)}`] = {
9621
+ const workflow = {
9476
9622
  ...getTriggerWorkflow(trigger),
9477
9623
  ...workflowPermissions(images),
9478
9624
  env: workflowEnv,
@@ -9484,13 +9630,30 @@ class GithubBackend {
9484
9630
  // required. Added at finalization so its needs cover the
9485
9631
  // image-build jobs too.
9486
9632
  jobs: trigger === "mr"
9487
- ? { ...workflowJobs, ...(0, aggregateCheckJob_1.makeAggregateCheckJob)(workflowJobs) }
9633
+ ? {
9634
+ ...workflowJobs,
9635
+ ...(0, aggregateCheckJob_1.makeAggregateCheckJob)(workflowJobs, inRunDeliveryIds),
9636
+ // the stable review app result of an in-run deploy
9637
+ ...(inRunCondition && inRunDeliveryIds.length > 0
9638
+ ? (0, reviewDeployWorkflow_1.makeReviewAppAggregateJob)(inRunDeliveryIds, inRunCondition)
9639
+ : {}),
9640
+ // once green, hand over to the review deploy workflow
9641
+ // when the PR deploys but this run didn't
9642
+ ...(workflows[reviewDeployWorkflow_1.REVIEW_DEPLOY_WORKFLOW_FILE] &&
9643
+ (0, reviewApps_1.isReviewDeployGated)(reviewApps)
9644
+ ? (0, reviewDeployWorkflow_1.makeReviewDeployTriggerJob)(reviewApps, inRunCondition)
9645
+ : {}),
9646
+ }
9488
9647
  : workflowJobs,
9489
9648
  };
9649
+ workflows[`${GENERATED_FILE_PREFIX}${workflowFileName(trigger)}`] =
9650
+ trigger === "mr" && reviewApps.drafts === "skip"
9651
+ ? (0, reviewDeployWorkflow_1.withDraftsSkipped)(workflow)
9652
+ : workflow;
9490
9653
  }
9491
9654
  }
9492
9655
  if (Object.keys(reviewStopJobs).length > 0) {
9493
- workflows[`${GENERATED_FILE_PREFIX}review-stop.yml`] = {
9656
+ const reviewStopWorkflow = {
9494
9657
  name: "🛑 catladder stop review app",
9495
9658
  on: {
9496
9659
  pull_request: { types: ["closed"] },
@@ -9517,6 +9680,10 @@ class GithubBackend {
9517
9680
  },
9518
9681
  jobs: { ...(0, ensureImageJobs_1.makeEnsureImageGithubJobs)(images), ...reviewStopJobs },
9519
9682
  };
9683
+ workflows[`${GENERATED_FILE_PREFIX}review-stop.yml`] = workflows[reviewDeployWorkflow_1.REVIEW_DEPLOY_WORKFLOW_FILE]
9684
+ ? (0, reviewDeployWorkflow_1.withReviewStopSwitch)(reviewStopWorkflow, reviewApps, (0, reviewApps_1.isReviewDeployGated)(reviewApps) &&
9685
+ (0, reviewDeployWorkflow_1.getInRunDeployCondition)(reviewApps) !== undefined)
9686
+ : reviewStopWorkflow;
9520
9687
  }
9521
9688
  for (const [kind, bucket] of Object.entries(manualKindBuckets)) {
9522
9689
  if (Object.keys(bucket.jobs).length === 0) {
@@ -9651,7 +9818,12 @@ exports.AGGREGATE_CHECK_JOB_NAME = "catladder ✅";
9651
9818
  * `success` to `needs` even when red, so they stay non-blocking here
9652
9819
  * by the same mechanism that keeps their workflow green.
9653
9820
  */
9654
- const makeAggregateCheckJob = (reviewJobs) => ({
9821
+ const makeAggregateCheckJob = (reviewJobs,
9822
+ /**
9823
+ * jobs that may be skipped on purpose (review delivery chains behind
9824
+ * the reviewApps switch) — their skip is no failure, their failure is
9825
+ */
9826
+ skippable = []) => ({
9655
9827
  [exports.AGGREGATE_CHECK_JOB_ID]: {
9656
9828
  name: exports.AGGREGATE_CHECK_JOB_NAME,
9657
9829
  "runs-on": "ubuntu-latest",
@@ -9662,13 +9834,22 @@ const makeAggregateCheckJob = (reviewJobs) => ({
9662
9834
  name: exports.AGGREGATE_CHECK_JOB_NAME,
9663
9835
  // the needs context only interpolates inside the workflow file,
9664
9836
  // so this script stays inline
9665
- run: [
9666
- `results='\${{ toJSON(needs) }}'`,
9667
- `echo "$results"`,
9668
- `if echo "$results" | grep -qE '"result": "(failure|cancelled|skipped)"'; then`,
9669
- ` echo "a required job did not succeed"; exit 1`,
9670
- `fi`,
9671
- ].join("\n"),
9837
+ run: (skippable.length === 0
9838
+ ? [
9839
+ `results='\${{ toJSON(needs) }}'`,
9840
+ `echo "$results"`,
9841
+ `if echo "$results" | grep -qE '"result": "(failure|cancelled|skipped)"'; then`,
9842
+ ` echo "a required job did not succeed"; exit 1`,
9843
+ `fi`,
9844
+ ]
9845
+ : [
9846
+ `results='\${{ toJSON(needs) }}'`,
9847
+ `echo "$results"`,
9848
+ `failed=$(echo "$results" | jq -r --arg skippable ' ${[...skippable].sort().join(" ")} ' 'to_entries[] | .key as $job | select(.value.result == "failure" or .value.result == "cancelled" or (.value.result == "skipped" and ($skippable | contains(" " + $job + " ") | not))) | $job')`,
9849
+ `if [ -n "$failed" ]; then`,
9850
+ ` echo "required jobs did not succeed: $failed"; exit 1`,
9851
+ `fi`,
9852
+ ]).join("\n"),
9672
9853
  shell: "bash",
9673
9854
  },
9674
9855
  ],
@@ -10713,6 +10894,523 @@ exports.resolveGithubRegistryImage = resolveGithubRegistryImage;
10713
10894
 
10714
10895
  /***/ }),
10715
10896
 
10897
+ /***/ 73671:
10898
+ /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
10899
+
10900
+ "use strict";
10901
+
10902
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
10903
+ exports.withDraftsSkipped = exports.withReviewStopSwitch = exports.makeReviewDeployTriggerJob = exports.makeReviewAppAggregateJob = exports.REVIEW_APP_STATUS_CONTEXT = exports.REVIEW_APP_AGGREGATE_JOB_NAME = exports.REVIEW_APP_AGGREGATE_JOB_ID = exports.makeReviewDeployWorkflow = exports.getGatedReviewDeliveryJobIds = exports.withInRunCondition = exports.getInRunDeployCondition = exports.REVIEW_DEPLOY_WORKFLOW_FILE = void 0;
10904
+ const shippedCatci_1 = __nccwpck_require__(27967);
10905
+ const resolveRequirements_1 = __nccwpck_require__(81710);
10906
+ const reviewApps_1 = __nccwpck_require__(67071);
10907
+ const aggregateCheckJob_1 = __nccwpck_require__(82145);
10908
+ const ensureImageJobs_1 = __nccwpck_require__(77590);
10909
+ const CATCI = `${shippedCatci_1.GENERATED_CATCI_FOLDER}/index.js`;
10910
+ /**
10911
+ * the generated review deploy workflow. Keep in sync with
10912
+ * GITHUB_REVIEW_DEPLOY_WORKFLOW in apps/cli/src/reviewApps/github.ts.
10913
+ */
10914
+ exports.REVIEW_DEPLOY_WORKFLOW_FILE = "catladder-deploy-review.yml";
10915
+ const GUARD_JOB_ID = "catladder-review-deploy-guard";
10916
+ const TRIGGER_JOB_ID = "catladder-review-deploy-trigger";
10917
+ /** the PR of the run — from the event, or the dispatch input */
10918
+ const PR_NUMBER_EXPRESSION = "github.event.number || inputs.pr";
10919
+ /** quote a value for a github expression string literal */
10920
+ const literal = (value) => `'${value.replaceAll("'", "''")}'`;
10921
+ /** combine a job's existing `if` with another condition */
10922
+ const andIf = (existing, condition) => {
10923
+ if (!existing) {
10924
+ return `\${{ ${condition} }}`;
10925
+ }
10926
+ const unwrapped = existing.replace(/^\$\{\{\s*([\s\S]*?)\s*\}\}$/, "$1");
10927
+ return `\${{ (${unwrapped}) && (${condition}) }}`;
10928
+ };
10929
+ /**
10930
+ * the condition under which the review workflow deploys the review apps
10931
+ * ITSELF, decided from the pull request event at the start of the run
10932
+ * (labels, draft) — the common case then needs one run and one app
10933
+ * build, like gitlab's switch job. Whatever changes later (a label set
10934
+ * during or after the run, a draft marked ready, an explicit request) is
10935
+ * handled by the review deploy workflow. `deploy: "manual"` never
10936
+ * deploys on its own: undefined.
10937
+ */
10938
+ const getInRunDeployCondition = (reviewApps) => {
10939
+ const labels = "github.event.pull_request.labels.*.name";
10940
+ const policy = reviewApps.deploy === "optIn"
10941
+ ? `contains(${labels}, ${literal(reviewApps.label)})`
10942
+ : reviewApps.deploy === "optOut"
10943
+ ? `!contains(${labels}, ${literal(reviewApps.skipLabel)})`
10944
+ : reviewApps.deploy === "auto"
10945
+ ? undefined
10946
+ : null;
10947
+ if (policy === null) {
10948
+ return undefined;
10949
+ }
10950
+ const notDraft = reviewApps.drafts !== "likeReady"
10951
+ ? "!github.event.pull_request.draft"
10952
+ : undefined;
10953
+ return [policy, notDraft].filter(Boolean).join(" && ") || "true";
10954
+ };
10955
+ exports.getInRunDeployCondition = getInRunDeployCondition;
10956
+ /** combine a job's `if` with the in-run deploy condition */
10957
+ const withInRunCondition = (job, condition) => ({
10958
+ ...job,
10959
+ if: andIf(job.if, condition),
10960
+ });
10961
+ exports.withInRunCondition = withInRunCondition;
10962
+ /**
10963
+ * the review delivery jobs (docker → deploy → verify) that move out of
10964
+ * the review workflow into the review deploy workflow:
10965
+ * - all of them when the reviewApps config gates review deploys
10966
+ * - otherwise those of components whose review deploy is manual
10967
+ * (`deploy.when: "manual"`) — github has no manual jobs inside a run,
10968
+ * and the generic manual-task dispatch can't carry the pull request
10969
+ */
10970
+ const getGatedReviewDeliveryJobIds = (triggerJobs, reviewApps) => {
10971
+ var _a;
10972
+ const gatedByConfig = (0, reviewApps_1.isReviewDeployGated)(reviewApps);
10973
+ const manualComponents = new Set([...triggerJobs.values()]
10974
+ .filter(({ job, context }) => (0, reviewApps_1.isReviewDeliveryJob)(context, job) &&
10975
+ job.stage === "deploy" &&
10976
+ job.gate === "manual")
10977
+ .map(({ context }) => context.name));
10978
+ const ids = new Set([...triggerJobs]
10979
+ .filter(([, { job, context }]) => (0, reviewApps_1.isReviewDeliveryJob)(context, job) &&
10980
+ (gatedByConfig || manualComponents.has(context.name)))
10981
+ .map(([id]) => id));
10982
+ // jobs depending on a moved job (e.g. a custom e2e job waiting for a
10983
+ // review deploy) can't stay behind: they move along
10984
+ let grown = ids.size > 0;
10985
+ while (grown) {
10986
+ grown = false;
10987
+ for (const [id, { githubJob }] of triggerJobs) {
10988
+ if (!ids.has(id) && ((_a = githubJob.needs) === null || _a === void 0 ? void 0 : _a.some((need) => ids.has(need)))) {
10989
+ ids.add(id);
10990
+ grown = true;
10991
+ }
10992
+ }
10993
+ }
10994
+ return [...ids];
10995
+ };
10996
+ exports.getGatedReviewDeliveryJobIds = getGatedReviewDeliveryJobIds;
10997
+ /**
10998
+ * the review deploy workflow (`▶️ catladder deploy review`): the review
10999
+ * delivery chains of a pull request, behind a guard that decides
11000
+ * whether this run deploys (see `catci review-app github-guard`).
11001
+ *
11002
+ * Started by
11003
+ * - the review workflow, once it is green and the label/draft policy
11004
+ * says the PR deploys (`source: ci`)
11005
+ * - switching the label on (opt-in: adding it, opt-out: removing the
11006
+ * skip label) or marking a draft ready (`drafts: "checksOnly"`)
11007
+ * - a human or agent dispatching it with the PR number — explicit
11008
+ * requests always deploy, provided the PR's CI is green
11009
+ *
11010
+ * Artifacts are scoped to a single run, so the run rebuilds what the
11011
+ * chains need (the app build); tests are not rerun — the guard checks
11012
+ * the PR's `catladder ✅` instead.
11013
+ */
11014
+ const makeReviewDeployWorkflow = ({ deliveryIds, triggerJobs, reviewApps, images, workflowEnv, permissions, }) => {
11015
+ var _a, _b;
11016
+ const delivery = new Set(deliveryIds);
11017
+ const isQualityJob = (id) => {
11018
+ const entry = triggerJobs.get(id);
11019
+ return !!entry && (0, resolveRequirements_1.providesCapability)(entry.job, "qualityGate");
11020
+ };
11021
+ // the delivery jobs plus everything upstream they need from the
11022
+ // trigger (rebuilt in this run), except the quality jobs
11023
+ const included = new Set();
11024
+ const stack = [...deliveryIds];
11025
+ let current;
11026
+ while ((current = stack.pop()) !== undefined) {
11027
+ if (included.has(current))
11028
+ continue;
11029
+ included.add(current);
11030
+ for (const need of (_b = (_a = triggerJobs.get(current)) === null || _a === void 0 ? void 0 : _a.githubJob.needs) !== null && _b !== void 0 ? _b : []) {
11031
+ if (triggerJobs.has(need) && !isQualityJob(need))
11032
+ stack.push(need);
11033
+ }
11034
+ }
11035
+ // the guard runs first, every other job only when it says deploy
11036
+ const gatedJob = (job) => {
11037
+ var _a;
11038
+ return ({
11039
+ ...job,
11040
+ needs: [GUARD_JOB_ID, ...((_a = job.needs) !== null && _a !== void 0 ? _a : [])],
11041
+ if: andIf(job.if, `needs.${GUARD_JOB_ID}.outputs.deploy == 'true'`),
11042
+ });
11043
+ };
11044
+ const checkoutPrHead = (step) => {
11045
+ var _a;
11046
+ return ((_a = step.uses) === null || _a === void 0 ? void 0 : _a.startsWith("actions/checkout@"))
11047
+ ? {
11048
+ ...step,
11049
+ // the PR head — a label event's default is the merge ref
11050
+ with: {
11051
+ ...step.with,
11052
+ ref: "${{ github.event.pull_request.head.sha || github.sha }}",
11053
+ },
11054
+ }
11055
+ : step;
11056
+ };
11057
+ const chainJobs = Object.fromEntries([...included].sort().map((id) => {
11058
+ const { githubJob } = triggerJobs.get(id);
11059
+ return [
11060
+ id,
11061
+ gatedJob({
11062
+ ...githubJob,
11063
+ // the quality jobs stay in the review workflow — the guard
11064
+ // checks their aggregate instead
11065
+ ...(githubJob.needs
11066
+ ? {
11067
+ needs: githubJob.needs.filter((need) => !isQualityJob(need) || delivery.has(need)),
11068
+ }
11069
+ : {}),
11070
+ steps: githubJob.steps.map(checkoutPrHead),
11071
+ }),
11072
+ ];
11073
+ }));
11074
+ // only the images the chains run in (not e.g. the release image)
11075
+ const neededIds = new Set(Object.values(chainJobs).flatMap((job) => { var _a; return (_a = job.needs) !== null && _a !== void 0 ? _a : []; }));
11076
+ const imageJobs = Object.fromEntries(Object.entries((0, ensureImageJobs_1.makeEnsureImageGithubJobs)(images))
11077
+ .filter(([id]) => neededIds.has(id))
11078
+ .map(([id, job]) => [id, gatedJob(job)]));
11079
+ const switchLabel = (0, reviewApps_1.getReviewAppsSwitchLabel)(reviewApps);
11080
+ const pullRequestTypes = [
11081
+ ...(switchLabel ? [switchLabel.deployOn] : []),
11082
+ ...(reviewApps.drafts === "checksOnly" ? ["ready_for_review"] : []),
11083
+ ];
11084
+ // pull request events that concern the review apps — other label
11085
+ // changes must neither run the guard nor cancel a running deploy
11086
+ const relevantEvent = [
11087
+ "github.event_name == 'workflow_dispatch'",
11088
+ ...(switchLabel
11089
+ ? [
11090
+ `(github.event.action == ${literal(switchLabel.deployOn)} && github.event.label.name == ${literal(switchLabel.label)})`,
11091
+ ]
11092
+ : []),
11093
+ ...(reviewApps.drafts === "checksOnly"
11094
+ ? ["github.event.action == 'ready_for_review'"]
11095
+ : []),
11096
+ ].join(" || ");
11097
+ return {
11098
+ name: "▶️ catladder deploy review",
11099
+ on: {
11100
+ workflow_dispatch: {
11101
+ inputs: {
11102
+ pr: {
11103
+ description: "number of the pull request whose review apps to deploy",
11104
+ required: true,
11105
+ type: "string",
11106
+ },
11107
+ source: {
11108
+ description: "who requests the deploy: manual (always deploys) or ci (applies the label/draft policy)",
11109
+ required: false,
11110
+ type: "choice",
11111
+ options: ["manual", "ci"],
11112
+ default: "manual",
11113
+ },
11114
+ },
11115
+ },
11116
+ ...(pullRequestTypes.length > 0
11117
+ ? { pull_request: { types: pullRequestTypes } }
11118
+ : {}),
11119
+ },
11120
+ concurrency: reviewDeployConcurrency(relevantEvent),
11121
+ permissions,
11122
+ env: { ...workflowEnv, CL_PR_NUMBER: `\${{ ${PR_NUMBER_EXPRESSION} }}` },
11123
+ jobs: {
11124
+ [GUARD_JOB_ID]: {
11125
+ name: "🚦 review deploy guard",
11126
+ "runs-on": "ubuntu-latest",
11127
+ ...(pullRequestTypes.length > 0
11128
+ ? { if: `\${{ ${relevantEvent} }}` }
11129
+ : {}),
11130
+ permissions: {
11131
+ contents: "read",
11132
+ "pull-requests": "read",
11133
+ checks: "read",
11134
+ statuses: "write",
11135
+ },
11136
+ env: {
11137
+ GITHUB_TOKEN: "${{ github.token }}",
11138
+ CL_REVIEW_APPS: JSON.stringify(reviewApps),
11139
+ CL_DEPLOY_SOURCE: "${{ github.event_name == 'workflow_dispatch' && (inputs.source || 'manual') || 'label' }}",
11140
+ CL_HEAD_SHA: "${{ github.event.pull_request.head.sha || github.sha }}",
11141
+ },
11142
+ outputs: { deploy: "${{ steps.main.outputs.deploy }}" },
11143
+ steps: [
11144
+ catciCheckout(),
11145
+ {
11146
+ name: "🚦 review deploy guard",
11147
+ id: "main",
11148
+ run: `node ${CATCI} review-app github-guard`,
11149
+ shell: "bash",
11150
+ },
11151
+ {
11152
+ // the pull request shows the deploy in flight
11153
+ name: "report pending review app",
11154
+ if: "${{ steps.main.outputs.deploy == 'true' }}",
11155
+ env: { GH_TOKEN: "${{ github.token }}" },
11156
+ run: `gh api "repos/$GITHUB_REPOSITORY/statuses/$CL_HEAD_SHA" -f state=pending -f context="${exports.REVIEW_APP_STATUS_CONTEXT}" -f description="deploying the review apps" -f target_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" > /dev/null`,
11157
+ shell: "bash",
11158
+ },
11159
+ ],
11160
+ },
11161
+ ...imageJobs,
11162
+ ...chainJobs,
11163
+ ...(0, exports.makeReviewAppAggregateJob)([GUARD_JOB_ID, ...Object.keys(imageJobs), ...Object.keys(chainJobs)], `needs.${GUARD_JOB_ID}.outputs.deploy == 'true'`),
11164
+ },
11165
+ };
11166
+ };
11167
+ exports.makeReviewDeployWorkflow = makeReviewDeployWorkflow;
11168
+ exports.REVIEW_APP_AGGREGATE_JOB_ID = "catladder-review-app-ok";
11169
+ exports.REVIEW_APP_AGGREGATE_JOB_NAME = "catladder review app ✅";
11170
+ /** the commit status context the aggregate job reports */
11171
+ exports.REVIEW_APP_STATUS_CONTEXT = "catladder review app";
11172
+ /**
11173
+ * one job with a STABLE name that succeeds exactly when the review apps
11174
+ * of this run deployed and verified — the result a merge gate (a bot,
11175
+ * or a branch rule for projects that deploy every PR) waits for,
11176
+ * regardless of the project's components.
11177
+ *
11178
+ * It also reports a commit status on the PR head: runs started by a
11179
+ * dispatch (the trigger job, or by hand) are attached to the commit but
11180
+ * not shown on the pull request, a commit status always is.
11181
+ *
11182
+ * Runs only when the run deploys (the guard decided so, or the review
11183
+ * workflow's start condition held) — a run that doesn't deploy says
11184
+ * nothing about the review app (its check is skipped, the commit status
11185
+ * untouched). Neither does a cancelled run (superseded by a newer
11186
+ * deploy, or stopped): a gate keeps waiting instead of seeing red.
11187
+ */
11188
+ const makeReviewAppAggregateJob = (needs,
11189
+ /** when the run deploys the review apps (a github expression) */
11190
+ deploysWhen) => ({
11191
+ [exports.REVIEW_APP_AGGREGATE_JOB_ID]: {
11192
+ name: exports.REVIEW_APP_AGGREGATE_JOB_NAME,
11193
+ "runs-on": "ubuntu-latest",
11194
+ needs: [...needs].sort(),
11195
+ if: `\${{ always() && (${deploysWhen}) }}`,
11196
+ permissions: { statuses: "write" },
11197
+ env: {
11198
+ GH_TOKEN: "${{ github.token }}",
11199
+ HEAD_SHA: "${{ github.event.pull_request.head.sha || github.sha }}",
11200
+ RUN_URL: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}",
11201
+ },
11202
+ steps: [
11203
+ {
11204
+ name: exports.REVIEW_APP_AGGREGATE_JOB_NAME,
11205
+ // the needs context only interpolates inside the workflow file,
11206
+ // so this script stays inline
11207
+ run: [
11208
+ `results='\${{ toJSON(needs) }}'`,
11209
+ `echo "$results"`,
11210
+ `if echo "$results" | grep -qE '"result": "failure"'; then`,
11211
+ ` state=failure; description="the review apps did not deploy and verify"`,
11212
+ `elif echo "$results" | grep -qE '"result": "cancelled"'; then`,
11213
+ ` # superseded by a newer deploy, or stopped: no result, not red`,
11214
+ ` echo "cancelled — no review app result reported"; exit 0`,
11215
+ `elif echo "$results" | grep -qE '"result": "skipped"'; then`,
11216
+ ` state=failure; description="the review apps did not deploy and verify"`,
11217
+ `else`,
11218
+ ` state=success; description="review apps deployed and verified"`,
11219
+ `fi`,
11220
+ `gh api "repos/$GITHUB_REPOSITORY/statuses/$HEAD_SHA" -f state="$state" -f context="${exports.REVIEW_APP_STATUS_CONTEXT}" -f description="$description" -f target_url="$RUN_URL" > /dev/null`,
11221
+ `echo "$description"`,
11222
+ `[ "$state" = success ]`,
11223
+ ].join("\n"),
11224
+ shell: "bash",
11225
+ },
11226
+ ],
11227
+ },
11228
+ });
11229
+ exports.makeReviewAppAggregateJob = makeReviewAppAggregateJob;
11230
+ /** a checkout of just the materialized catci */
11231
+ const catciCheckout = () => ({
11232
+ name: "Checkout",
11233
+ uses: "actions/checkout@v4",
11234
+ with: { "sparse-checkout": shippedCatci_1.GENERATED_CATCI_FOLDER },
11235
+ });
11236
+ /**
11237
+ * one deploy/stop of a PR's review apps at a time, the newest wins —
11238
+ * shared by the deploy and the stop workflow, so closing a PR or
11239
+ * switching the label off cancels a deploy in flight before tearing
11240
+ * down. Events that don't concern the review apps get a group of
11241
+ * their own, so they never cancel anything.
11242
+ */
11243
+ const reviewDeployConcurrency = (relevantEvent) => ({
11244
+ group: `catladder-review-deploy-\${{ ${PR_NUMBER_EXPRESSION} }}\${{ !(${relevantEvent}) && format('-ignored-{0}', github.run_id) || '' }}`,
11245
+ "cancel-in-progress": true,
11246
+ });
11247
+ /**
11248
+ * the last job of the review workflow: once it is green, dispatch the
11249
+ * review deploy workflow when the label/draft policy says this PR
11250
+ * deploys (read fresh from the PR, so a label set while CI ran counts).
11251
+ * Only for policies that deploy on their own — `deploy: "manual"` waits
11252
+ * for an explicit dispatch.
11253
+ */
11254
+ const makeReviewDeployTriggerJob = (reviewApps,
11255
+ /** the review workflow deployed itself when this held at its start */
11256
+ inRunCondition) => reviewApps.deploy === "manual"
11257
+ ? {}
11258
+ : {
11259
+ [TRIGGER_JOB_ID]: {
11260
+ name: "▶️ trigger review deploy",
11261
+ "runs-on": "ubuntu-latest",
11262
+ needs: [aggregateCheckJob_1.AGGREGATE_CHECK_JOB_ID],
11263
+ ...(inRunCondition
11264
+ ? {
11265
+ // NOT success(): it also checks the transitive needs,
11266
+ // and the chains this run skipped on purpose would make
11267
+ // it false — exactly when the trigger is needed
11268
+ if: `\${{ !cancelled() && needs.${aggregateCheckJob_1.AGGREGATE_CHECK_JOB_ID}.result == 'success' && !(${inRunCondition}) }}`,
11269
+ }
11270
+ : {}),
11271
+ permissions: {
11272
+ contents: "read",
11273
+ "pull-requests": "read",
11274
+ actions: "write",
11275
+ },
11276
+ env: {
11277
+ GITHUB_TOKEN: "${{ github.token }}",
11278
+ CL_REVIEW_APPS: JSON.stringify(reviewApps),
11279
+ CL_HEAD_REF: "${{ github.head_ref }}",
11280
+ CL_HEAD_SHA: "${{ github.event.pull_request.head.sha }}",
11281
+ },
11282
+ steps: [
11283
+ catciCheckout(),
11284
+ {
11285
+ name: "▶️ trigger review deploy",
11286
+ run: `node ${CATCI} review-app github-trigger`,
11287
+ shell: "bash",
11288
+ },
11289
+ ],
11290
+ },
11291
+ };
11292
+ exports.makeReviewDeployTriggerJob = makeReviewDeployTriggerJob;
11293
+ /**
11294
+ * the review stop workflow under a label policy: switching the label
11295
+ * off (opt-in: removing it, opt-out: adding the skip label) stops the
11296
+ * review apps, like closing the PR does. It shares the deploy
11297
+ * workflow's concurrency group, so a deploy in flight is cancelled
11298
+ * first.
11299
+ */
11300
+ const withReviewStopSwitch = (workflow, reviewApps,
11301
+ /** the review workflow deploys itself (see getInRunDeployCondition) */
11302
+ reviewRunDeploys = false) => {
11303
+ const switchLabel = (0, reviewApps_1.getReviewAppsSwitchLabel)(reviewApps);
11304
+ const relevantEvent = [
11305
+ "github.event_name == 'workflow_dispatch'",
11306
+ "github.event.action == 'closed'",
11307
+ ...(switchLabel
11308
+ ? [
11309
+ `(github.event.action == ${literal(switchLabel.stopOn)} && github.event.label.name == ${literal(switchLabel.label)})`,
11310
+ ]
11311
+ : []),
11312
+ ].join(" || ");
11313
+ return {
11314
+ ...workflow,
11315
+ on: {
11316
+ ...workflow.on,
11317
+ pull_request: {
11318
+ types: ["closed", ...(switchLabel ? [switchLabel.stopOn] : [])],
11319
+ },
11320
+ },
11321
+ concurrency: reviewDeployConcurrency(relevantEvent),
11322
+ jobs: Object.fromEntries(Object.entries({
11323
+ ...(reviewRunDeploys ? makeCancelReviewRunsJob() : {}),
11324
+ ...Object.fromEntries(Object.entries(workflow.jobs).map(([id, job]) => {
11325
+ var _a;
11326
+ return [
11327
+ id,
11328
+ // the teardown waits until no review run deploys anymore
11329
+ reviewRunDeploys && !id.startsWith("catladder-image-")
11330
+ ? {
11331
+ ...job,
11332
+ needs: [CANCEL_REVIEW_RUNS_JOB_ID, ...((_a = job.needs) !== null && _a !== void 0 ? _a : [])],
11333
+ }
11334
+ : job,
11335
+ ];
11336
+ })),
11337
+ }).map(([id, job]) => [
11338
+ id,
11339
+ switchLabel ? { ...job, if: andIf(job.if, relevantEvent) } : job,
11340
+ ])),
11341
+ };
11342
+ };
11343
+ exports.withReviewStopSwitch = withReviewStopSwitch;
11344
+ const CANCEL_REVIEW_RUNS_JOB_ID = "catladder-wait-review-runs";
11345
+ /**
11346
+ * the review workflow deploys review apps itself, outside the deploy /
11347
+ * stop concurrency group — a teardown would race a deploy still running
11348
+ * there (the deploy finishing after the stop leaves the app and its
11349
+ * database behind). So the stop first waits for the PR's unfinished
11350
+ * review runs to end — cancelling them when the PR was closed, letting
11351
+ * them finish when only the label was switched off (their CI still
11352
+ * counts). Never fails: a stuck run must not keep the teardown from
11353
+ * running (it gives up after 30 minutes).
11354
+ */
11355
+ const makeCancelReviewRunsJob = () => ({
11356
+ [CANCEL_REVIEW_RUNS_JOB_ID]: {
11357
+ name: "wait for running review deploys",
11358
+ "runs-on": "ubuntu-latest",
11359
+ permissions: { actions: "write", "pull-requests": "read" },
11360
+ env: { GH_TOKEN: "${{ github.token }}" },
11361
+ steps: [
11362
+ {
11363
+ name: "wait for running review deploys",
11364
+ run: [
11365
+ `branch=$(gh pr view "$CL_PR_NUMBER" -R "$GITHUB_REPOSITORY" --json headRefName -q .headRefName) || exit 0`,
11366
+ `runs=$(gh run list -R "$GITHUB_REPOSITORY" --workflow catladder-review.yml --branch "$branch" --limit 20 --json databaseId,status -q '.[] | select(.status != "completed") | .databaseId') || exit 0`,
11367
+ `# a closed PR's CI is worthless: cancel it. Otherwise let the`,
11368
+ `# PR's CI finish (cancelling it would leave catladder ✅ red)`,
11369
+ `if [ "$(jq -r '.action // ""' "$GITHUB_EVENT_PATH")" = closed ]; then`,
11370
+ ` for id in $runs; do echo "cancelling review run $id"; gh run cancel "$id" -R "$GITHUB_REPOSITORY" || true; done`,
11371
+ `fi`,
11372
+ `for id in $runs; do`,
11373
+ ` echo "waiting for review run $id"`,
11374
+ ` for _ in $(seq 1 360); do`,
11375
+ ` [ "$(gh run view "$id" -R "$GITHUB_REPOSITORY" --json status -q .status)" = completed ] && break`,
11376
+ ` sleep 5`,
11377
+ ` done`,
11378
+ `done`,
11379
+ `exit 0`,
11380
+ ].join("\n"),
11381
+ shell: "bash",
11382
+ },
11383
+ ],
11384
+ },
11385
+ });
11386
+ /**
11387
+ * `drafts: "skip"`: draft PRs run nothing — every job of the review
11388
+ * workflow skips them, and marking the PR ready starts the run. A
11389
+ * skipped `catladder ✅` can't let a draft through: drafts can't be
11390
+ * merged.
11391
+ */
11392
+ const withDraftsSkipped = (workflow) => {
11393
+ const on = workflow.on;
11394
+ return {
11395
+ ...workflow,
11396
+ on: {
11397
+ ...workflow.on,
11398
+ pull_request: {
11399
+ ...on.pull_request,
11400
+ types: [...on.pull_request.types, "ready_for_review"],
11401
+ },
11402
+ },
11403
+ jobs: Object.fromEntries(Object.entries(workflow.jobs).map(([id, job]) => [
11404
+ id,
11405
+ { ...job, if: andIf(job.if, "!github.event.pull_request.draft") },
11406
+ ])),
11407
+ };
11408
+ };
11409
+ exports.withDraftsSkipped = withDraftsSkipped;
11410
+ //# sourceMappingURL=reviewDeployWorkflow.js.map
11411
+
11412
+ /***/ }),
11413
+
10716
11414
  /***/ 61071:
10717
11415
  /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
10718
11416
 
@@ -10817,6 +11515,8 @@ const rules_1 = __nccwpck_require__(78503);
10817
11515
  const config_1 = __nccwpck_require__(75591);
10818
11516
  const configruedEnvs_1 = __nccwpck_require__(38866);
10819
11517
  const autoStop_1 = __nccwpck_require__(72792);
11518
+ const reviewApps_1 = __nccwpck_require__(67071);
11519
+ const reviewDeployGate_1 = __nccwpck_require__(3485);
10820
11520
  const createAllJobs_1 = __nccwpck_require__(83477);
10821
11521
  const jobImagesPlan_1 = __nccwpck_require__(59623);
10822
11522
  const shippedCatci_1 = __nccwpck_require__(27967);
@@ -10930,10 +11630,32 @@ class GitlabBackend {
10930
11630
  ...config_1.ALL_PIPELINE_TRIGGERS,
10931
11631
  ...branchTriggers,
10932
11632
  ];
10933
- const jobsPerTrigger = await Promise.all(allTriggers.map(async (trigger) => ({
10934
- trigger,
10935
- jobs: await (0, createGitlabJobs_1.createGitlabJobs)(await (0, createAllJobs_1.createAllJobs)({ config, trigger, pipelineType: this.type }), images, getGitlabRulesForTrigger(trigger)),
10936
- })));
11633
+ const reviewApps = (0, reviewApps_1.getReviewAppsConfig)(config);
11634
+ const jobsPerTrigger = await Promise.all(allTriggers.map(async (trigger) => {
11635
+ const catladderJobs = await (0, createAllJobs_1.createAllJobs)({
11636
+ config,
11637
+ trigger,
11638
+ pipelineType: this.type,
11639
+ });
11640
+ // review deploys behind one switch per MR (reviewApps config)
11641
+ const reviewDeployGate = trigger === "mr"
11642
+ ? (0, reviewDeployGate_1.addGitlabReviewDeployGate)(catladderJobs, reviewApps)
11643
+ : undefined;
11644
+ const jobs = await (0, createGitlabJobs_1.createGitlabJobs)(catladderJobs, images, getGitlabRulesForTrigger(trigger));
11645
+ return {
11646
+ trigger,
11647
+ jobs: reviewDeployGate
11648
+ ? [
11649
+ {
11650
+ name: reviewDeployGate_1.REVIEW_DEPLOY_GATE_JOB_NAME,
11651
+ gitlabJob: reviewDeployGate,
11652
+ context: null,
11653
+ },
11654
+ ...jobs,
11655
+ ]
11656
+ : jobs,
11657
+ };
11658
+ }));
10937
11659
  const allJobsPerTrigger = jobsPerTrigger.flatMap(({ jobs }) => jobs);
10938
11660
  const stages = (0, getPipelineStages_1.getPipelineStages)(config, new Set(allJobsPerTrigger
10939
11661
  .map(({ gitlabJob }) => gitlabJob.stage)
@@ -11013,6 +11735,7 @@ class GitlabBackend {
11013
11735
  },
11014
11736
  branchPipelines: branchTriggers.map(({ branch }) => branch),
11015
11737
  autoStop: (0, autoStop_1.getAutoStopConfig)(config),
11738
+ reviewApps,
11016
11739
  });
11017
11740
  }
11018
11741
  }
@@ -11467,21 +12190,23 @@ function getGitlabNeedsForWorkspaceJob(context, { needs }, allJobs) {
11467
12190
  "use strict";
11468
12191
 
11469
12192
  Object.defineProperty(exports, "__esModule", ({ value: true }));
11470
- exports.createGitlabPipelineWithDefaults = void 0;
12193
+ exports.createGitlabPipelineWithDefaults = exports.DEFAULT_PIPELINE_IMAGE = void 0;
11471
12194
  const bash_1 = __nccwpck_require__(94143);
11472
12195
  const autoStop_1 = __nccwpck_require__(72792);
12196
+ const reviewApps_1 = __nccwpck_require__(67071);
11473
12197
  const rules_1 = __nccwpck_require__(78503);
11474
12198
  /**
11475
12199
  * gitlab requires a top-level default image for jobs that don't set their
11476
12200
  * own. Every catladder-generated job sets an explicit image, so this
11477
- * fallback is never actually pulled — it just needs to be a valid,
12201
+ * fallback is never pulled for them — it just needs to be a valid,
11478
12202
  * build-free image (a catladder job image would drag in a build job).
12203
+ * Trivial jobs (the review deploy switch) reuse it explicitly.
11479
12204
  */
11480
- const DEFAULT_PIPELINE_IMAGE = "node:22";
11481
- const createGitlabPipelineWithDefaults = ({ image, variables, before_script, branchPipelines = [], autoStop, ...config }) => {
12205
+ exports.DEFAULT_PIPELINE_IMAGE = "node:22";
12206
+ const createGitlabPipelineWithDefaults = ({ image, variables, before_script, branchPipelines = [], autoStop, reviewApps, ...config }) => {
11482
12207
  const pinLabel = autoStop && autoStop.pinLabel;
11483
12208
  return {
11484
- image: image !== null && image !== void 0 ? image : DEFAULT_PIPELINE_IMAGE,
12209
+ image: image !== null && image !== void 0 ? image : exports.DEFAULT_PIPELINE_IMAGE,
11485
12210
  variables: {
11486
12211
  FF_USE_FASTZIP: "true", // enable fastzip - a faster zip implementation that also supports level configuration.
11487
12212
  ARTIFACT_COMPRESSION_LEVEL: "fast", // we value speed over compression
@@ -11519,6 +12244,16 @@ const createGitlabPipelineWithDefaults = ({ image, variables, before_script, bra
11519
12244
  PIPELINE_NAME: "Thinking...",
11520
12245
  },
11521
12246
  },
12247
+ // reviewApps.drafts: "skip" — draft MRs get no pipeline (rules
12248
+ // stop at the first match, so this must come first)
12249
+ ...((reviewApps === null || reviewApps === void 0 ? void 0 : reviewApps.drafts) === "skip"
12250
+ ? [
12251
+ {
12252
+ if: `${rules_1.RULE_IS_MERGE_REQUEST.if} && ${reviewApps_1.GITLAB_DRAFT_MR_CONDITION}`,
12253
+ when: "never",
12254
+ },
12255
+ ]
12256
+ : []),
11522
12257
  // pinned MRs (rules stop at the first match, so this must come
11523
12258
  // before the plain merge-request rule)
11524
12259
  ...(pinLabel
@@ -11886,6 +12621,107 @@ exports.getReleaseGateJobNames = getReleaseGateJobNames;
11886
12621
 
11887
12622
  /***/ }),
11888
12623
 
12624
+ /***/ 3485:
12625
+ /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
12626
+
12627
+ "use strict";
12628
+
12629
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
12630
+ exports.addGitlabReviewDeployGate = exports.REVIEW_DEPLOY_GATE_JOB_NAME = void 0;
12631
+ const reviewApps_1 = __nccwpck_require__(67071);
12632
+ const rules_1 = __nccwpck_require__(78503);
12633
+ const createGitlabPipeline_1 = __nccwpck_require__(32060);
12634
+ exports.REVIEW_DEPLOY_GATE_JOB_NAME = "🚀 deploy review";
12635
+ /**
12636
+ * the review deploy switch of a merge request (see reviewApps.ts): ONE
12637
+ * job per MR pipeline that unlocks the delivery chains (docker → deploy
12638
+ * → verify) of all components' review apps, instead of a manual job per
12639
+ * component — or a docker build per component in every pipeline.
12640
+ *
12641
+ * The chain heads `needs` the switch. It is a no-op job with
12642
+ * `allow_failure: true`: while it is unplayed, gitlab marks its
12643
+ * dependents skipped and the pipeline still passes ("Pipelines must
12644
+ * succeed" keeps working); playing it resets them to created, so it
12645
+ * can be played any time — early clicks wait for the builds and tests
12646
+ * like the chains always did (the same mechanism as the release
12647
+ * button).
12648
+ *
12649
+ * The label/draft policy decides whether it starts on its own. Label
12650
+ * changes only apply to the next pipeline (gitlab doesn't start one for
12651
+ * them) — `catladder mr review-app-on` sets the label and triggers one.
12652
+ *
12653
+ * Mutates the needs of the gated jobs; returns the switch job, or
12654
+ * nothing when review deploys aren't gated (or nothing deploys).
12655
+ */
12656
+ const addGitlabReviewDeployGate = (allJobs, reviewApps) => {
12657
+ var _a;
12658
+ if (!(0, reviewApps_1.isReviewDeployGated)(reviewApps)) {
12659
+ return undefined;
12660
+ }
12661
+ let gated = false;
12662
+ for (const { context, jobs } of allJobs.components) {
12663
+ for (const job of jobs) {
12664
+ // the chain heads: verify follows its deploy anyway
12665
+ if ((0, reviewApps_1.isReviewDeliveryJob)(context, job) && job.stage !== "verify") {
12666
+ job.needs = [
12667
+ ...((_a = job.needs) !== null && _a !== void 0 ? _a : []),
12668
+ { job: exports.REVIEW_DEPLOY_GATE_JOB_NAME, artifacts: false, global: true },
12669
+ ];
12670
+ gated = true;
12671
+ }
12672
+ }
12673
+ }
12674
+ if (!gated) {
12675
+ return undefined;
12676
+ }
12677
+ const isMr = rules_1.RULE_IS_MERGE_REQUEST.if;
12678
+ const manual = { when: "manual", allow_failure: true };
12679
+ const auto = { when: "on_success" };
12680
+ const policyRules = reviewApps.deploy === "optIn"
12681
+ ? [
12682
+ {
12683
+ if: `${isMr} && $CI_MERGE_REQUEST_LABELS =~ ${(0, reviewApps_1.getGitlabLabelRegex)(reviewApps.label)}`,
12684
+ ...auto,
12685
+ },
12686
+ { if: isMr, ...manual },
12687
+ ]
12688
+ : reviewApps.deploy === "optOut"
12689
+ ? [
12690
+ {
12691
+ if: `${isMr} && $CI_MERGE_REQUEST_LABELS =~ ${(0, reviewApps_1.getGitlabLabelRegex)(reviewApps.skipLabel)}`,
12692
+ ...manual,
12693
+ },
12694
+ { if: isMr, ...auto },
12695
+ ]
12696
+ : reviewApps.deploy === "manual"
12697
+ ? [{ if: isMr, ...manual }]
12698
+ : [{ if: isMr, ...auto }];
12699
+ return {
12700
+ stage: "setup",
12701
+ image: createGitlabPipeline_1.DEFAULT_PIPELINE_IMAGE,
12702
+ variables: { GIT_STRATEGY: "none" },
12703
+ needs: [],
12704
+ interruptible: true,
12705
+ allow_failure: true,
12706
+ script: [
12707
+ `echo "review apps unlocked — the docker → deploy → verify chains of all components run as soon as their builds and tests succeeded"`,
12708
+ ],
12709
+ rules: [
12710
+ rules_1.RULE_NEVER_ON_AGENT_TRIGGER,
12711
+ // drafts never deploy on their own (drafts: "skip" has no
12712
+ // pipeline at all, see the workflow rules)
12713
+ ...(reviewApps.drafts !== "likeReady"
12714
+ ? [{ if: `${isMr} && ${reviewApps_1.GITLAB_DRAFT_MR_CONDITION}`, ...manual }]
12715
+ : []),
12716
+ ...policyRules,
12717
+ ],
12718
+ };
12719
+ };
12720
+ exports.addGitlabReviewDeployGate = addGitlabReviewDeployGate;
12721
+ //# sourceMappingURL=reviewDeployGate.js.map
12722
+
12723
+ /***/ }),
12724
+
11889
12725
  /***/ 21444:
11890
12726
  /***/ ((__unused_webpack_module, exports) => {
11891
12727
 
@@ -15610,6 +16446,7 @@ const jobs_1 = __nccwpck_require__(98521);
15610
16446
  const utils_1 = __nccwpck_require__(71286);
15611
16447
  const autoStop_1 = __nccwpck_require__(72792);
15612
16448
  const stop_1 = __nccwpck_require__(35937);
16449
+ const reviewApps_1 = __nccwpck_require__(67071);
15613
16450
  const variables_1 = __nccwpck_require__(54768);
15614
16451
  exports.DEPLOY_JOB_NAME = "🚀 Deploy";
15615
16452
  class DeployJob extends jobs_1.CatladderJob {
@@ -15646,9 +16483,15 @@ class DeployJob extends jobs_1.CatladderJob {
15646
16483
  : ((_b = context.componentConfig.env) === null || _b === void 0 ? void 0 : _b.stage) === false
15647
16484
  ? "auto" // is prod, but no staging, auto deploy
15648
16485
  : "manual"; // manually deploy
15649
- const whenDeploy = whenDeployDefined
15650
- ? whenDeployDefined
15651
- : whenDeployDefault;
16486
+ // gated review apps: one switch per MR/PR unlocks the whole delivery
16487
+ // chain (see reviewApps.ts), the deploy itself then runs on its own
16488
+ const isGatedReviewApp = context.environment.instance.type === "review" &&
16489
+ (0, reviewApps_1.isReviewDeployGated)((0, reviewApps_1.getReviewAppsConfig)(context.fullConfig));
16490
+ const whenDeploy = isGatedReviewApp
16491
+ ? "auto"
16492
+ : whenDeployDefined
16493
+ ? whenDeployDefined
16494
+ : whenDeployDefault;
15652
16495
  // wait for the deployment of other components first
15653
16496
  const waitForRequirements = (_d = (_c = deployConfig === null || deployConfig === void 0 ? void 0 : deployConfig.waitFor) === null || _c === void 0 ? void 0 : _c.map((c) => ({
15654
16497
  capability: "deployment",
@@ -18486,6 +19329,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
18486
19329
  Object.defineProperty(exports, "__esModule", ({ value: true }));
18487
19330
  __exportStar(__nccwpck_require__(46734), exports);
18488
19331
  __exportStar(__nccwpck_require__(72792), exports);
19332
+ __exportStar(__nccwpck_require__(67071), exports);
18489
19333
  __exportStar(__nccwpck_require__(62853), exports);
18490
19334
  __exportStar(__nccwpck_require__(63557), exports);
18491
19335
  __exportStar(__nccwpck_require__(78503), exports);
@@ -19617,7 +20461,7 @@ exports.projectFileExists = projectFileExists;
19617
20461
  "use strict";
19618
20462
 
19619
20463
  Object.defineProperty(exports, "__esModule", ({ value: true }));
19620
- exports.resolveRequirements = void 0;
20464
+ exports.resolveRequirements = exports.providesCapability = void 0;
19621
20465
  /**
19622
20466
  * jobs that don't declare `provides` get a capability derived from
19623
20467
  * their stage. This keeps e.g. custom jobs in the test stage acting
@@ -19648,6 +20492,7 @@ const providesCapability = (job, capability) => {
19648
20492
  ((_a = CAPABILITY_IMPLICATIONS[c]) !== null && _a !== void 0 ? _a : []).includes(capability);
19649
20493
  });
19650
20494
  };
20495
+ exports.providesCapability = providesCapability;
19651
20496
  /**
19652
20497
  * the planner: resolves the semantic `requires` declarations of all jobs
19653
20498
  * to concrete job dependencies (`needs`), now that all jobs of the
@@ -19689,7 +20534,7 @@ const expandRequirement = (context, job, requirement, allJobs) => {
19689
20534
  c.context.env === env;
19690
20535
  })
19691
20536
  .flatMap((c) => c.jobs);
19692
- const providers = candidates.filter((candidate) => candidate !== job && providesCapability(candidate, capability));
20537
+ const providers = candidates.filter((candidate) => candidate !== job && (0, exports.providesCapability)(candidate, capability));
19693
20538
  // consuming artifacts from nobody is almost always a broken config
19694
20539
  const isStrict = (_b = strict !== null && strict !== void 0 ? strict : artifacts) !== null && _b !== void 0 ? _b : false;
19695
20540
  if (isStrict && providers.length === 0) {
@@ -20003,6 +20848,143 @@ exports.getReleaseMethod = getReleaseMethod;
20003
20848
 
20004
20849
  /***/ }),
20005
20850
 
20851
+ /***/ 67071:
20852
+ /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
20853
+
20854
+ "use strict";
20855
+
20856
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
20857
+ if (k2 === undefined) k2 = k;
20858
+ var desc = Object.getOwnPropertyDescriptor(m, k);
20859
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
20860
+ desc = { enumerable: true, get: function() { return m[k]; } };
20861
+ }
20862
+ Object.defineProperty(o, k2, desc);
20863
+ }) : (function(o, m, k, k2) {
20864
+ if (k2 === undefined) k2 = k;
20865
+ o[k2] = m[k];
20866
+ }));
20867
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
20868
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
20869
+ };
20870
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
20871
+ exports.isReviewDeliveryJob = exports.GITLAB_DRAFT_MR_CONDITION = exports.getGitlabLabelRegex = exports.getReviewAppsSwitchLabel = exports.isReviewDeployGated = exports.getReviewAppsConfig = void 0;
20872
+ const autoStop_1 = __nccwpck_require__(72792);
20873
+ const reviewAppsPolicy_1 = __nccwpck_require__(49821);
20874
+ __exportStar(__nccwpck_require__(49821), exports);
20875
+ const getReviewAppsConfig = (config) => {
20876
+ var _a, _b, _c, _d, _e, _f, _g, _h;
20877
+ return ({
20878
+ deploy: (_b = (_a = config.reviewApps) === null || _a === void 0 ? void 0 : _a.deploy) !== null && _b !== void 0 ? _b : "auto",
20879
+ // single colon on purpose: gitlab treats `a::b` labels as scoped and
20880
+ // mutually exclusive, which would make this label and the
20881
+ // `catladder::pin-review` pin label displace each other
20882
+ label: (_d = (_c = config.reviewApps) === null || _c === void 0 ? void 0 : _c.label) !== null && _d !== void 0 ? _d : reviewAppsPolicy_1.DEFAULT_REVIEW_APP_LABEL,
20883
+ skipLabel: (_f = (_e = config.reviewApps) === null || _e === void 0 ? void 0 : _e.skipLabel) !== null && _f !== void 0 ? _f : reviewAppsPolicy_1.DEFAULT_NO_REVIEW_APP_LABEL,
20884
+ drafts: (_h = (_g = config.reviewApps) === null || _g === void 0 ? void 0 : _g.drafts) !== null && _h !== void 0 ? _h : "likeReady",
20885
+ });
20886
+ };
20887
+ exports.getReviewAppsConfig = getReviewAppsConfig;
20888
+ /**
20889
+ * whether the review deploys sit behind the per-MR/PR deploy switch
20890
+ * instead of running in every pipeline
20891
+ */
20892
+ const isReviewDeployGated = (config) => config.deploy !== "auto" || config.drafts === "checksOnly";
20893
+ exports.isReviewDeployGated = isReviewDeployGated;
20894
+ /**
20895
+ * the label whose change switches the review apps on (`labeled` on
20896
+ * opt-in, `unlabeled` on opt-out) or off (the inverse), if the mode is
20897
+ * label-driven at all
20898
+ */
20899
+ const getReviewAppsSwitchLabel = (config) => {
20900
+ switch (config.deploy) {
20901
+ case "optIn":
20902
+ return { label: config.label, deployOn: "labeled", stopOn: "unlabeled" };
20903
+ case "optOut":
20904
+ return {
20905
+ label: config.skipLabel,
20906
+ deployOn: "unlabeled",
20907
+ stopOn: "labeled",
20908
+ };
20909
+ default:
20910
+ return undefined;
20911
+ }
20912
+ };
20913
+ exports.getReviewAppsSwitchLabel = getReviewAppsSwitchLabel;
20914
+ /**
20915
+ * gitlab: matches a label inside `$CI_MERGE_REQUEST_LABELS`
20916
+ * (comma-separated) — anchored so `x` never matches `no-x`
20917
+ */
20918
+ const getGitlabLabelRegex = (label) => `/${(0, autoStop_1.getPinLabelRegexSource)(label)}/`;
20919
+ exports.getGitlabLabelRegex = getGitlabLabelRegex;
20920
+ /**
20921
+ * gitlab: a draft MR, by its title prefix — the only draft signal every
20922
+ * gitlab version exposes to rules
20923
+ */
20924
+ exports.GITLAB_DRAFT_MR_CONDITION = "$CI_MERGE_REQUEST_TITLE =~ /^\\s*(\\[draft\\]|\\(draft\\)|draft:)/i";
20925
+ /**
20926
+ * whether a job belongs to the delivery chain of a review app — the
20927
+ * docker image, the deploy and the post-deploy verification. These jobs
20928
+ * sit behind the review deploy switch; quality jobs (lint, test, audit)
20929
+ * and the app build never do.
20930
+ */
20931
+ const isReviewDeliveryJob = (context, job) => {
20932
+ var _a;
20933
+ return context.type === "component" &&
20934
+ context.environment.instance.type === "review" &&
20935
+ (job.stage === "deploy" ||
20936
+ job.stage === "verify" ||
20937
+ ((_a = job.provides) !== null && _a !== void 0 ? _a : []).includes("dockerImage"));
20938
+ };
20939
+ exports.isReviewDeliveryJob = isReviewDeliveryJob;
20940
+ //# sourceMappingURL=reviewApps.js.map
20941
+
20942
+ /***/ }),
20943
+
20944
+ /***/ 49821:
20945
+ /***/ ((__unused_webpack_module, exports) => {
20946
+
20947
+ "use strict";
20948
+
20949
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
20950
+ exports.shouldAutoDeployReviewApps = exports.DEFAULT_NO_REVIEW_APP_LABEL = exports.DEFAULT_REVIEW_APP_LABEL = void 0;
20951
+ exports.DEFAULT_REVIEW_APP_LABEL = "catladder:review-app";
20952
+ exports.DEFAULT_NO_REVIEW_APP_LABEL = "catladder:no-review-app";
20953
+ /**
20954
+ * whether the review apps of an MR/PR in the given state deploy
20955
+ * automatically. An explicit request (playing the gate job, dispatching
20956
+ * the deploy workflow) always deploys and doesn't consult this.
20957
+ */
20958
+ const shouldAutoDeployReviewApps = (config, { labels, draft }) => {
20959
+ if (draft && config.drafts !== "likeReady") {
20960
+ return { deploy: false, reason: "drafts don't deploy review apps" };
20961
+ }
20962
+ switch (config.deploy) {
20963
+ case "auto":
20964
+ return { deploy: true, reason: "review apps deploy automatically" };
20965
+ case "manual":
20966
+ return {
20967
+ deploy: false,
20968
+ reason: "review apps only deploy on request (deploy: manual)",
20969
+ };
20970
+ case "optIn":
20971
+ return labels.includes(config.label)
20972
+ ? { deploy: true, reason: `the '${config.label}' label is set` }
20973
+ : { deploy: false, reason: `the '${config.label}' label is not set` };
20974
+ case "optOut":
20975
+ return labels.includes(config.skipLabel)
20976
+ ? { deploy: false, reason: `the '${config.skipLabel}' label is set` }
20977
+ : {
20978
+ deploy: true,
20979
+ reason: `the '${config.skipLabel}' label is not set`,
20980
+ };
20981
+ }
20982
+ };
20983
+ exports.shouldAutoDeployReviewApps = shouldAutoDeployReviewApps;
20984
+ //# sourceMappingURL=reviewAppsPolicy.js.map
20985
+
20986
+ /***/ }),
20987
+
20006
20988
  /***/ 78503:
20007
20989
  /***/ ((__unused_webpack_module, exports) => {
20008
20990