@catladder/cli 0.0.0-next-b3e567e8 → 0.0.0-next-78f04d3f

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 (53) hide show
  1. package/dist/apps/cli/src/catci.js +23 -0
  2. package/dist/apps/cli/src/catci.js.map +1 -1
  3. package/dist/apps/cli/src/release/githubQueuedRelease.d.ts +14 -0
  4. package/dist/apps/cli/src/release/githubQueuedRelease.js +152 -0
  5. package/dist/apps/cli/src/release/githubQueuedRelease.js.map +1 -0
  6. package/dist/bundles/catci/index.js +3 -3
  7. package/dist/bundles/catenv/index.js +516 -136
  8. package/dist/bundles/cli/index.js +516 -136
  9. package/dist/bundles/skills/catladder-builds/SKILL.md +49 -2
  10. package/dist/bundles/skills/catladder-builds/references/build-types.md +2 -2
  11. package/dist/bundles/skills/catladder-pipelines/SKILL.md +13 -3
  12. package/dist/bundles/skills/catladder-releases/SKILL.md +13 -2
  13. package/dist/packages/pipeline/src/backends/github/GithubBackend.js +11 -4
  14. package/dist/packages/pipeline/src/backends/github/GithubBackend.js.map +1 -1
  15. package/dist/packages/pipeline/src/backends/github/githubReleaseJobs.d.ts +18 -2
  16. package/dist/packages/pipeline/src/backends/github/githubReleaseJobs.js +92 -10
  17. package/dist/packages/pipeline/src/backends/github/githubReleaseJobs.js.map +1 -1
  18. package/dist/packages/pipeline/src/backends/gitlab/GitlabBackend.js +35 -9
  19. package/dist/packages/pipeline/src/backends/gitlab/GitlabBackend.js.map +1 -1
  20. package/dist/packages/pipeline/src/backends/gitlab/gitlabReleaseJobs.d.ts +285 -3
  21. package/dist/packages/pipeline/src/backends/gitlab/gitlabReleaseJobs.js +142 -54
  22. package/dist/packages/pipeline/src/backends/gitlab/gitlabReleaseJobs.js.map +1 -1
  23. package/dist/packages/pipeline/src/build/types.d.ts +6 -5
  24. package/dist/packages/pipeline/src/build/types.js.map +1 -1
  25. package/dist/packages/pipeline/src/customImages/hash.d.ts +9 -0
  26. package/dist/packages/pipeline/src/customImages/hash.js +41 -16
  27. package/dist/packages/pipeline/src/customImages/hash.js.map +1 -1
  28. package/dist/packages/pipeline/src/customImages/jobImagesPlan.d.ts +23 -5
  29. package/dist/packages/pipeline/src/customImages/jobImagesPlan.js +177 -43
  30. package/dist/packages/pipeline/src/customImages/jobImagesPlan.js.map +1 -1
  31. package/dist/packages/pipeline/src/customImages/projectImages.d.ts +67 -0
  32. package/dist/packages/pipeline/src/customImages/projectImages.js +9 -0
  33. package/dist/packages/pipeline/src/customImages/projectImages.js.map +1 -0
  34. package/dist/packages/pipeline/src/deploy/types/custom.d.ts +2 -2
  35. package/dist/packages/pipeline/src/deploy/types/index.d.ts +4 -4
  36. package/dist/packages/pipeline/src/deploy/types/pages.d.ts +2 -2
  37. package/dist/packages/pipeline/src/index.d.ts +2 -0
  38. package/dist/packages/pipeline/src/index.js +2 -0
  39. package/dist/packages/pipeline/src/index.js.map +1 -1
  40. package/dist/packages/pipeline/src/types/config.d.ts +8 -0
  41. package/dist/packages/pipeline/src/types/config.js.map +1 -1
  42. package/dist/packages/pipeline/src/types/github-types.d.ts +1 -0
  43. package/dist/packages/pipeline/src/types/jobs.d.ts +2 -1
  44. package/dist/packages/pipeline/src/types/jobs.js.map +1 -1
  45. package/dist/packages/pipeline/src/types/release.d.ts +7 -1
  46. package/dist/skills/catladder-builds/SKILL.md +49 -2
  47. package/dist/skills/catladder-builds/references/build-types.md +2 -2
  48. package/dist/skills/catladder-pipelines/SKILL.md +13 -3
  49. package/dist/skills/catladder-releases/SKILL.md +13 -2
  50. package/dist/tsconfig.tsbuildinfo +1 -1
  51. package/package.json +1 -1
  52. package/src/catci.ts +30 -0
  53. package/src/release/githubQueuedRelease.ts +173 -0
@@ -8314,7 +8314,7 @@ class GithubBackend {
8314
8314
  await (0, promises_1.rm)(scriptFiles_1.GITHUB_SCRIPTS_FOLDER, { recursive: true, force: true });
8315
8315
  }
8316
8316
  async createFiles(config) {
8317
- const images = this.createImagesPlan();
8317
+ const images = this.createImagesPlan(config);
8318
8318
  const scripts = new scriptFiles_1.GithubScriptFiles();
8319
8319
  const workflows = await this.createWorkflows(config, images, scripts);
8320
8320
  return [
@@ -8329,13 +8329,13 @@ class GithubBackend {
8329
8329
  ...(0, shippedCatci_1.getCatciGeneratedFiles)(),
8330
8330
  ];
8331
8331
  }
8332
- createImagesPlan() {
8333
- return new jobImagesPlan_1.JobImagesPlan(this.type);
8332
+ createImagesPlan(config) {
8333
+ return new jobImagesPlan_1.JobImagesPlan(this.type, config.images);
8334
8334
  }
8335
8335
  /**
8336
8336
  * all workflows keyed by file name, mainly for testing purposes
8337
8337
  */
8338
- async createWorkflows(config, images = this.createImagesPlan(), scripts = new scriptFiles_1.GithubScriptFiles()) {
8338
+ async createWorkflows(config, images = this.createImagesPlan(config), scripts = new scriptFiles_1.GithubScriptFiles()) {
8339
8339
  var _a, _b, _c, _d;
8340
8340
  const workflows = {};
8341
8341
  // per-pipeline-type variables (pipelines.github.runnerVariables);
@@ -8456,6 +8456,13 @@ class GithubBackend {
8456
8456
  manualJobConditions[id] = `inputs.job == '${id}'`;
8457
8457
  manualDispatchOptions.push(id);
8458
8458
  }
8459
+ // manual release mode: the workflow that runs a queued release
8460
+ // once the main workflow completed green
8461
+ const releaseOnGreen = (0, githubReleaseJobs_1.getGithubReleaseOnGreenWorkflow)(config, images, TRIGGER_WORKFLOWS.mainBranch.name, workflowEnv);
8462
+ if (releaseOnGreen) {
8463
+ workflows[`${GENERATED_FILE_PREFIX}release-on-green.yml`] =
8464
+ releaseOnGreen;
8465
+ }
8459
8466
  }
8460
8467
  if (Object.keys(jobs).length > 0) {
8461
8468
  workflows[`${GENERATED_FILE_PREFIX}${workflowFileName(trigger)}`] = {
@@ -9000,16 +9007,21 @@ exports.getUploadProviderIds = getUploadProviderIds;
9000
9007
  "use strict";
9001
9008
 
9002
9009
  Object.defineProperty(exports, "__esModule", ({ value: true }));
9003
- exports.getGithubReleaseCheckJobs = exports.getGithubReleaseJobs = void 0;
9010
+ exports.getGithubReleaseCheckJobs = exports.getGithubReleaseOnGreenWorkflow = exports.getGithubReleaseJobs = void 0;
9004
9011
  const createGithubJobs_1 = __nccwpck_require__(57877);
9005
9012
  const release_1 = __nccwpck_require__(38904);
9006
- const createReleaseJob = (config, images, needs, extraEnv) => {
9013
+ const shippedCatci_1 = __nccwpck_require__(79024);
9014
+ const CATCI = `${shippedCatci_1.GENERATED_CATCI_FOLDER}/index.js`;
9015
+ const createReleaseJob = (config, images, options = {}) => {
9016
+ var _a;
9007
9017
  const method = (0, release_1.getReleaseMethod)(config);
9008
9018
  const resolved = images.resolveRef({ catladderImage: method.image });
9009
- const imageNeeds = resolved.need && typeof resolved.need === "object"
9019
+ const imageNeeds = !options.skipImageNeeds &&
9020
+ resolved.need &&
9021
+ typeof resolved.need === "object"
9010
9022
  ? [(0, createGithubJobs_1.githubGlobalJobId)(resolved.need.job)]
9011
9023
  : [];
9012
- const allNeeds = [...imageNeeds, ...(needs !== null && needs !== void 0 ? needs : [])];
9024
+ const allNeeds = [...imageNeeds, ...((_a = options.needs) !== null && _a !== void 0 ? _a : [])];
9013
9025
  return {
9014
9026
  name: "create release",
9015
9027
  "runs-on": "ubuntu-latest",
@@ -9038,7 +9050,7 @@ const createReleaseJob = (config, images, needs, extraEnv) => {
9038
9050
  ...(allNeeds.length > 0 ? { needs: allNeeds } : {}),
9039
9051
  env: {
9040
9052
  GITHUB_TOKEN: "${{ github.token }}",
9041
- ...extraEnv,
9053
+ ...options.extraEnv,
9042
9054
  },
9043
9055
  steps: [
9044
9056
  {
@@ -9048,8 +9060,21 @@ const createReleaseJob = (config, images, needs, extraEnv) => {
9048
9060
  // tags since the last release) to determine the version
9049
9061
  with: { "fetch-depth": 0 },
9050
9062
  },
9063
+ ...(options.queueCheck
9064
+ ? [
9065
+ {
9066
+ name: "check main workflow",
9067
+ id: "queue",
9068
+ run: `node ${CATCI} release github-queue-check`,
9069
+ shell: "bash",
9070
+ },
9071
+ ]
9072
+ : []),
9051
9073
  {
9052
9074
  name: "create release",
9075
+ ...(options.queueCheck
9076
+ ? { if: "steps.queue.outputs.queued != 'true'" }
9077
+ : {}),
9053
9078
  run: method.script,
9054
9079
  shell: "bash",
9055
9080
  },
@@ -9063,7 +9088,10 @@ const createReleaseJob = (config, images, needs, extraEnv) => {
9063
9088
  * workflow after all other jobs; a force variant goes to the manual
9064
9089
  * tasks workflow
9065
9090
  * - otherwise (manual, the default): the release job goes to the
9066
- * manual tasks workflow
9091
+ * manual tasks workflow. Its queue-check step releases right away on
9092
+ * a green main run and otherwise queues the release for the
9093
+ * release-on-green workflow (see getGithubReleaseOnGreenWorkflow) —
9094
+ * the counterpart of gitlab's queue button + executor pair.
9067
9095
  */
9068
9096
  const getGithubReleaseJobs = (config, mainWorkflowJobIds, images) => {
9069
9097
  var _a;
@@ -9071,28 +9099,89 @@ const getGithubReleaseJobs = (config, mainWorkflowJobIds, images) => {
9071
9099
  if (((_a = config.releases) === null || _a === void 0 ? void 0 : _a.when) === "auto") {
9072
9100
  return {
9073
9101
  main: {
9074
- "create-release": createReleaseJob(config, images, mainWorkflowJobIds),
9102
+ "create-release": createReleaseJob(config, images, {
9103
+ needs: mainWorkflowJobIds,
9104
+ }),
9075
9105
  },
9076
9106
  manual: {
9077
- "force-create-release": createReleaseJob(config, images, undefined, method.forceReleaseVariables),
9107
+ "force-create-release": createReleaseJob(config, images, {
9108
+ extraEnv: method.forceReleaseVariables,
9109
+ }),
9078
9110
  },
9079
9111
  };
9080
9112
  }
9081
9113
  return {
9082
9114
  main: {},
9083
9115
  manual: {
9084
- "create-release": createReleaseJob(config, images),
9116
+ "create-release": createReleaseJob(config, images, { queueCheck: true }),
9085
9117
  // an explicit force option wherever the method distinguishes it
9086
9118
  // (e.g. changesets: release a patch bump without changesets)
9087
9119
  ...(method.forceReleaseVariables
9088
9120
  ? {
9089
- "force-create-release": createReleaseJob(config, images, undefined, method.forceReleaseVariables),
9121
+ "force-create-release": createReleaseJob(config, images, {
9122
+ extraEnv: method.forceReleaseVariables,
9123
+ }),
9090
9124
  }
9091
9125
  : {}),
9092
9126
  },
9093
9127
  };
9094
9128
  };
9095
9129
  exports.getGithubReleaseJobs = getGithubReleaseJobs;
9130
+ /**
9131
+ * the release-on-green workflow (manual release mode only): runs on
9132
+ * every completed main workflow run. A lightweight host job checks the
9133
+ * queue marker (written by the create-release task while the main
9134
+ * workflow was still running) — most runs have nothing queued and end
9135
+ * after that single quick job. When the marker matches the completed
9136
+ * run, the containerized release job creates the release.
9137
+ *
9138
+ * No image-build jobs here: the release image was ensured by the main
9139
+ * workflow run that just completed (same commit, same content-hashed
9140
+ * tag).
9141
+ */
9142
+ const getGithubReleaseOnGreenWorkflow = (config, images, mainWorkflowName, workflowEnv) => {
9143
+ var _a;
9144
+ if (((_a = config.releases) === null || _a === void 0 ? void 0 : _a.when) === "auto") {
9145
+ return null;
9146
+ }
9147
+ return {
9148
+ name: "catladder release on green",
9149
+ on: {
9150
+ workflow_run: {
9151
+ workflows: [mainWorkflowName],
9152
+ types: ["completed"],
9153
+ },
9154
+ },
9155
+ env: workflowEnv,
9156
+ jobs: {
9157
+ guard: {
9158
+ name: "check queued release",
9159
+ "runs-on": "ubuntu-latest",
9160
+ // consuming the queue marker is a ref deletion
9161
+ permissions: { contents: "write" },
9162
+ env: { GITHUB_TOKEN: "${{ github.token }}" },
9163
+ outputs: { release: "${{ steps.guard.outputs.release }}" },
9164
+ steps: [
9165
+ { name: "Checkout", uses: "actions/checkout@v4" },
9166
+ {
9167
+ name: "check queued release",
9168
+ id: "guard",
9169
+ run: `node ${CATCI} release github-queued-guard "\${{ github.event.workflow_run.head_sha }}" "\${{ github.event.workflow_run.conclusion }}"`,
9170
+ shell: "bash",
9171
+ },
9172
+ ],
9173
+ },
9174
+ "create-release": {
9175
+ ...createReleaseJob(config, images, {
9176
+ needs: ["guard"],
9177
+ skipImageNeeds: true,
9178
+ }),
9179
+ if: "${{ needs.guard.outputs.release == 'true' }}",
9180
+ },
9181
+ },
9182
+ };
9183
+ };
9184
+ exports.getGithubReleaseOnGreenWorkflow = getGithubReleaseOnGreenWorkflow;
9096
9185
  /**
9097
9186
  * the informational MR/PR check job of the release method (e.g. the
9098
9187
  * changeset check), added to the mr workflow. continue-on-error keeps
@@ -9287,6 +9376,7 @@ const config_1 = __nccwpck_require__(17860);
9287
9376
  const createAllJobs_1 = __nccwpck_require__(54114);
9288
9377
  const jobImagesPlan_1 = __nccwpck_require__(47864);
9289
9378
  const shippedCatci_1 = __nccwpck_require__(79024);
9379
+ const release_1 = __nccwpck_require__(38904);
9290
9380
  const index_1 = __nccwpck_require__(68906);
9291
9381
  const createGitlabJobs_1 = __nccwpck_require__(41509);
9292
9382
  const createGitlabPipeline_1 = __nccwpck_require__(94411);
@@ -9304,7 +9394,7 @@ class GitlabBackend {
9304
9394
  await (0, promises_1.rm)(this.generatedFolder, { force: true, recursive: true });
9305
9395
  }
9306
9396
  async createFiles(config) {
9307
- const images = this.createImagesPlan();
9397
+ const images = this.createImagesPlan(config);
9308
9398
  const includes = await this.createIncludes(config, images);
9309
9399
  const mainFile = {
9310
9400
  path: ".gitlab-ci.yml",
@@ -9321,15 +9411,15 @@ class GitlabBackend {
9321
9411
  ...(0, shippedCatci_1.getCatciGeneratedFiles)(),
9322
9412
  ];
9323
9413
  }
9324
- createImagesPlan() {
9325
- return new jobImagesPlan_1.JobImagesPlan(this.type);
9414
+ createImagesPlan(config) {
9415
+ return new jobImagesPlan_1.JobImagesPlan(this.type, config.images);
9326
9416
  }
9327
9417
  /**
9328
9418
  * the complete pipeline as one object (all includes merged into one),
9329
9419
  * mainly for testing purposes
9330
9420
  */
9331
9421
  async createCompletePipeline(config) {
9332
- const includes = await this.createIncludes(config, this.createImagesPlan());
9422
+ const includes = await this.createIncludes(config, this.createImagesPlan(config));
9333
9423
  return includes.reduce((acc, { content }) => {
9334
9424
  return {
9335
9425
  ...acc,
@@ -9373,12 +9463,29 @@ class GitlabBackend {
9373
9463
  }
9374
9464
  async createPipeline(config, images) {
9375
9465
  const stages = (0, getPipelineStages_1.getPipelineStages)(config);
9376
- // release jobs first: they register their image on the plan, and the
9377
- // per-trigger job creation below emits the build jobs of all images
9378
- // registered so far
9379
- const releaseJobs = (0, gitlabReleaseJobs_1.getGitlabReleaseJobs)(config, images);
9466
+ // register the release image first: the per-trigger job creation
9467
+ // below emits the build jobs of all images registered so far. The
9468
+ // release jobs themselves are created afterwards — they need the
9469
+ // main-branch job list for the queued-release executor's needs.
9470
+ images.resolveRef({ catladderImage: (0, release_1.getReleaseMethod)(config).image });
9380
9471
  // for all triggers create jobs and add base rules
9381
- const allJobsPerTrigger = await Promise.all(config_1.ALL_PIPELINE_TRIGGERS.map(async (trigger) => await (0, createGitlabJobs_1.createGitlabJobs)(await (0, createAllJobs_1.createAllJobs)({ config, trigger, pipelineType: this.type }), images, getGitlabRulesForTrigger(trigger)))).then((j) => j.flat());
9472
+ const jobsPerTrigger = await Promise.all(config_1.ALL_PIPELINE_TRIGGERS.map(async (trigger) => ({
9473
+ trigger,
9474
+ jobs: await (0, createGitlabJobs_1.createGitlabJobs)(await (0, createAllJobs_1.createAllJobs)({ config, trigger, pipelineType: this.type }), images, getGitlabRulesForTrigger(trigger)),
9475
+ })));
9476
+ const allJobsPerTrigger = jobsPerTrigger.flatMap(({ jobs }) => jobs);
9477
+ // the jobs the queued-release executor waits for: everything that
9478
+ // runs automatically in a main-branch pipeline. Manual jobs (stop,
9479
+ // rollback, gates) are excluded — gitlab skips a job whose needs
9480
+ // include an unplayed manual job.
9481
+ const mainBranchJobNames = [
9482
+ ...new Set(jobsPerTrigger
9483
+ .filter(({ trigger }) => trigger === "mainBranch")
9484
+ .flatMap(({ jobs }) => jobs)
9485
+ .filter(({ gitlabJob }) => !isManualGitlabJob(gitlabJob))
9486
+ .map(({ name }) => name)),
9487
+ ];
9488
+ const releaseJobs = (0, gitlabReleaseJobs_1.getGitlabReleaseJobs)(config, images, mainBranchJobNames);
9382
9489
  const allWorkspaceJobs = allJobsPerTrigger
9383
9490
  .filter((j) => { var _a; return ((_a = j.context) === null || _a === void 0 ? void 0 : _a.type) === "workspace"; }) // sort by componentName in the same order as they appear in the config
9384
9491
  // this is purely for better readability in git diffs when you add new components
@@ -9444,6 +9551,14 @@ class GitlabBackend {
9444
9551
  }
9445
9552
  }
9446
9553
  exports.GitlabBackend = GitlabBackend;
9554
+ /**
9555
+ * whether a job can end up waiting for a human in a main-branch
9556
+ * pipeline (catladder expresses manual-ness only through rules)
9557
+ */
9558
+ function isManualGitlabJob(job) {
9559
+ var _a;
9560
+ return ((_a = job.rules) !== null && _a !== void 0 ? _a : []).some((rule) => rule.when === "manual");
9561
+ }
9447
9562
  function getGitlabRulesForTrigger(trigger) {
9448
9563
  // mainBranch: on push to main branch except it's a release commit
9449
9564
  // mr: on merge request
@@ -9976,91 +10091,179 @@ exports.getPipelineStages = getPipelineStages;
9976
10091
  "use strict";
9977
10092
 
9978
10093
  Object.defineProperty(exports, "__esModule", ({ value: true }));
9979
- exports.getGitlabReleaseJobs = void 0;
10094
+ exports.getGitlabReleaseJobs = exports.RELEASE_EXECUTOR_JOB_NAME = exports.RELEASE_BUTTON_JOB_NAME = void 0;
9980
10095
  const rules_1 = __nccwpck_require__(35086);
9981
10096
  const release_1 = __nccwpck_require__(38904);
9982
10097
  const EXPIRED_TOKEN_HELP = "echo '👉 If this job failed with access denied, the project access token might be invald - run `project-renew-token` in catladder CLI to fix.'";
10098
+ /**
10099
+ * gitlab rejects jobs with more than 50 `needs:` entries (default
10100
+ * limit). Pipelines beyond that fall back to the legacy manual release
10101
+ * job, which is only playable once all prior stages finished.
10102
+ */
10103
+ const GITLAB_MAX_NEEDS = 50;
10104
+ exports.RELEASE_BUTTON_JOB_NAME = "create release";
10105
+ exports.RELEASE_EXECUTOR_JOB_NAME = "🚀 release once pipeline succeeds";
9983
10106
  const baseReleaseRules = [
9984
10107
  rules_1.RULE_NEVER_ON_RELEASE_COMMIT,
9985
10108
  rules_1.RULE_NEVER_ON_AGENT_TRIGGER,
9986
10109
  rules_1.RULE_NEVER_ON_SCHEDULE,
9987
10110
  ];
9988
- const getGitlabReleaseJobs = (config, images) => {
10111
+ // NOTE: with `rules:`, when: manual defaults to allow_failure: false,
10112
+ // which turns every unclicked pipeline into status "blocked" — the
10113
+ // manual release jobs therefore carry allow_failure: true so pipelines
10114
+ // end "passed" when nobody releases
10115
+ const manualReleaseRules = [
10116
+ ...baseReleaseRules,
10117
+ {
10118
+ if: rules_1.RULE_CONDITION_MAIN_BRANCH,
10119
+ when: "manual",
10120
+ },
10121
+ {
10122
+ if: rules_1.RULE_CONDITION_HOTFIX_BRANCH,
10123
+ when: "manual",
10124
+ },
10125
+ ];
10126
+ const getGitlabReleaseJobs = (config, images,
10127
+ /**
10128
+ * names of the jobs that run automatically in main-branch pipelines
10129
+ * (manual jobs excluded: a needed-but-unplayed manual job would make
10130
+ * gitlab skip the release executor)
10131
+ */
10132
+ mainBranchJobNames) => {
9989
10133
  var _a;
9990
10134
  const method = (0, release_1.getReleaseMethod)(config);
9991
10135
  // NOTE: only the resolved image url is used, not the `need`: release
9992
- // jobs have no `needs` on purpose — their stage ordering already
9993
- // guarantees the image build job (setup stage) ran, and adding a
9994
- // `needs` would switch gitlab to DAG mode, so the release job would no
9995
- // longer wait for the build/deploy stages.
10136
+ // jobs have no `needs` on the whole pipeline on purpose — where they
10137
+ // must wait for it, the wait is expressed by enumerating the jobs
10138
+ // explicitly (see the executor below).
9996
10139
  const releaseImage = images.resolveRef({
9997
10140
  catladderImage: method.image,
9998
10141
  }).image;
9999
- return {
10000
- ["create release"]: {
10001
- stage: "release",
10002
- image: releaseImage,
10003
- script: [method.script],
10004
- after_script: [EXPIRED_TOKEN_HELP],
10005
- rules: [
10006
- ...baseReleaseRules,
10007
- {
10008
- if: rules_1.RULE_CONDITION_MAIN_BRANCH,
10009
- when: ((_a = config.releases) === null || _a === void 0 ? void 0 : _a.when) === "auto" ? "on_success" : "manual",
10010
- },
10011
- {
10012
- if: rules_1.RULE_CONDITION_HOTFIX_BRANCH,
10013
- when: "manual",
10014
- },
10015
- ],
10016
- },
10142
+ const releaseJobBase = {
10143
+ stage: "release",
10144
+ image: releaseImage,
10145
+ script: [method.script],
10146
+ after_script: [EXPIRED_TOKEN_HELP],
10147
+ };
10148
+ const forceReleaseJob = {
10017
10149
  ["⚠️ force create release"]: {
10018
- stage: "release",
10019
- image: releaseImage,
10020
- script: [method.script],
10021
- after_script: [EXPIRED_TOKEN_HELP],
10150
+ ...releaseJobBase,
10022
10151
  needs: [],
10152
+ allow_failure: true,
10023
10153
  // force semantics of the method's script (e.g. changesets:
10024
10154
  // release a patch bump even without pending changesets)
10025
10155
  ...(method.forceReleaseVariables
10026
10156
  ? { variables: method.forceReleaseVariables }
10027
10157
  : {}),
10158
+ rules: manualReleaseRules,
10159
+ },
10160
+ };
10161
+ // informational MR check of the release method (e.g. the changeset
10162
+ // check): warns via allow_failure, the report lands in the MR
10163
+ // widget as an exposed artifact
10164
+ const checkJob = method.checkScript
10165
+ ? {
10166
+ ["🦋 changeset check"]: {
10167
+ stage: "test",
10168
+ image: releaseImage,
10169
+ script: [method.checkScript],
10170
+ allow_failure: true,
10171
+ artifacts: {
10172
+ paths: ["changeset-report.md"],
10173
+ expose_as: "changeset report",
10174
+ // the report must survive the warning exit
10175
+ when: "always",
10176
+ },
10177
+ rules: [
10178
+ rules_1.RULE_NEVER_ON_AGENT_TRIGGER,
10179
+ rules_1.RULE_NEVER_ON_SCHEDULE,
10180
+ rules_1.RULE_IS_MERGE_REQUEST,
10181
+ ],
10182
+ },
10183
+ }
10184
+ : {};
10185
+ if (((_a = config.releases) === null || _a === void 0 ? void 0 : _a.when) === "auto") {
10186
+ return {
10187
+ [exports.RELEASE_BUTTON_JOB_NAME]: {
10188
+ ...releaseJobBase,
10189
+ rules: [
10190
+ ...baseReleaseRules,
10191
+ {
10192
+ if: rules_1.RULE_CONDITION_MAIN_BRANCH,
10193
+ when: "on_success",
10194
+ },
10195
+ {
10196
+ if: rules_1.RULE_CONDITION_HOTFIX_BRANCH,
10197
+ when: "manual",
10198
+ // per-rule: only the unclicked hotfix button must not
10199
+ // block — a failed automatic release stays loud
10200
+ allow_failure: true,
10201
+ },
10202
+ ],
10203
+ },
10204
+ ...forceReleaseJob,
10205
+ ...checkJob,
10206
+ };
10207
+ }
10208
+ const executorNeeds = [
10209
+ { job: exports.RELEASE_BUTTON_JOB_NAME },
10210
+ // optional: hotfix pipelines (and rules-filtered jobs) contain a
10211
+ // different job subset
10212
+ ...mainBranchJobNames.map((name) => ({ job: name, optional: true })),
10213
+ ];
10214
+ if (executorNeeds.length > GITLAB_MAX_NEEDS) {
10215
+ console.warn(`⚠️ the main-branch pipeline has more than ${GITLAB_MAX_NEEDS} jobs — ` +
10216
+ `the "${exports.RELEASE_BUTTON_JOB_NAME}" job falls back to being playable ` +
10217
+ `only once the pipeline finished (gitlab caps needs: at ${GITLAB_MAX_NEEDS})`);
10218
+ return {
10219
+ [exports.RELEASE_BUTTON_JOB_NAME]: {
10220
+ ...releaseJobBase,
10221
+ allow_failure: true,
10222
+ rules: manualReleaseRules,
10223
+ },
10224
+ ...forceReleaseJob,
10225
+ ...checkJob,
10226
+ };
10227
+ }
10228
+ return {
10229
+ // the queue button: playable from the first second of the pipeline.
10230
+ // It only records the intent to release — the executor below does
10231
+ // the actual work once every other job succeeded. Clicking after the
10232
+ // pipeline finished works the same (the executor starts right away).
10233
+ [exports.RELEASE_BUTTON_JOB_NAME]: {
10234
+ stage: "release",
10235
+ image: releaseImage,
10236
+ script: [
10237
+ `echo "release queued — '${exports.RELEASE_EXECUTOR_JOB_NAME}' runs it as soon as every other job in this pipeline succeeded"`,
10238
+ ],
10239
+ needs: [],
10240
+ // allow_failure keeps the unclicked button from blocking the
10241
+ // pipeline AND makes the executor's needs treat it as optional
10242
+ // (skipped executor instead of a forever-waiting pipeline)
10243
+ allow_failure: true,
10244
+ rules: manualReleaseRules,
10245
+ },
10246
+ // the executor: never played manually. Skipped when the button was
10247
+ // not clicked; otherwise starts automatically as soon as the button
10248
+ // and every other (non-manual) job of the pipeline succeeded, and
10249
+ // is skipped when any of them failed.
10250
+ [exports.RELEASE_EXECUTOR_JOB_NAME]: {
10251
+ ...releaseJobBase,
10252
+ needs: executorNeeds,
10028
10253
  rules: [
10029
10254
  ...baseReleaseRules,
10030
10255
  {
10031
10256
  if: rules_1.RULE_CONDITION_MAIN_BRANCH,
10032
- when: "manual",
10257
+ when: "on_success",
10033
10258
  },
10034
10259
  {
10035
10260
  if: rules_1.RULE_CONDITION_HOTFIX_BRANCH,
10036
- when: "manual",
10261
+ when: "on_success",
10037
10262
  },
10038
10263
  ],
10039
10264
  },
10040
- // informational MR check of the release method (e.g. the changeset
10041
- // check): warns via allow_failure, the report lands in the MR
10042
- // widget as an exposed artifact
10043
- ...(method.checkScript
10044
- ? {
10045
- ["🦋 changeset check"]: {
10046
- stage: "test",
10047
- image: releaseImage,
10048
- script: [method.checkScript],
10049
- allow_failure: true,
10050
- artifacts: {
10051
- paths: ["changeset-report.md"],
10052
- expose_as: "changeset report",
10053
- // the report must survive the warning exit
10054
- when: "always",
10055
- },
10056
- rules: [
10057
- rules_1.RULE_NEVER_ON_AGENT_TRIGGER,
10058
- rules_1.RULE_NEVER_ON_SCHEDULE,
10059
- rules_1.RULE_IS_MERGE_REQUEST,
10060
- ],
10061
- },
10062
- }
10063
- : {}),
10265
+ ...forceReleaseJob,
10266
+ ...checkJob,
10064
10267
  };
10065
10268
  };
10066
10269
  exports.getGitlabReleaseJobs = getGitlabReleaseJobs;
@@ -12974,7 +13177,7 @@ exports.makeSecretEnvVarMapping = makeSecretEnvVarMapping;
12974
13177
  "use strict";
12975
13178
 
12976
13179
  Object.defineProperty(exports, "__esModule", ({ value: true }));
12977
- exports.computeCustomImageHash = void 0;
13180
+ exports.computeInlineImageHash = exports.computeCustomImageHash = void 0;
12978
13181
  const crypto_1 = __nccwpck_require__(76982);
12979
13182
  const fs_1 = __nccwpck_require__(79896);
12980
13183
  const path_1 = __nccwpck_require__(16928);
@@ -12995,6 +13198,29 @@ function listFilesRecursive(dir) {
12995
13198
  })
12996
13199
  .sort();
12997
13200
  }
13201
+ /**
13202
+ * the parts every image definition hashes on top of its content:
13203
+ * files outside the definition, and the build args
13204
+ */
13205
+ function hashExtras(hash, config) {
13206
+ const watchedPaths = [];
13207
+ // Hash extra paths (for files outside the dir)
13208
+ if (config.hashExtraPaths) {
13209
+ // Extra paths are literal file paths (not globs) for simplicity
13210
+ const extraFiles = [...config.hashExtraPaths].sort();
13211
+ for (const file of extraFiles) {
13212
+ hash.update(file);
13213
+ hash.update((0, fs_1.readFileSync)(file));
13214
+ }
13215
+ watchedPaths.push(...config.hashExtraPaths);
13216
+ }
13217
+ // Hash build args (sorted keys for determinism)
13218
+ if (config.buildArgs) {
13219
+ const sorted = Object.entries(config.buildArgs).sort(([a], [b]) => a.localeCompare(b));
13220
+ hash.update(JSON.stringify(sorted));
13221
+ }
13222
+ return watchedPaths;
13223
+ }
12998
13224
  function computeCustomImageHash(config) {
12999
13225
  var _a;
13000
13226
  const hash = (0, crypto_1.createHash)("sha256");
@@ -13015,27 +13241,29 @@ function computeCustomImageHash(config) {
13015
13241
  }
13016
13242
  watchedPaths.push(`${extraDir}/**/*`);
13017
13243
  }
13018
- // Hash extra paths (for files outside the dir)
13019
- if (config.hashExtraPaths) {
13020
- // Extra paths are literal file paths (not globs) for simplicity
13021
- const extraFiles = [...config.hashExtraPaths].sort();
13022
- for (const file of extraFiles) {
13023
- hash.update(file);
13024
- hash.update((0, fs_1.readFileSync)(file));
13025
- }
13026
- watchedPaths.push(...config.hashExtraPaths);
13027
- }
13028
- // Hash build args (sorted keys for determinism)
13029
- if (config.buildArgs) {
13030
- const sorted = Object.entries(config.buildArgs).sort(([a], [b]) => a.localeCompare(b));
13031
- hash.update(JSON.stringify(sorted));
13032
- }
13244
+ watchedPaths.push(...hashExtras(hash, config));
13033
13245
  return {
13034
13246
  hash: hash.digest("hex").slice(0, 12),
13035
13247
  watchedPaths,
13036
13248
  };
13037
13249
  }
13038
13250
  exports.computeCustomImageHash = computeCustomImageHash;
13251
+ /**
13252
+ * the hash of an image defined by an inline Dockerfile: its content
13253
+ * stands in for the definition dir. The build context is deliberately
13254
+ * NOT hashed (it may be the whole repository) — `hashExtraPaths` is how
13255
+ * context files opt into the hash.
13256
+ */
13257
+ function computeInlineImageHash(config) {
13258
+ const hash = (0, crypto_1.createHash)("sha256");
13259
+ hash.update(config.dockerfileContent);
13260
+ const watchedPaths = hashExtras(hash, config);
13261
+ return {
13262
+ hash: hash.digest("hex").slice(0, 12),
13263
+ watchedPaths,
13264
+ };
13265
+ }
13266
+ exports.computeInlineImageHash = computeInlineImageHash;
13039
13267
  //# sourceMappingURL=hash.js.map
13040
13268
 
13041
13269
  /***/ }),
@@ -13046,7 +13274,7 @@ exports.computeCustomImageHash = computeCustomImageHash;
13046
13274
  "use strict";
13047
13275
 
13048
13276
  Object.defineProperty(exports, "__esModule", ({ value: true }));
13049
- exports.JobImagesPlan = exports.GENERATED_IMAGES_FOLDER = void 0;
13277
+ exports.JobImagesPlan = exports.PROJECT_IMAGES_FOLDER = exports.GENERATED_IMAGES_FOLDER = void 0;
13050
13278
  const fs_1 = __nccwpck_require__(79896);
13051
13279
  const path_1 = __nccwpck_require__(16928);
13052
13280
  const ciVariables_1 = __nccwpck_require__(72700);
@@ -13054,6 +13282,7 @@ const docker_1 = __nccwpck_require__(83777);
13054
13282
  const runner_1 = __nccwpck_require__(71065);
13055
13283
  const gitlab_1 = __nccwpck_require__(37393);
13056
13284
  const hash_1 = __nccwpck_require__(34092);
13285
+ const projectImages_1 = __nccwpck_require__(55537);
13057
13286
  const shippedImages_1 = __nccwpck_require__(28687);
13058
13287
  exports.GENERATED_IMAGES_FOLDER = ".catladder-generated/images";
13059
13288
  /**
@@ -13061,7 +13290,44 @@ exports.GENERATED_IMAGES_FOLDER = ".catladder-generated/images";
13061
13290
  * catladder image, so building images never depends on one (bootstrap)
13062
13291
  */
13063
13292
  const BUILDER_IMAGE = "docker.io/docker:29.5.1";
13064
- const ensureJobName = (name) => `🐳 job image ${name}`;
13293
+ /**
13294
+ * every planned image is consumed the same way: its url, plus an
13295
+ * optional dependency on its build job
13296
+ */
13297
+ const toResolvedJobImage = ({ imageRef, jobName, }) => ({
13298
+ image: imageRef,
13299
+ fromRepoRegistry: true,
13300
+ need: {
13301
+ job: jobName,
13302
+ artifacts: false,
13303
+ optional: true,
13304
+ global: true,
13305
+ },
13306
+ });
13307
+ /**
13308
+ * materializes an image definition directory into the generated files
13309
+ * (used for the definitions catladder ships)
13310
+ */
13311
+ const materializeDir = (sourceDir, targetDir) => listFilesRecursive(sourceDir).map((file) => ({
13312
+ path: (0, path_1.join)(targetDir, file),
13313
+ content: (0, fs_1.readFileSync)((0, path_1.join)(sourceDir, file), "utf-8"),
13314
+ }));
13315
+ // built-in (catladder-shipped) images carry the catladder marker in
13316
+ // their job name; project-declared images get the short plain name
13317
+ const ensureJobName = (name) => `🐳 catladder image ${name}`;
13318
+ const projectEnsureJobName = (name) => `🐳 image ${name}`;
13319
+ /**
13320
+ * the config file whose content feeds the project image hashes
13321
+ * (`buildArgs`, `hashExtraPaths`): the gitlab change detection has to
13322
+ * watch it, because a config-only change alters the image tag without
13323
+ * touching the image dir
13324
+ */
13325
+ const CONFIG_FILE = "catladder.ts";
13326
+ /**
13327
+ * where inline project Dockerfiles are materialized — a subfolder, so a
13328
+ * project image can carry the same name as a shipped one
13329
+ */
13330
+ exports.PROJECT_IMAGES_FOLDER = `${exports.GENERATED_IMAGES_FOLDER}/project`;
13065
13331
  /**
13066
13332
  * plans the catladder job images of one backend run: resolves image
13067
13333
  * references, collects the image build jobs, and the image definitions
@@ -13072,33 +13338,30 @@ const ensureJobName = (name) => `🐳 job image ${name}`;
13072
13338
  * Reference-driven: only images actually used by a job are built.
13073
13339
  */
13074
13340
  class JobImagesPlan {
13075
- constructor(pipelineType) {
13341
+ constructor(pipelineType, projectImages = {}) {
13076
13342
  this.pipelineType = pipelineType;
13343
+ this.projectImages = projectImages;
13077
13344
  this.used = new Map();
13345
+ this.usedProject = new Map();
13078
13346
  }
13079
13347
  /**
13080
13348
  * resolves a job image (marker or concrete) to a concrete image url
13081
13349
  * plus the build job dependency
13082
13350
  */
13083
13351
  resolve(image) {
13084
- if (!(0, runner_1.isCatladderImageRef)(image)) {
13085
- return { image };
13352
+ if ((0, runner_1.isCatladderImageRef)(image)) {
13353
+ return this.resolveRef(image);
13086
13354
  }
13087
- return this.resolveRef(image);
13355
+ if ((0, projectImages_1.isProjectImageRef)(image)) {
13356
+ return this.resolveProjectRef(image);
13357
+ }
13358
+ return { image };
13088
13359
  }
13089
13360
  resolveRef(ref) {
13090
- const name = ref.catladderImage;
13091
- const { hash } = this.use(name);
13092
- return {
13093
- image: `${this.registryImageRef()}/catladder/${name}:${hash}`,
13094
- fromRepoRegistry: true,
13095
- need: {
13096
- job: ensureJobName(name),
13097
- artifacts: false,
13098
- optional: true,
13099
- global: true,
13100
- },
13101
- };
13361
+ return toResolvedJobImage(this.use(ref.catladderImage));
13362
+ }
13363
+ resolveProjectRef(ref) {
13364
+ return toResolvedJobImage(this.useProject(ref.image));
13102
13365
  }
13103
13366
  /**
13104
13367
  * the registry image prefix. On github, container images can't use
@@ -13109,6 +13372,85 @@ class JobImagesPlan {
13109
13372
  ? "ghcr.io/${{ github.repository }}"
13110
13373
  : `${(0, ciVariables_1.getCiVariable)({ pipelineType: this.pipelineType }, "registryImage")}`;
13111
13374
  }
13375
+ useProject(name) {
13376
+ const existing = this.usedProject.get(name);
13377
+ if (existing) {
13378
+ return existing;
13379
+ }
13380
+ const imageConfig = this.projectImages[name];
13381
+ if (!imageConfig) {
13382
+ const declared = Object.keys(this.projectImages);
13383
+ throw new Error(`unknown job image "${name}" — ${declared.length > 0
13384
+ ? `images declared in config.images: ${declared.join(", ")}`
13385
+ : "no images are declared in config.images"}`);
13386
+ }
13387
+ const source = this.planProjectSource(name, imageConfig);
13388
+ const planned = {
13389
+ ...source,
13390
+ jobName: projectEnsureJobName(name),
13391
+ buildArgs: imageConfig.buildArgs,
13392
+ // `job-images/` keeps the project's job images apart from its
13393
+ // deployable component images (registry root) and build caches
13394
+ imageRef: `${this.registryImageRef()}/job-images/${name}:${source.hash}`,
13395
+ };
13396
+ this.usedProject.set(name, planned);
13397
+ return planned;
13398
+ }
13399
+ /**
13400
+ * the source-dependent half of a project image: an inline Dockerfile
13401
+ * is materialized into the generated files and hashed by its content,
13402
+ * a directory is hashed and watched in place
13403
+ */
13404
+ planProjectSource(name, imageConfig) {
13405
+ var _a, _b;
13406
+ if (imageConfig.dockerfile !== undefined) {
13407
+ const content = toDockerfileContent(imageConfig.dockerfile);
13408
+ const dockerfilePath = `${exports.PROJECT_IMAGES_FOLDER}/${name}/Dockerfile`;
13409
+ const { hash, watchedPaths } = (0, hash_1.computeInlineImageHash)({
13410
+ dockerfileContent: content,
13411
+ hashExtraPaths: imageConfig.hashExtraPaths,
13412
+ buildArgs: imageConfig.buildArgs,
13413
+ });
13414
+ return {
13415
+ hash,
13416
+ dockerfilePath,
13417
+ // an inline definition carries no directory of its own — the
13418
+ // repository root is the sensible default context
13419
+ contextDir: (_a = imageConfig.context) !== null && _a !== void 0 ? _a : ".",
13420
+ watchedPaths: [
13421
+ `${exports.PROJECT_IMAGES_FOLDER}/${name}/**/*`,
13422
+ ...watchedPaths,
13423
+ CONFIG_FILE,
13424
+ ],
13425
+ files: [{ path: dockerfilePath, content }],
13426
+ };
13427
+ }
13428
+ const dir = imageConfig.dir;
13429
+ // fail at generation time with the offending image named: an
13430
+ // unreadable dir would otherwise surface as a bare fs ENOENT, and a
13431
+ // missing Dockerfile only as a docker build failure in CI
13432
+ if (!(0, fs_1.existsSync)(dir)) {
13433
+ throw new Error(`job image "${name}": the directory "${dir}" does not exist (config.images.${name}.dir, relative to the repository root)`);
13434
+ }
13435
+ if (!(0, fs_1.existsSync)((0, path_1.join)(dir, "Dockerfile"))) {
13436
+ throw new Error(`job image "${name}": no Dockerfile in "${dir}" (config.images.${name}.dir)`);
13437
+ }
13438
+ const { hash, watchedPaths } = (0, hash_1.computeCustomImageHash)({
13439
+ dir,
13440
+ hashExtraPaths: imageConfig.hashExtraPaths,
13441
+ buildArgs: imageConfig.buildArgs,
13442
+ });
13443
+ return {
13444
+ hash,
13445
+ dockerfilePath: `${dir}/Dockerfile`,
13446
+ contextDir: (_b = imageConfig.context) !== null && _b !== void 0 ? _b : dir,
13447
+ // the dir is committed in the project — watched in place, nothing
13448
+ // is materialized. The config file is watched too: buildArgs /
13449
+ // hashExtraPaths changes alter the tag without touching the dir
13450
+ watchedPaths: [...watchedPaths, CONFIG_FILE],
13451
+ files: [],
13452
+ };
13453
+ }
13112
13454
  use(name) {
13113
13455
  var _a;
13114
13456
  const existing = this.used.get(name);
@@ -13123,27 +13465,43 @@ class JobImagesPlan {
13123
13465
  dir: (0, shippedImages_1.getShippedImageDir)(name),
13124
13466
  extraDirs: dependencies.map((dependency) => (0, shippedImages_1.getShippedImageDir)(dependency)),
13125
13467
  });
13126
- const entry = { hash };
13127
- this.used.set(name, entry);
13128
- return entry;
13468
+ const targetDir = `${exports.GENERATED_IMAGES_FOLDER}/${name}`;
13469
+ const planned = {
13470
+ hash,
13471
+ jobName: ensureJobName(name),
13472
+ imageRef: `${this.registryImageRef()}/catladder/${name}:${hash}`,
13473
+ dockerfilePath: `${targetDir}/Dockerfile`,
13474
+ // the jobs family INCLUDEs sibling images, so it builds from the
13475
+ // images root; the others from their own dir
13476
+ contextDir: shippedImages_1.RUNNER_IMAGE_BUILD_CONTEXT[name] === "root"
13477
+ ? exports.GENERATED_IMAGES_FOLDER
13478
+ : targetDir,
13479
+ watchedPaths: [name, ...dependencies].map((dir) => `${exports.GENERATED_IMAGES_FOLDER}/${dir}/**/*`),
13480
+ // pipelines build from committed, reviewable files (and gitlab's
13481
+ // rules:changes can watch them)
13482
+ files: materializeDir((0, shippedImages_1.getShippedImageDir)(name), targetDir),
13483
+ };
13484
+ this.used.set(name, planned);
13485
+ return planned;
13129
13486
  }
13130
13487
  /**
13131
13488
  * the image build jobs for all used images
13132
13489
  */
13133
13490
  getEnsureJobs() {
13134
- return [...this.used.entries()].map(([name, { hash }]) => this.createEnsureJob(name, hash));
13491
+ return [...this.used.values(), ...this.usedProject.values()].map((planned) => this.createImageBuildJob(planned));
13135
13492
  }
13136
- createEnsureJob(name, hash) {
13137
- var _a;
13493
+ createImageBuildJob({ jobName, imageRef, watchedPaths, dockerfilePath, contextDir, buildArgs, }) {
13494
+ const buildArgFlags = Object.entries(buildArgs !== null && buildArgs !== void 0 ? buildArgs : {})
13495
+ .map(([key, value]) => ` --build-arg '${key}=${value}'`)
13496
+ .join("");
13497
+ const buildCommand = `DOCKER_BUILDKIT=1 docker build -t ${imageRef}${buildArgFlags} -f ${dockerfilePath} ${contextDir}`;
13138
13498
  const ctx = { pipelineType: this.pipelineType };
13139
13499
  const registryUser = (0, ciVariables_1.getCiVariable)(ctx, "registryUser");
13140
13500
  const jobToken = (0, ciVariables_1.getCiVariable)(ctx, "jobToken");
13141
13501
  const registry = (0, ciVariables_1.getCiVariable)(ctx, "registry");
13142
- const imageRef = `${this.registryImageRef()}/catladder/${name}:${hash}`;
13143
- const watchedDirs = [name, ...((_a = shippedImages_1.RUNNER_IMAGE_DEPENDENCIES[name]) !== null && _a !== void 0 ? _a : [])];
13144
13502
  const { services, runnerVariables } = (0, docker_1.getDockerJobBaseProps)();
13145
13503
  return {
13146
- name: ensureJobName(name),
13504
+ name: jobName,
13147
13505
  stage: "setup",
13148
13506
  image: BUILDER_IMAGE,
13149
13507
  services,
@@ -13153,7 +13511,9 @@ class JobImagesPlan {
13153
13511
  // no equivalent and always runs (the existence check is fast)
13154
13512
  rules: [
13155
13513
  {
13156
- changes: watchedDirs.map((dir) => `${exports.GENERATED_IMAGES_FOLDER}/${dir}/**/*`),
13514
+ // a fresh array per job: the yaml serializer anchors repeated
13515
+ // objects, and a shared reference would reshuffle anchors
13516
+ changes: [...watchedPaths],
13157
13517
  },
13158
13518
  ],
13159
13519
  variables: {},
@@ -13167,31 +13527,33 @@ class JobImagesPlan {
13167
13527
  ` exit 0`,
13168
13528
  `fi`,
13169
13529
  ]),
13170
- ...(0, gitlab_1.collapseableSection)("docker-build", "Building image")([
13171
- `DOCKER_BUILDKIT=1 docker build -t ${imageRef} -f ${exports.GENERATED_IMAGES_FOLDER}/${name}/Dockerfile ${shippedImages_1.RUNNER_IMAGE_BUILD_CONTEXT[name] === "root"
13172
- ? exports.GENERATED_IMAGES_FOLDER
13173
- : `${exports.GENERATED_IMAGES_FOLDER}/${name}`}`,
13174
- ]),
13530
+ ...(0, gitlab_1.collapseableSection)("docker-build", "Building image")([buildCommand]),
13175
13531
  ...(0, gitlab_1.collapseableSection)("docker-push", "Pushing image")([`docker push ${imageRef}`]),
13176
13532
  ],
13177
13533
  };
13178
13534
  }
13179
13535
  /**
13180
- * the image definitions of all used images, materialized into the
13181
- * generated files (so pipelines build from committed, reviewable
13182
- * files and gitlab's rules:changes can watch them)
13536
+ * the image definitions to materialize into the generated files (so
13537
+ * pipelines build from committed, reviewable files and gitlab's
13538
+ * rules:changes can watch them): the definitions catladder ships and
13539
+ * the project's inline Dockerfiles. A project image directory is
13540
+ * already committed and contributes nothing.
13183
13541
  */
13184
13542
  getGeneratedFiles() {
13185
- return [...this.used.keys()].flatMap((name) => {
13186
- const dir = (0, shippedImages_1.getShippedImageDir)(name);
13187
- return listFilesRecursive(dir).map((file) => ({
13188
- path: (0, path_1.join)(exports.GENERATED_IMAGES_FOLDER, name, file),
13189
- content: (0, fs_1.readFileSync)((0, path_1.join)(dir, file), "utf-8"),
13190
- }));
13191
- });
13543
+ return [...this.used.values(), ...this.usedProject.values()].flatMap(({ files }) => files);
13192
13544
  }
13193
13545
  }
13194
13546
  exports.JobImagesPlan = JobImagesPlan;
13547
+ /**
13548
+ * an inline Dockerfile is written as given — as one string, or as lines
13549
+ * joined with newlines (always newline-terminated)
13550
+ */
13551
+ const toDockerfileContent = (dockerfile) => {
13552
+ const content = Array.isArray(dockerfile)
13553
+ ? dockerfile.join("\n")
13554
+ : dockerfile;
13555
+ return content.endsWith("\n") ? content : `${content}\n`;
13556
+ };
13195
13557
  const IGNORED_FILES = new Set([".DS_Store", "Thumbs.db"]);
13196
13558
  const listFilesRecursive = (dir) => (0, fs_1.readdirSync)(dir, { recursive: true, encoding: "utf-8" })
13197
13559
  .filter((entry) => {
@@ -13205,6 +13567,22 @@ const listFilesRecursive = (dir) => (0, fs_1.readdirSync)(dir, { recursive: true
13205
13567
 
13206
13568
  /***/ }),
13207
13569
 
13570
+ /***/ 55537:
13571
+ /***/ ((__unused_webpack_module, exports) => {
13572
+
13573
+ "use strict";
13574
+
13575
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
13576
+ exports.isProjectImageRef = void 0;
13577
+ const isProjectImageRef = (value) => typeof value === "object" &&
13578
+ value !== null &&
13579
+ "image" in value &&
13580
+ typeof value.image === "string";
13581
+ exports.isProjectImageRef = isProjectImageRef;
13582
+ //# sourceMappingURL=projectImages.js.map
13583
+
13584
+ /***/ }),
13585
+
13208
13586
  /***/ 28687:
13209
13587
  /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
13210
13588
 
@@ -15918,6 +16296,8 @@ __exportStar(__nccwpck_require__(68906), exports);
15918
16296
  __exportStar(__nccwpck_require__(23040), exports);
15919
16297
  __exportStar(__nccwpck_require__(35086), exports);
15920
16298
  __exportStar(__nccwpck_require__(71065), exports);
16299
+ __exportStar(__nccwpck_require__(55537), exports);
16300
+ __exportStar(__nccwpck_require__(47864), exports);
15921
16301
  __exportStar(__nccwpck_require__(80769), exports);
15922
16302
  __exportStar(__nccwpck_require__(7732), exports);
15923
16303
  __exportStar(__nccwpck_require__(66526), exports);