@catladder/cli 5.5.0 → 5.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/bundles/catenv/index.js +364 -45
  2. package/dist/bundles/catenv/xdg-open +0 -0
  3. package/dist/bundles/cli/clipboard_i686.exe +0 -0
  4. package/dist/bundles/cli/clipboard_x86_64.exe +0 -0
  5. package/dist/bundles/cli/index.js +364 -45
  6. package/dist/bundles/cli/xdg-open +0 -0
  7. package/dist/bundles/cli/xsel +0 -0
  8. package/dist/bundles/runner-images/changesets/scripts/changesetsRelease +0 -0
  9. package/dist/bundles/runner-images/kubernetes/scripts/kubernetesCreateSecret +0 -0
  10. package/dist/bundles/runner-images/kubernetes/scripts/kubernetesDelete +0 -0
  11. package/dist/bundles/runner-images/kubernetes/scripts/kubernetesDeploy +0 -0
  12. package/dist/bundles/runner-images/kubernetes/scripts/kubernetesRollback +0 -0
  13. package/dist/bundles/skills/catladder-builds/SKILL.md +50 -2
  14. package/dist/bundles/skills/catladder-builds/references/build-types.md +10 -2
  15. package/dist/bundles/skills/catladder-pipelines/SKILL.md +18 -2
  16. package/dist/packages/pipeline/src/backends/github/artifactArchive.d.ts +22 -0
  17. package/dist/packages/pipeline/src/backends/github/artifactArchive.js +84 -0
  18. package/dist/packages/pipeline/src/backends/github/artifactArchive.js.map +1 -0
  19. package/dist/packages/pipeline/src/backends/github/createGithubJobs.js +12 -29
  20. package/dist/packages/pipeline/src/backends/github/createGithubJobs.js.map +1 -1
  21. package/dist/packages/pipeline/src/backends/gitlab/GitlabBackend.js +3 -1
  22. package/dist/packages/pipeline/src/backends/gitlab/GitlabBackend.js.map +1 -1
  23. package/dist/packages/pipeline/src/backends/gitlab/getPipelineStages.d.ts +4 -1
  24. package/dist/packages/pipeline/src/backends/gitlab/getPipelineStages.js +11 -2
  25. package/dist/packages/pipeline/src/backends/gitlab/getPipelineStages.js.map +1 -1
  26. package/dist/packages/pipeline/src/build/types.d.ts +4 -3
  27. package/dist/packages/pipeline/src/build/types.js.map +1 -1
  28. package/dist/packages/pipeline/src/deploy/base/deploy.js +15 -9
  29. package/dist/packages/pipeline/src/deploy/base/deploy.js.map +1 -1
  30. package/dist/packages/pipeline/src/index.d.ts +1 -0
  31. package/dist/packages/pipeline/src/index.js +1 -0
  32. package/dist/packages/pipeline/src/index.js.map +1 -1
  33. package/dist/packages/pipeline/src/pipeline/createJobsForComponent.js +12 -2
  34. package/dist/packages/pipeline/src/pipeline/createJobsForComponent.js.map +1 -1
  35. package/dist/packages/pipeline/src/pipeline/createJobsForWorkspace.js +7 -2
  36. package/dist/packages/pipeline/src/pipeline/createJobsForWorkspace.js.map +1 -1
  37. package/dist/packages/pipeline/src/pipeline/resolveRequirements.js +1 -0
  38. package/dist/packages/pipeline/src/pipeline/resolveRequirements.js.map +1 -1
  39. package/dist/packages/pipeline/src/postBuildTest/createPostBuildTestJobs.d.ts +16 -0
  40. package/dist/packages/pipeline/src/postBuildTest/createPostBuildTestJobs.js +167 -0
  41. package/dist/packages/pipeline/src/postBuildTest/createPostBuildTestJobs.js.map +1 -0
  42. package/dist/packages/pipeline/src/postBuildTest/index.d.ts +2 -0
  43. package/dist/packages/pipeline/src/postBuildTest/index.js +19 -0
  44. package/dist/packages/pipeline/src/postBuildTest/index.js.map +1 -0
  45. package/dist/packages/pipeline/src/postBuildTest/types.d.ts +61 -0
  46. package/dist/packages/pipeline/src/postBuildTest/types.js +3 -0
  47. package/dist/packages/pipeline/src/postBuildTest/types.js.map +1 -0
  48. package/dist/packages/pipeline/src/types/jobs.d.ts +2 -2
  49. package/dist/packages/pipeline/src/types/jobs.js +1 -0
  50. package/dist/packages/pipeline/src/types/jobs.js.map +1 -1
  51. package/dist/runner-images/changesets/scripts/changesetsRelease +0 -0
  52. package/dist/runner-images/kubernetes/scripts/kubernetesCreateSecret +0 -0
  53. package/dist/runner-images/kubernetes/scripts/kubernetesDelete +0 -0
  54. package/dist/runner-images/kubernetes/scripts/kubernetesDeploy +0 -0
  55. package/dist/runner-images/kubernetes/scripts/kubernetesRollback +0 -0
  56. package/dist/skills/catladder-builds/SKILL.md +50 -2
  57. package/dist/skills/catladder-builds/references/build-types.md +10 -2
  58. package/dist/skills/catladder-pipelines/SKILL.md +18 -2
  59. package/dist/tsconfig.tsbuildinfo +1 -1
  60. package/package.json +1 -1
@@ -9679,6 +9679,97 @@ exports.makeAggregateCheckJob = makeAggregateCheckJob;
9679
9679
 
9680
9680
  /***/ }),
9681
9681
 
9682
+ /***/ 75125:
9683
+ /***/ ((__unused_webpack_module, exports) => {
9684
+
9685
+ "use strict";
9686
+
9687
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
9688
+ exports.createArtifactDownloadSteps = exports.createArtifactUploadSteps = exports.artifactArchiveFile = void 0;
9689
+ /**
9690
+ * Artifacts are handed between github jobs as ONE tar archive instead
9691
+ * of letting upload-artifact walk the paths itself.
9692
+ *
9693
+ * upload-artifact does not preserve symlinks (it uploads the target's
9694
+ * content) nor file modes, and it strips the paths' common ancestor.
9695
+ * Gitlab artifacts keep all of that, and builds rely on it: next.js
9696
+ * with turbopack writes `.next/node_modules/<pkg>-<hash>` as relative
9697
+ * symlinks into the pnpm virtual store. Copied as plain directories,
9698
+ * node resolves the package's own dependencies from the wrong place
9699
+ * and the app crashes with ERR_MODULE_NOT_FOUND at runtime. tar keeps
9700
+ * symlinks, modes and the paths relative to the repo root (github's
9701
+ * own docs recommend it for exactly this).
9702
+ *
9703
+ * The archive is not hidden (upload-artifact skips hidden files) and
9704
+ * lives in the workspace, the one path run steps and actions agree on
9705
+ * inside job containers too.
9706
+ */
9707
+ const artifactArchiveFile = (jobId) => `catladder-artifacts-${jobId}.tar`;
9708
+ exports.artifactArchiveFile = artifactArchiveFile;
9709
+ const createArtifactUploadSteps = (jobId, paths, condition) => {
9710
+ const archive = (0, exports.artifactArchiveFile)(jobId);
9711
+ const ifCondition = condition ? { if: condition } : {};
9712
+ return [
9713
+ {
9714
+ name: "Archive artifacts",
9715
+ ...ifCondition,
9716
+ run: [
9717
+ "shopt -s nullglob globstar",
9718
+ "paths=()",
9719
+ // unquoted on purpose: artifact paths may be globs. Non-matching
9720
+ // globs vanish (nullglob), missing literal paths are skipped —
9721
+ // gitlab only warns about both, so do we
9722
+ `for path in ${paths.join(" ")}; do`,
9723
+ ' if [ -e "$path" ] || [ -L "$path" ]; then paths+=("$path"); else echo "::warning::artifact path not found: $path"; fi',
9724
+ "done",
9725
+ "if [ ${#paths[@]} -eq 0 ]; then",
9726
+ // an empty archive keeps downstream downloads from failing.
9727
+ // Written by hand: busybox tar (alpine job images) refuses to
9728
+ // create one, and zero blocks are what an empty tar is
9729
+ ` head -c 10240 /dev/zero > ${archive}`,
9730
+ "else",
9731
+ ` tar -cf ${archive} -- "\${paths[@]}"`,
9732
+ "fi",
9733
+ ].join("\n"),
9734
+ shell: "bash",
9735
+ },
9736
+ {
9737
+ name: "Upload artifacts",
9738
+ ...ifCondition,
9739
+ uses: "actions/upload-artifact@v4",
9740
+ with: {
9741
+ name: jobId,
9742
+ path: archive,
9743
+ "if-no-files-found": "error",
9744
+ },
9745
+ },
9746
+ ];
9747
+ };
9748
+ exports.createArtifactUploadSteps = createArtifactUploadSteps;
9749
+ const createArtifactDownloadSteps = (providerIds) => providerIds.length === 0
9750
+ ? []
9751
+ : [
9752
+ ...providerIds.map((id) => ({
9753
+ name: `Download artifacts from ${id}`,
9754
+ uses: "actions/download-artifact@v4",
9755
+ with: { name: id },
9756
+ })),
9757
+ {
9758
+ name: "Extract artifacts",
9759
+ run: providerIds
9760
+ .flatMap((id) => [
9761
+ `tar -xf ${(0, exports.artifactArchiveFile)(id)}`,
9762
+ `rm ${(0, exports.artifactArchiveFile)(id)}`,
9763
+ ])
9764
+ .join("\n"),
9765
+ shell: "bash",
9766
+ },
9767
+ ];
9768
+ exports.createArtifactDownloadSteps = createArtifactDownloadSteps;
9769
+ //# sourceMappingURL=artifactArchive.js.map
9770
+
9771
+ /***/ }),
9772
+
9682
9773
  /***/ 17300:
9683
9774
  /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
9684
9775
 
@@ -9748,6 +9839,7 @@ const utils_1 = __nccwpck_require__(49401);
9748
9839
  const gitlab_1 = __nccwpck_require__(86962);
9749
9840
  const scriptFunctions_1 = __nccwpck_require__(85873);
9750
9841
  const ciVariables_1 = __nccwpck_require__(17300);
9842
+ const artifactArchive_1 = __nccwpck_require__(75125);
9751
9843
  /**
9752
9844
  * runner variables that only make sense on gitlab runners and must not
9753
9845
  * leak into github jobs (docker-in-docker daemon config, kubernetes
@@ -9958,7 +10050,7 @@ const getReferencedSecretsEnv = (script, kinds) => Object.fromEntries([...script
9958
10050
  .filter((name) => !ciVariables_1.GITHUB_INJECTED_WORKFLOW_ENV_NAMES.has(name))
9959
10051
  .map((name) => [name, vaultValueExpression(name, kinds)]));
9960
10052
  const makeGithubJob = (context, job, allJobs, uploadProviderIds, images, scripts, secretKinds = new Map()) => {
9961
- var _a, _b, _c, _d, _e, _f, _g, _h, _j;
10053
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
9962
10054
  const id = (0, exports.githubJobId)(context, job.name);
9963
10055
  const isComponent = context.type === "component";
9964
10056
  const createsJobEnv = !!job.environment &&
@@ -10103,13 +10195,7 @@ const makeGithubJob = (context, job, allJobs, uploadProviderIds, images, scripts
10103
10195
  ? []
10104
10196
  : [{ name: "Checkout", uses: "actions/checkout@v4" }]),
10105
10197
  ...createCacheSteps(context, job),
10106
- ...needs
10107
- .filter((n) => n.artifacts)
10108
- .map((n) => ({
10109
- name: `Download artifacts from ${n.id}`,
10110
- uses: "actions/download-artifact@v4",
10111
- with: { name: n.id },
10112
- })),
10198
+ ...(0, artifactArchive_1.createArtifactDownloadSteps)(needs.filter((n) => n.artifacts).map((n) => n.id)),
10113
10199
  {
10114
10200
  name: job.name,
10115
10201
  id: "main",
@@ -10130,27 +10216,15 @@ const makeGithubJob = (context, job, allJobs, uploadProviderIds, images, scripts
10130
10216
  },
10131
10217
  ]
10132
10218
  : []),
10133
- ...(pagesPublishDir
10134
- ? []
10135
- : uploadProviderIds.has(id) || artifactPaths.length > 0
10136
- ? artifactPaths.length > 0
10137
- ? [
10138
- {
10139
- name: "Upload artifacts",
10140
- uses: "actions/upload-artifact@v4",
10141
- with: {
10142
- name: id,
10143
- path: artifactPaths.join("\n"),
10144
- "if-no-files-found": "warn",
10145
- // gitlab artifacts include everything; upload-artifact
10146
- // silently drops hidden files by default — which is
10147
- // exactly where next.js builds live (.next)
10148
- "include-hidden-files": true,
10149
- },
10150
- },
10151
- ]
10152
- : []
10153
- : []),
10219
+ ...(!pagesPublishDir && artifactPaths.length > 0
10220
+ ? (0, artifactArchive_1.createArtifactUploadSteps)(id, artifactPaths,
10221
+ // gitlab's `artifacts.when`: steps only run on success by default
10222
+ ((_k = job.artifacts) === null || _k === void 0 ? void 0 : _k.when) === "always"
10223
+ ? "always()"
10224
+ : ((_l = job.artifacts) === null || _l === void 0 ? void 0 : _l.when) === "on_failure"
10225
+ ? "failure()"
10226
+ : undefined)
10227
+ : []),
10154
10228
  ],
10155
10229
  };
10156
10230
  return [id, githubJob];
@@ -10843,7 +10917,6 @@ class GitlabBackend {
10843
10917
  return [mainInclude, ...componentIncludes];
10844
10918
  }
10845
10919
  async createPipeline(config, images) {
10846
- const stages = (0, getPipelineStages_1.getPipelineStages)(config);
10847
10920
  // register the release image first: the per-trigger job creation
10848
10921
  // below emits the build jobs of all images registered so far. The
10849
10922
  // release jobs themselves are created afterwards — they need the
@@ -10862,6 +10935,9 @@ class GitlabBackend {
10862
10935
  jobs: await (0, createGitlabJobs_1.createGitlabJobs)(await (0, createAllJobs_1.createAllJobs)({ config, trigger, pipelineType: this.type }), images, getGitlabRulesForTrigger(trigger)),
10863
10936
  })));
10864
10937
  const allJobsPerTrigger = jobsPerTrigger.flatMap(({ jobs }) => jobs);
10938
+ const stages = (0, getPipelineStages_1.getPipelineStages)(config, new Set(allJobsPerTrigger
10939
+ .map(({ gitlabJob }) => gitlabJob.stage)
10940
+ .filter((stage) => typeof stage === "string")));
10865
10941
  // the jobs the queued-release executor waits for: everything that
10866
10942
  // runs automatically in a main-branch pipeline. Manual jobs (stop,
10867
10943
  // rollback, gates) are excluded — gitlab skips a job whose needs
@@ -11516,14 +11592,23 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
11516
11592
  exports.getPipelineStages = void 0;
11517
11593
  const config_1 = __nccwpck_require__(11646);
11518
11594
  const jobs_1 = __nccwpck_require__(98521);
11595
+ /**
11596
+ * stages that only appear in the pipeline when a job uses them, so that
11597
+ * projects not using the feature keep their generated files unchanged
11598
+ */
11599
+ const OPTIONAL_STAGES = ["post-build"];
11519
11600
  /**
11520
11601
  * while technically not required, we group different envs in its own stage
11521
11602
  * each job from `createJobs` that is defined as `envMode: "stagePerEnv"` will have `deploy dev`, etc. instead of just `deploy`
11522
11603
  * this is just so that it looks nicer in gitlab and makes running mutliple manual tasks more easy to use
11604
+ *
11605
+ * `usedStages` are the stages of all generated jobs; optional stages are
11606
+ * dropped when none of them is used.
11523
11607
  */
11524
- function getPipelineStages(config) {
11608
+ function getPipelineStages(config, usedStages = new Set()) {
11525
11609
  const allEnvs = (0, config_1.getAllEnvsInAllComponents)(config).filter((e) => e !== "local");
11526
- const stages = jobs_1.BASE_STAGES.reduce((acc, baseStage) => [
11610
+ const isUsed = (baseStage) => [...usedStages].some((stage) => stage === baseStage || stage.startsWith(`${baseStage} `));
11611
+ const stages = jobs_1.BASE_STAGES.filter((baseStage) => !OPTIONAL_STAGES.includes(baseStage) || isUsed(baseStage)).reduce((acc, baseStage) => [
11527
11612
  ...acc,
11528
11613
  baseStage,
11529
11614
  ...allEnvs.map((e) => `${baseStage} ${e}`),
@@ -15607,14 +15692,20 @@ class DeployJob extends jobs_1.CatladderJob {
15607
15692
  : []),
15608
15693
  ];
15609
15694
  // we don't want to deploy when there is a broken test
15610
- const qualityGateRequirement = {
15611
- capability: "qualityGate",
15612
- artifacts: false,
15613
- // use test from workspace build
15614
- from: (0, context_1.componentContextHasWorkspaceBuild)(context)
15615
- ? { workspace: context.build.workspaceName }
15616
- : undefined,
15617
- };
15695
+ const qualityGateRequirements = [
15696
+ // the component's own gates (e.g. post-build tests, custom test jobs)
15697
+ { capability: "qualityGate", artifacts: false },
15698
+ // ...and the gates of the workspace build it is built in
15699
+ ...((0, context_1.componentContextHasWorkspaceBuild)(context)
15700
+ ? [
15701
+ {
15702
+ capability: "qualityGate",
15703
+ artifacts: false,
15704
+ from: { workspace: context.build.workspaceName },
15705
+ },
15706
+ ]
15707
+ : []),
15708
+ ];
15618
15709
  super({
15619
15710
  name: exports.DEPLOY_JOB_NAME,
15620
15711
  script: jobDefinition.script,
@@ -15631,7 +15722,7 @@ class DeployJob extends jobs_1.CatladderJob {
15631
15722
  ...waitForRequirements,
15632
15723
  // if the build is disabled, we don't need to wait for it
15633
15724
  ...(context.build.type !== "disabled"
15634
- ? [...buildRequirements, qualityGateRequirement]
15725
+ ? [...buildRequirements, ...qualityGateRequirements]
15635
15726
  : []),
15636
15727
  ],
15637
15728
  gate: whenDeploy,
@@ -18407,6 +18498,7 @@ __exportStar(__nccwpck_require__(15707), exports);
18407
18498
  __exportStar(__nccwpck_require__(45492), exports);
18408
18499
  __exportStar(__nccwpck_require__(3659), exports);
18409
18500
  __exportStar(__nccwpck_require__(50713), exports);
18501
+ __exportStar(__nccwpck_require__(28458), exports);
18410
18502
  __exportStar(__nccwpck_require__(26041), exports);
18411
18503
  __exportStar(__nccwpck_require__(47663), exports);
18412
18504
  __exportStar(__nccwpck_require__(91804), exports);
@@ -19004,6 +19096,7 @@ exports.createJobsForComponentContext = void 0;
19004
19096
  const build_1 = __nccwpck_require__(45492);
19005
19097
  const deploy_1 = __nccwpck_require__(3659);
19006
19098
  const jobs_1 = __nccwpck_require__(98521);
19099
+ const createPostBuildTestJobs_1 = __nccwpck_require__(61844);
19007
19100
  const createVerifyJobs_1 = __nccwpck_require__(66900);
19008
19101
  const injectDefaultVarsInCustomJobs = (context, jobs) => jobs.map(({ variables, ...job }) => {
19009
19102
  var _a;
@@ -19023,17 +19116,26 @@ const getCustomJobs = (context) => {
19023
19116
  return injectDefaultVarsInCustomJobs(context, rawJobs);
19024
19117
  };
19025
19118
  const createJobsForComponentContext = async (context) => {
19026
- const [buildJobs, deployJobs, verifyJobs] = await Promise.all([
19119
+ const [buildJobs, postBuildTestJobs, deployJobs, verifyJobs] = await Promise.all([
19027
19120
  context.build.type !== "disabled"
19028
19121
  ? build_1.BUILD_TYPES[context.build.buildType].jobs(context)
19029
19122
  : [],
19123
+ (0, createPostBuildTestJobs_1.createPostBuildTestJobs)(context),
19030
19124
  context.componentConfig.deploy !== false
19031
19125
  ? deploy_1.DEPLOY_TYPES[context.componentConfig.deploy.type].jobs(context)
19032
19126
  : [],
19033
19127
  (0, createVerifyJobs_1.createVerifyJobs)(context),
19034
19128
  ]);
19035
19129
  const customJobs = getCustomJobs(context);
19036
- return [...buildJobs, ...deployJobs, ...verifyJobs, ...customJobs];
19130
+ const otherJobs = [...buildJobs, ...deployJobs, ...verifyJobs, ...customJobs];
19131
+ (0, createPostBuildTestJobs_1.assertUniquePostBuildTestNames)(context, postBuildTestJobs, otherJobs);
19132
+ return [
19133
+ ...buildJobs,
19134
+ ...postBuildTestJobs,
19135
+ ...deployJobs,
19136
+ ...verifyJobs,
19137
+ ...customJobs,
19138
+ ];
19037
19139
  };
19038
19140
  exports.createJobsForComponentContext = createJobsForComponentContext;
19039
19141
  //# sourceMappingURL=createJobsForComponent.js.map
@@ -19048,9 +19150,14 @@ exports.createJobsForComponentContext = createJobsForComponentContext;
19048
19150
  Object.defineProperty(exports, "__esModule", ({ value: true }));
19049
19151
  exports.createJobsForWorkspace = void 0;
19050
19152
  const build_1 = __nccwpck_require__(45492);
19153
+ const createPostBuildTestJobs_1 = __nccwpck_require__(61844);
19051
19154
  const createJobsForWorkspace = async (context) => {
19052
- const buildJobs = await build_1.WORKSPACE_BUILD_TYPES[context.build.buildType].jobs(context);
19053
- return buildJobs;
19155
+ const [buildJobs, postBuildTestJobs] = await Promise.all([
19156
+ build_1.WORKSPACE_BUILD_TYPES[context.build.buildType].jobs(context),
19157
+ (0, createPostBuildTestJobs_1.createPostBuildTestJobs)(context),
19158
+ ]);
19159
+ (0, createPostBuildTestJobs_1.assertUniquePostBuildTestNames)(context, postBuildTestJobs, buildJobs);
19160
+ return [...buildJobs, ...postBuildTestJobs];
19054
19161
  };
19055
19162
  exports.createJobsForWorkspace = createJobsForWorkspace;
19056
19163
  //# sourceMappingURL=createJobsForWorkspace.js.map
@@ -19519,6 +19626,7 @@ exports.resolveRequirements = void 0;
19519
19626
  const DEFAULT_STAGE_CAPABILITIES = {
19520
19627
  build: "build",
19521
19628
  test: "qualityGate",
19629
+ "post-build": "qualityGate",
19522
19630
  deploy: "deployment",
19523
19631
  };
19524
19632
  /**
@@ -19660,6 +19768,216 @@ exports.getWorkspaceDependencies = getWorkspaceDependencies;
19660
19768
 
19661
19769
  /***/ }),
19662
19770
 
19771
+ /***/ 61844:
19772
+ /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
19773
+
19774
+ "use strict";
19775
+
19776
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
19777
+ exports.assertUniquePostBuildTestNames = exports.createPostBuildTestJobs = exports.postBuildTestJobName = exports.POST_BUILD_TEST_STAGE = void 0;
19778
+ const createArtifactsConfig_1 = __nccwpck_require__(14149);
19779
+ const cache_1 = __nccwpck_require__(74559);
19780
+ const constants_1 = __nccwpck_require__(5576);
19781
+ const packageManagerInstall_1 = __nccwpck_require__(28419);
19782
+ const types_1 = __nccwpck_require__(35249);
19783
+ const runner_1 = __nccwpck_require__(92146);
19784
+ const jobs_1 = __nccwpck_require__(98521);
19785
+ const utils_1 = __nccwpck_require__(49401);
19786
+ exports.POST_BUILD_TEST_STAGE = "post-build";
19787
+ const postBuildTestJobName = (name) => `🔬 ${name}`;
19788
+ exports.postBuildTestJobName = postBuildTestJobName;
19789
+ const getPostBuildTestsConfig = (context) => {
19790
+ if (context.type === "workspace") {
19791
+ return context.build.config.postBuildTests;
19792
+ }
19793
+ if (context.build.type === "disabled") {
19794
+ return undefined;
19795
+ }
19796
+ return context.build.config.postBuildTests;
19797
+ };
19798
+ /**
19799
+ * whether the build output is a node project, so the job has to install
19800
+ * the dependencies before running the tests
19801
+ */
19802
+ const isNodeBuild = (context) => {
19803
+ if (context.type === "workspace") {
19804
+ return true; // workspace builds are node builds
19805
+ }
19806
+ if (context.build.type === "fromWorkspace") {
19807
+ return true;
19808
+ }
19809
+ if (context.build.type === "standalone") {
19810
+ return (0, types_1.isOfBuildType)(context.build.config, "node", "node-static", "storybook", "meteor");
19811
+ }
19812
+ return false;
19813
+ };
19814
+ const hasBuildCommand = (buildCommand) => buildCommand !== null && buildCommand !== false;
19815
+ /**
19816
+ * whether the build creates an app build job with artifacts. Rails builds
19817
+ * and builds with `buildCommand: false` only build the docker image.
19818
+ */
19819
+ const hasBuildArtifacts = (context) => {
19820
+ if (context.type === "workspace") {
19821
+ return hasBuildCommand(context.build.config.buildCommand);
19822
+ }
19823
+ if (context.build.type === "fromWorkspace") {
19824
+ return hasBuildCommand(context.build.workspaceBuildConfig.buildCommand);
19825
+ }
19826
+ if (context.build.type === "standalone") {
19827
+ return (!(0, types_1.isOfBuildType)(context.build.config, "rails") &&
19828
+ hasBuildCommand(context.build.config.buildCommand));
19829
+ }
19830
+ return false;
19831
+ };
19832
+ const getDefaultImage = (context) => {
19833
+ if (context.type === "component" &&
19834
+ context.build.type === "standalone" &&
19835
+ (0, types_1.isOfBuildType)(context.build.config, "custom")) {
19836
+ // custom builds bring their own toolchain image
19837
+ return context.build.config.jobImage;
19838
+ }
19839
+ return (0, runner_1.getRunnerImage)("jobs-default");
19840
+ };
19841
+ /**
19842
+ * jobs for `postBuildTests`: they run after the build in the `post-build`
19843
+ * stage, against the build artifacts, and act as quality gates, so a
19844
+ * failing post-build test blocks the deploy.
19845
+ */
19846
+ const createPostBuildTestJobs = async (context) => {
19847
+ var _a;
19848
+ // like lint and test: tagged releases deploy what was already tested
19849
+ if (context.trigger === "taggedRelease") {
19850
+ return [];
19851
+ }
19852
+ const tests = Object.entries((_a = getPostBuildTestsConfig(context)) !== null && _a !== void 0 ? _a : {}).filter((entry) => entry[1] !== false);
19853
+ if (tests.length === 0) {
19854
+ return [];
19855
+ }
19856
+ const nodeBuild = isNodeBuild(context);
19857
+ const [packageManagerInstall, nodeCache] = nodeBuild
19858
+ ? await Promise.all([
19859
+ (0, packageManagerInstall_1.getPackageManagerInstall)(context),
19860
+ // pull-only: the build job is the designated cache writer
19861
+ (0, cache_1.getNodeCache)(context, "pull"),
19862
+ ])
19863
+ : [null, null];
19864
+ // components built in a workspace test the workspace build output
19865
+ const from = context.type === "component" && context.build.type === "fromWorkspace"
19866
+ ? { workspace: context.build.workspaceName }
19867
+ : undefined;
19868
+ const buildRequirement = hasBuildArtifacts(context)
19869
+ ? { capability: "buildArtifacts", artifacts: true, strict: true, from }
19870
+ : // no build artifacts (e.g. rails): run after the build (docker image)
19871
+ { capability: "build", artifacts: false, from };
19872
+ return tests.map(([name, test]) => {
19873
+ var _a, _b, _c;
19874
+ const artifactsConfig = (0, createArtifactsConfig_1.createArtifactsConfig)(context.build.dir, test.artifactsReports, test.artifacts);
19875
+ return new jobs_1.CatladderJob({
19876
+ name: (0, exports.postBuildTestJobName)(name),
19877
+ stage: exports.POST_BUILD_TEST_STAGE,
19878
+ envMode: "stagePerEnv",
19879
+ provides: ["qualityGate"],
19880
+ needs: [],
19881
+ requires: [buildRequirement],
19882
+ image: (_a = test.jobImage) !== null && _a !== void 0 ? _a : getDefaultImage(context),
19883
+ services: test.services,
19884
+ // neutral cache declarations, lowered by each backend
19885
+ caches: nodeCache !== null && nodeCache !== void 0 ? nodeCache : undefined,
19886
+ variables: {
19887
+ APP_PATH: context.build.dir,
19888
+ ...(context.type === "component"
19889
+ ? context.environment.jobOnlyVars.build.envVars
19890
+ : {}),
19891
+ ...((_b = test.vars) !== null && _b !== void 0 ? _b : {}),
19892
+ },
19893
+ runnerVariables: {
19894
+ ...(nodeBuild ? constants_1.NODE_RUNNER_BUILD_VARIABLES : {}),
19895
+ ...((_c = test.runnerVariables) !== null && _c !== void 0 ? _c : {}),
19896
+ },
19897
+ script: [
19898
+ ...(nodeBuild ? (0, packageManagerInstall_1.ensureNodeVersion)(context) : []),
19899
+ `cd ${context.build.dir}`,
19900
+ ...(packageManagerInstall !== null && packageManagerInstall !== void 0 ? packageManagerInstall : []),
19901
+ ...(0, utils_1.ensureArray)(test.command),
19902
+ ],
19903
+ allow_failure: test.allowFailure,
19904
+ // reports and screenshots are most needed when the tests fail
19905
+ ...(artifactsConfig
19906
+ ? {
19907
+ artifacts: {
19908
+ when: "always",
19909
+ ...artifactsConfig.artifacts,
19910
+ },
19911
+ }
19912
+ : {}),
19913
+ });
19914
+ });
19915
+ };
19916
+ exports.createPostBuildTestJobs = createPostBuildTestJobs;
19917
+ const jobIdOf = (jobName) => jobName
19918
+ .toLowerCase()
19919
+ .replace(/[^a-z0-9]+/g, "-")
19920
+ .replace(/^-+|-+$/g, "");
19921
+ /**
19922
+ * post-build test names are chosen by the user. Github job ids drop the
19923
+ * emoji of the job name, so e.g. a post-build test named `test` would
19924
+ * clash with the `🧪 test` job.
19925
+ */
19926
+ const assertUniquePostBuildTestNames = (context, postBuildTestJobs, otherJobs) => {
19927
+ const seenIds = new Set(otherJobs.map((job) => jobIdOf(job.name)));
19928
+ // also catches two post-build tests that only differ in case or
19929
+ // punctuation (e.g. `e2e` and `E2E`)
19930
+ const clash = postBuildTestJobs.find((job) => {
19931
+ const id = jobIdOf(job.name);
19932
+ const isClash = seenIds.has(id);
19933
+ seenIds.add(id);
19934
+ return isClash;
19935
+ });
19936
+ if (clash) {
19937
+ throw new Error(`${context.name}: post-build test '${clash.name}' clashes with another job of the same name, please rename it in postBuildTests`);
19938
+ }
19939
+ };
19940
+ exports.assertUniquePostBuildTestNames = assertUniquePostBuildTestNames;
19941
+ //# sourceMappingURL=createPostBuildTestJobs.js.map
19942
+
19943
+ /***/ }),
19944
+
19945
+ /***/ 28458:
19946
+ /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
19947
+
19948
+ "use strict";
19949
+
19950
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
19951
+ if (k2 === undefined) k2 = k;
19952
+ var desc = Object.getOwnPropertyDescriptor(m, k);
19953
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
19954
+ desc = { enumerable: true, get: function() { return m[k]; } };
19955
+ }
19956
+ Object.defineProperty(o, k2, desc);
19957
+ }) : (function(o, m, k, k2) {
19958
+ if (k2 === undefined) k2 = k;
19959
+ o[k2] = m[k];
19960
+ }));
19961
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
19962
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
19963
+ };
19964
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
19965
+ __exportStar(__nccwpck_require__(92967), exports);
19966
+ __exportStar(__nccwpck_require__(61844), exports);
19967
+ //# sourceMappingURL=index.js.map
19968
+
19969
+ /***/ }),
19970
+
19971
+ /***/ 92967:
19972
+ /***/ ((__unused_webpack_module, exports) => {
19973
+
19974
+ "use strict";
19975
+
19976
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
19977
+ //# sourceMappingURL=types.js.map
19978
+
19979
+ /***/ }),
19980
+
19663
19981
  /***/ 26041:
19664
19982
  /***/ ((__unused_webpack_module, exports) => {
19665
19983
 
@@ -19993,6 +20311,7 @@ exports.BASE_STAGES = [
19993
20311
  "setup",
19994
20312
  "test",
19995
20313
  "build",
20314
+ "post-build",
19996
20315
  "deploy",
19997
20316
  "verify",
19998
20317
  "agents",
File without changes
File without changes
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: catladder-builds
3
- description: Configuring how a component is built in a catladder project — the `build` config in catladder.ts (build types node / rails / meteor / custom, build & start commands, artifacts, caching, lint/test/audit jobs, the Docker image, project-declared job images, and shared workspace builds). Use when adding or changing a component's build, choosing a build type, wiring a Dockerfile/nginx image, declaring a custom CI job image (`images` config), tuning the build/test/lint jobs, or setting up a monorepo workspace build. Triggers on "build config", "buildCommand", "build type", "Dockerfile", "docker image", "job image", "jobImage", "custom image", "project image", "rails build", "monorepo build", "artifacts".
3
+ description: Configuring how a component is built in a catladder project — the `build` config in catladder.ts (build types node / rails / meteor / custom, build & start commands, artifacts, caching, lint/test/audit jobs, the Docker image, project-declared job images, and shared workspace builds). Use when adding or changing a component's build, choosing a build type, wiring a Dockerfile/nginx image, declaring a custom CI job image (`images` config), tuning the build/test/lint jobs, adding e2e tests against the build (`postBuildTests`), or setting up a monorepo workspace build. Triggers on "build config", "postBuildTests", "post-build test", "e2e", "playwright", "buildCommand", "build type", "Dockerfile", "docker image", "job image", "jobImage", "custom image", "project image", "rails build", "monorepo build", "artifacts".
4
4
  ---
5
5
 
6
6
  # Component builds with catladder
@@ -66,6 +66,10 @@ type: `build: { from: "web" }` (see workspace builds below).
66
66
  won't run).
67
67
  - `lint` / `test` / `audit` — customize (`{ command, jobImage, … }`) or
68
68
  set to `false` to disable that job.
69
+ - `postBuildTests` — named tests against the **build output** (e.g. an
70
+ e2e suite against the production build with a Postgres service). Each
71
+ entry becomes a job in the `post-build` stage between build and deploy
72
+ and blocks the deploy when it fails (see below).
69
73
  - `artifactsPaths` / `artifactsExcludePaths` — extra build artifacts
70
74
  (`dist` and `.next` are always included).
71
75
  - `cache` — build caching (see the `catladder-pipelines` skill for the
@@ -82,11 +86,55 @@ type: `build: { from: "web" }` (see workspace builds below).
82
86
  (`{ type: "nginx" | "node" | "meteor" }`) or
83
87
  `{ type: "custom" }` (expects a `Dockerfile`).
84
88
 
89
+ ## Post-build tests (e2e against the build)
90
+
91
+ `lint`/`test` run against the sources, in parallel with the build. Tests
92
+ that need the built app go into `postBuildTests`:
93
+
94
+ ```ts
95
+ build: {
96
+ type: "node",
97
+ postBuildTests: {
98
+ e2e: {
99
+ command: "pnpm test:e2e", // runs in the build dir
100
+ jobImage: "mcr.microsoft.com/playwright:v1.56.0-noble",
101
+ services: [{ name: "postgres:17", alias: "postgres",
102
+ variables: { POSTGRES_PASSWORD: "postgres" } }],
103
+ vars: { DATABASE_URL: "postgres://postgres:postgres@postgres:5432/postgres" },
104
+ artifacts: { paths: ["apps/web/playwright-report"] }, // repo-root relative
105
+ },
106
+ },
107
+ }
108
+ ```
109
+
110
+ - The job (`🔬 e2e`, stage `post-build <env>`) downloads the build
111
+ artifacts (`.next`, `dist`, `artifactsPaths`), installs dependencies
112
+ for node builds, then runs `command`. The command starts the app
113
+ itself, e.g. playwright `webServer: { command: "pnpm start" }`.
114
+ Builds without artifacts (`rails`, `buildCommand: false`) just run
115
+ it after the build.
116
+ - It is a quality gate: the env's deploy waits for it; on github it is
117
+ part of the `catladder ✅` required check. `allowFailure: true` makes
118
+ it report-only.
119
+ - It gets the build vars plus `vars`, **not** the deployed env's
120
+ runtime vars/secrets — use services instead of the real database.
121
+ - Artifacts are uploaded also when the job fails.
122
+ - Not run in tagged releases. Disable one per env:
123
+ `env: { dev: { build: { postBuildTests: { e2e: false } } } }`.
124
+ - Names must not clash with other jobs of the component (e.g. `test`,
125
+ `lint`) — generation fails with a clear error.
126
+ - Workspace builds: on `builds.<ws>` they block the deploys of all
127
+ components in the workspace; on `build: { from: "<ws>", postBuildTests }`
128
+ only that component's deploy.
129
+ - To test the **deployed** env instead (smoke tests on dev / review
130
+ apps), use the component's `verify` (post-deploy, does not block the
131
+ deploy). The same suite can serve both, with `BASE_URL` as the switch.
132
+
85
133
  ## Project images (custom CI job images)
86
134
 
87
135
  When a job needs a toolchain catladder doesn't ship (Java, Playwright,
88
136
  …), declare a Docker image at the top level under `images` and reference
89
- it in any `jobImage` field (build, `test.jobImage`, custom/pages deploy,
137
+ it in any `jobImage` field (build, `test.jobImage`, `postBuildTests`, custom/pages deploy,
90
138
  verify):
91
139
 
92
140
  ```ts
@@ -12,6 +12,7 @@ generated pipeline files.
12
12
  | `startCommand` | `string` | runtime start command |
13
13
  | `postInstall` | `string \| string[]` | runs after the package-manager install (node family) |
14
14
  | `lint` / `test` / `audit` | `false \| TestJobCustom` | customize or disable the job |
15
+ | `postBuildTests` | `Record<string, PostBuildTestConfig \| false>` | named tests against the build output in the `post-build` stage; block the deploy |
15
16
  | `artifactsPaths` | `string[]` | extra artifacts (`dist`, `.next` always included) |
16
17
  | `artifactsExcludePaths` | `string[]` | artifact excludes |
17
18
  | `artifactsReports` | `{ junit?: string[] }` | CI test report paths |
@@ -27,6 +28,13 @@ generated pipeline files.
27
28
  `artifactsReports`, `artifacts`, `runnerVariables`, `allowFailure`
28
29
  (`allowFailure` defaults to `true` for `audit`).
29
30
 
31
+ `PostBuildTestConfig` (for `postBuildTests` entries): `TestJobCustom` with
32
+ a required `command`, plus `services?: Services` (gitlab shape, reachable
33
+ by alias) and `vars?: Record<string,string>`. The job gets the build
34
+ artifacts, the build vars and `vars` (not the deployed env's runtime
35
+ vars), installs dependencies for node builds, and uploads `artifacts`
36
+ also on failure.
37
+
30
38
  `audit` additionally takes `level`: `"info" | "low" | "moderate" | "high" |
31
39
  "critical"` (default `"critical"`) — the lowest severity that fails the job,
32
40
  translated to the package manager's own flag (`pnpm audit --audit-level`,
@@ -81,5 +89,5 @@ defaulted `buildCommand: "yarn build-storybook --quiet -o ./dist"`.
81
89
  Top-level `builds: Record<string, WorkspaceBuildConfig>`. Only
82
90
  `type: "node"` is supported.
83
91
 
84
- - Node workspace build: `dir?`, `buildCommand?` (default `<pm> build`), `dockerDefaults?: { yarnRebuildEnabled }`, plus shared `lint?` (default `{ command: "<pm> lint" }`), `test?` (default `{ command: "<pm> test" }`), `audit?`, `runnerVariables?`, `artifactsReports?`, `jobImage?`, `jobTags?`, `cache?` (`<pm>` = `yarn`/`pnpm` per detection).
85
- - A component references it via `build: { from: "<name>", docker?, startCommand?, artifactsPaths?, artifactsExcludePaths?, cache? }` — the per-component overrides layer on top of the shared build.
92
+ - Node workspace build: `dir?`, `buildCommand?` (default `<pm> build`), `dockerDefaults?: { yarnRebuildEnabled }`, plus shared `lint?` (default `{ command: "<pm> lint" }`), `test?` (default `{ command: "<pm> test" }`), `audit?`, `postBuildTests?` (block the deploys of all its components), `runnerVariables?`, `artifactsReports?`, `jobImage?`, `jobTags?`, `cache?` (`<pm>` = `yarn`/`pnpm` per detection).
93
+ - A component references it via `build: { from: "<name>", docker?, startCommand?, artifactsPaths?, artifactsExcludePaths?, cache?, postBuildTests? }` — the per-component overrides layer on top of the shared build.