@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
@@ -2321,7 +2321,7 @@ class GithubBackend {
2321
2321
  await (0, promises_1.rm)(scriptFiles_1.GITHUB_SCRIPTS_FOLDER, { recursive: true, force: true });
2322
2322
  }
2323
2323
  async createFiles(config) {
2324
- const images = this.createImagesPlan();
2324
+ const images = this.createImagesPlan(config);
2325
2325
  const scripts = new scriptFiles_1.GithubScriptFiles();
2326
2326
  const workflows = await this.createWorkflows(config, images, scripts);
2327
2327
  return [
@@ -2336,13 +2336,13 @@ class GithubBackend {
2336
2336
  ...(0, shippedCatci_1.getCatciGeneratedFiles)(),
2337
2337
  ];
2338
2338
  }
2339
- createImagesPlan() {
2340
- return new jobImagesPlan_1.JobImagesPlan(this.type);
2339
+ createImagesPlan(config) {
2340
+ return new jobImagesPlan_1.JobImagesPlan(this.type, config.images);
2341
2341
  }
2342
2342
  /**
2343
2343
  * all workflows keyed by file name, mainly for testing purposes
2344
2344
  */
2345
- async createWorkflows(config, images = this.createImagesPlan(), scripts = new scriptFiles_1.GithubScriptFiles()) {
2345
+ async createWorkflows(config, images = this.createImagesPlan(config), scripts = new scriptFiles_1.GithubScriptFiles()) {
2346
2346
  var _a, _b, _c, _d;
2347
2347
  const workflows = {};
2348
2348
  // per-pipeline-type variables (pipelines.github.runnerVariables);
@@ -2463,6 +2463,13 @@ class GithubBackend {
2463
2463
  manualJobConditions[id] = `inputs.job == '${id}'`;
2464
2464
  manualDispatchOptions.push(id);
2465
2465
  }
2466
+ // manual release mode: the workflow that runs a queued release
2467
+ // once the main workflow completed green
2468
+ const releaseOnGreen = (0, githubReleaseJobs_1.getGithubReleaseOnGreenWorkflow)(config, images, TRIGGER_WORKFLOWS.mainBranch.name, workflowEnv);
2469
+ if (releaseOnGreen) {
2470
+ workflows[`${GENERATED_FILE_PREFIX}release-on-green.yml`] =
2471
+ releaseOnGreen;
2472
+ }
2466
2473
  }
2467
2474
  if (Object.keys(jobs).length > 0) {
2468
2475
  workflows[`${GENERATED_FILE_PREFIX}${workflowFileName(trigger)}`] = {
@@ -3007,16 +3014,21 @@ exports.getUploadProviderIds = getUploadProviderIds;
3007
3014
  "use strict";
3008
3015
 
3009
3016
  Object.defineProperty(exports, "__esModule", ({ value: true }));
3010
- exports.getGithubReleaseCheckJobs = exports.getGithubReleaseJobs = void 0;
3017
+ exports.getGithubReleaseCheckJobs = exports.getGithubReleaseOnGreenWorkflow = exports.getGithubReleaseJobs = void 0;
3011
3018
  const createGithubJobs_1 = __nccwpck_require__(57877);
3012
3019
  const release_1 = __nccwpck_require__(38904);
3013
- const createReleaseJob = (config, images, needs, extraEnv) => {
3020
+ const shippedCatci_1 = __nccwpck_require__(79024);
3021
+ const CATCI = `${shippedCatci_1.GENERATED_CATCI_FOLDER}/index.js`;
3022
+ const createReleaseJob = (config, images, options = {}) => {
3023
+ var _a;
3014
3024
  const method = (0, release_1.getReleaseMethod)(config);
3015
3025
  const resolved = images.resolveRef({ catladderImage: method.image });
3016
- const imageNeeds = resolved.need && typeof resolved.need === "object"
3026
+ const imageNeeds = !options.skipImageNeeds &&
3027
+ resolved.need &&
3028
+ typeof resolved.need === "object"
3017
3029
  ? [(0, createGithubJobs_1.githubGlobalJobId)(resolved.need.job)]
3018
3030
  : [];
3019
- const allNeeds = [...imageNeeds, ...(needs !== null && needs !== void 0 ? needs : [])];
3031
+ const allNeeds = [...imageNeeds, ...((_a = options.needs) !== null && _a !== void 0 ? _a : [])];
3020
3032
  return {
3021
3033
  name: "create release",
3022
3034
  "runs-on": "ubuntu-latest",
@@ -3045,7 +3057,7 @@ const createReleaseJob = (config, images, needs, extraEnv) => {
3045
3057
  ...(allNeeds.length > 0 ? { needs: allNeeds } : {}),
3046
3058
  env: {
3047
3059
  GITHUB_TOKEN: "${{ github.token }}",
3048
- ...extraEnv,
3060
+ ...options.extraEnv,
3049
3061
  },
3050
3062
  steps: [
3051
3063
  {
@@ -3055,8 +3067,21 @@ const createReleaseJob = (config, images, needs, extraEnv) => {
3055
3067
  // tags since the last release) to determine the version
3056
3068
  with: { "fetch-depth": 0 },
3057
3069
  },
3070
+ ...(options.queueCheck
3071
+ ? [
3072
+ {
3073
+ name: "check main workflow",
3074
+ id: "queue",
3075
+ run: `node ${CATCI} release github-queue-check`,
3076
+ shell: "bash",
3077
+ },
3078
+ ]
3079
+ : []),
3058
3080
  {
3059
3081
  name: "create release",
3082
+ ...(options.queueCheck
3083
+ ? { if: "steps.queue.outputs.queued != 'true'" }
3084
+ : {}),
3060
3085
  run: method.script,
3061
3086
  shell: "bash",
3062
3087
  },
@@ -3070,7 +3095,10 @@ const createReleaseJob = (config, images, needs, extraEnv) => {
3070
3095
  * workflow after all other jobs; a force variant goes to the manual
3071
3096
  * tasks workflow
3072
3097
  * - otherwise (manual, the default): the release job goes to the
3073
- * manual tasks workflow
3098
+ * manual tasks workflow. Its queue-check step releases right away on
3099
+ * a green main run and otherwise queues the release for the
3100
+ * release-on-green workflow (see getGithubReleaseOnGreenWorkflow) —
3101
+ * the counterpart of gitlab's queue button + executor pair.
3074
3102
  */
3075
3103
  const getGithubReleaseJobs = (config, mainWorkflowJobIds, images) => {
3076
3104
  var _a;
@@ -3078,28 +3106,89 @@ const getGithubReleaseJobs = (config, mainWorkflowJobIds, images) => {
3078
3106
  if (((_a = config.releases) === null || _a === void 0 ? void 0 : _a.when) === "auto") {
3079
3107
  return {
3080
3108
  main: {
3081
- "create-release": createReleaseJob(config, images, mainWorkflowJobIds),
3109
+ "create-release": createReleaseJob(config, images, {
3110
+ needs: mainWorkflowJobIds,
3111
+ }),
3082
3112
  },
3083
3113
  manual: {
3084
- "force-create-release": createReleaseJob(config, images, undefined, method.forceReleaseVariables),
3114
+ "force-create-release": createReleaseJob(config, images, {
3115
+ extraEnv: method.forceReleaseVariables,
3116
+ }),
3085
3117
  },
3086
3118
  };
3087
3119
  }
3088
3120
  return {
3089
3121
  main: {},
3090
3122
  manual: {
3091
- "create-release": createReleaseJob(config, images),
3123
+ "create-release": createReleaseJob(config, images, { queueCheck: true }),
3092
3124
  // an explicit force option wherever the method distinguishes it
3093
3125
  // (e.g. changesets: release a patch bump without changesets)
3094
3126
  ...(method.forceReleaseVariables
3095
3127
  ? {
3096
- "force-create-release": createReleaseJob(config, images, undefined, method.forceReleaseVariables),
3128
+ "force-create-release": createReleaseJob(config, images, {
3129
+ extraEnv: method.forceReleaseVariables,
3130
+ }),
3097
3131
  }
3098
3132
  : {}),
3099
3133
  },
3100
3134
  };
3101
3135
  };
3102
3136
  exports.getGithubReleaseJobs = getGithubReleaseJobs;
3137
+ /**
3138
+ * the release-on-green workflow (manual release mode only): runs on
3139
+ * every completed main workflow run. A lightweight host job checks the
3140
+ * queue marker (written by the create-release task while the main
3141
+ * workflow was still running) — most runs have nothing queued and end
3142
+ * after that single quick job. When the marker matches the completed
3143
+ * run, the containerized release job creates the release.
3144
+ *
3145
+ * No image-build jobs here: the release image was ensured by the main
3146
+ * workflow run that just completed (same commit, same content-hashed
3147
+ * tag).
3148
+ */
3149
+ const getGithubReleaseOnGreenWorkflow = (config, images, mainWorkflowName, workflowEnv) => {
3150
+ var _a;
3151
+ if (((_a = config.releases) === null || _a === void 0 ? void 0 : _a.when) === "auto") {
3152
+ return null;
3153
+ }
3154
+ return {
3155
+ name: "catladder release on green",
3156
+ on: {
3157
+ workflow_run: {
3158
+ workflows: [mainWorkflowName],
3159
+ types: ["completed"],
3160
+ },
3161
+ },
3162
+ env: workflowEnv,
3163
+ jobs: {
3164
+ guard: {
3165
+ name: "check queued release",
3166
+ "runs-on": "ubuntu-latest",
3167
+ // consuming the queue marker is a ref deletion
3168
+ permissions: { contents: "write" },
3169
+ env: { GITHUB_TOKEN: "${{ github.token }}" },
3170
+ outputs: { release: "${{ steps.guard.outputs.release }}" },
3171
+ steps: [
3172
+ { name: "Checkout", uses: "actions/checkout@v4" },
3173
+ {
3174
+ name: "check queued release",
3175
+ id: "guard",
3176
+ run: `node ${CATCI} release github-queued-guard "\${{ github.event.workflow_run.head_sha }}" "\${{ github.event.workflow_run.conclusion }}"`,
3177
+ shell: "bash",
3178
+ },
3179
+ ],
3180
+ },
3181
+ "create-release": {
3182
+ ...createReleaseJob(config, images, {
3183
+ needs: ["guard"],
3184
+ skipImageNeeds: true,
3185
+ }),
3186
+ if: "${{ needs.guard.outputs.release == 'true' }}",
3187
+ },
3188
+ },
3189
+ };
3190
+ };
3191
+ exports.getGithubReleaseOnGreenWorkflow = getGithubReleaseOnGreenWorkflow;
3103
3192
  /**
3104
3193
  * the informational MR/PR check job of the release method (e.g. the
3105
3194
  * changeset check), added to the mr workflow. continue-on-error keeps
@@ -3294,6 +3383,7 @@ const config_1 = __nccwpck_require__(17860);
3294
3383
  const createAllJobs_1 = __nccwpck_require__(54114);
3295
3384
  const jobImagesPlan_1 = __nccwpck_require__(47864);
3296
3385
  const shippedCatci_1 = __nccwpck_require__(79024);
3386
+ const release_1 = __nccwpck_require__(38904);
3297
3387
  const index_1 = __nccwpck_require__(68906);
3298
3388
  const createGitlabJobs_1 = __nccwpck_require__(41509);
3299
3389
  const createGitlabPipeline_1 = __nccwpck_require__(94411);
@@ -3311,7 +3401,7 @@ class GitlabBackend {
3311
3401
  await (0, promises_1.rm)(this.generatedFolder, { force: true, recursive: true });
3312
3402
  }
3313
3403
  async createFiles(config) {
3314
- const images = this.createImagesPlan();
3404
+ const images = this.createImagesPlan(config);
3315
3405
  const includes = await this.createIncludes(config, images);
3316
3406
  const mainFile = {
3317
3407
  path: ".gitlab-ci.yml",
@@ -3328,15 +3418,15 @@ class GitlabBackend {
3328
3418
  ...(0, shippedCatci_1.getCatciGeneratedFiles)(),
3329
3419
  ];
3330
3420
  }
3331
- createImagesPlan() {
3332
- return new jobImagesPlan_1.JobImagesPlan(this.type);
3421
+ createImagesPlan(config) {
3422
+ return new jobImagesPlan_1.JobImagesPlan(this.type, config.images);
3333
3423
  }
3334
3424
  /**
3335
3425
  * the complete pipeline as one object (all includes merged into one),
3336
3426
  * mainly for testing purposes
3337
3427
  */
3338
3428
  async createCompletePipeline(config) {
3339
- const includes = await this.createIncludes(config, this.createImagesPlan());
3429
+ const includes = await this.createIncludes(config, this.createImagesPlan(config));
3340
3430
  return includes.reduce((acc, { content }) => {
3341
3431
  return {
3342
3432
  ...acc,
@@ -3380,12 +3470,29 @@ class GitlabBackend {
3380
3470
  }
3381
3471
  async createPipeline(config, images) {
3382
3472
  const stages = (0, getPipelineStages_1.getPipelineStages)(config);
3383
- // release jobs first: they register their image on the plan, and the
3384
- // per-trigger job creation below emits the build jobs of all images
3385
- // registered so far
3386
- const releaseJobs = (0, gitlabReleaseJobs_1.getGitlabReleaseJobs)(config, images);
3473
+ // register the release image first: the per-trigger job creation
3474
+ // below emits the build jobs of all images registered so far. The
3475
+ // release jobs themselves are created afterwards — they need the
3476
+ // main-branch job list for the queued-release executor's needs.
3477
+ images.resolveRef({ catladderImage: (0, release_1.getReleaseMethod)(config).image });
3387
3478
  // for all triggers create jobs and add base rules
3388
- 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());
3479
+ const jobsPerTrigger = await Promise.all(config_1.ALL_PIPELINE_TRIGGERS.map(async (trigger) => ({
3480
+ trigger,
3481
+ jobs: await (0, createGitlabJobs_1.createGitlabJobs)(await (0, createAllJobs_1.createAllJobs)({ config, trigger, pipelineType: this.type }), images, getGitlabRulesForTrigger(trigger)),
3482
+ })));
3483
+ const allJobsPerTrigger = jobsPerTrigger.flatMap(({ jobs }) => jobs);
3484
+ // the jobs the queued-release executor waits for: everything that
3485
+ // runs automatically in a main-branch pipeline. Manual jobs (stop,
3486
+ // rollback, gates) are excluded — gitlab skips a job whose needs
3487
+ // include an unplayed manual job.
3488
+ const mainBranchJobNames = [
3489
+ ...new Set(jobsPerTrigger
3490
+ .filter(({ trigger }) => trigger === "mainBranch")
3491
+ .flatMap(({ jobs }) => jobs)
3492
+ .filter(({ gitlabJob }) => !isManualGitlabJob(gitlabJob))
3493
+ .map(({ name }) => name)),
3494
+ ];
3495
+ const releaseJobs = (0, gitlabReleaseJobs_1.getGitlabReleaseJobs)(config, images, mainBranchJobNames);
3389
3496
  const allWorkspaceJobs = allJobsPerTrigger
3390
3497
  .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
3391
3498
  // this is purely for better readability in git diffs when you add new components
@@ -3451,6 +3558,14 @@ class GitlabBackend {
3451
3558
  }
3452
3559
  }
3453
3560
  exports.GitlabBackend = GitlabBackend;
3561
+ /**
3562
+ * whether a job can end up waiting for a human in a main-branch
3563
+ * pipeline (catladder expresses manual-ness only through rules)
3564
+ */
3565
+ function isManualGitlabJob(job) {
3566
+ var _a;
3567
+ return ((_a = job.rules) !== null && _a !== void 0 ? _a : []).some((rule) => rule.when === "manual");
3568
+ }
3454
3569
  function getGitlabRulesForTrigger(trigger) {
3455
3570
  // mainBranch: on push to main branch except it's a release commit
3456
3571
  // mr: on merge request
@@ -3983,91 +4098,179 @@ exports.getPipelineStages = getPipelineStages;
3983
4098
  "use strict";
3984
4099
 
3985
4100
  Object.defineProperty(exports, "__esModule", ({ value: true }));
3986
- exports.getGitlabReleaseJobs = void 0;
4101
+ exports.getGitlabReleaseJobs = exports.RELEASE_EXECUTOR_JOB_NAME = exports.RELEASE_BUTTON_JOB_NAME = void 0;
3987
4102
  const rules_1 = __nccwpck_require__(35086);
3988
4103
  const release_1 = __nccwpck_require__(38904);
3989
4104
  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.'";
4105
+ /**
4106
+ * gitlab rejects jobs with more than 50 `needs:` entries (default
4107
+ * limit). Pipelines beyond that fall back to the legacy manual release
4108
+ * job, which is only playable once all prior stages finished.
4109
+ */
4110
+ const GITLAB_MAX_NEEDS = 50;
4111
+ exports.RELEASE_BUTTON_JOB_NAME = "create release";
4112
+ exports.RELEASE_EXECUTOR_JOB_NAME = "🚀 release once pipeline succeeds";
3990
4113
  const baseReleaseRules = [
3991
4114
  rules_1.RULE_NEVER_ON_RELEASE_COMMIT,
3992
4115
  rules_1.RULE_NEVER_ON_AGENT_TRIGGER,
3993
4116
  rules_1.RULE_NEVER_ON_SCHEDULE,
3994
4117
  ];
3995
- const getGitlabReleaseJobs = (config, images) => {
4118
+ // NOTE: with `rules:`, when: manual defaults to allow_failure: false,
4119
+ // which turns every unclicked pipeline into status "blocked" — the
4120
+ // manual release jobs therefore carry allow_failure: true so pipelines
4121
+ // end "passed" when nobody releases
4122
+ const manualReleaseRules = [
4123
+ ...baseReleaseRules,
4124
+ {
4125
+ if: rules_1.RULE_CONDITION_MAIN_BRANCH,
4126
+ when: "manual",
4127
+ },
4128
+ {
4129
+ if: rules_1.RULE_CONDITION_HOTFIX_BRANCH,
4130
+ when: "manual",
4131
+ },
4132
+ ];
4133
+ const getGitlabReleaseJobs = (config, images,
4134
+ /**
4135
+ * names of the jobs that run automatically in main-branch pipelines
4136
+ * (manual jobs excluded: a needed-but-unplayed manual job would make
4137
+ * gitlab skip the release executor)
4138
+ */
4139
+ mainBranchJobNames) => {
3996
4140
  var _a;
3997
4141
  const method = (0, release_1.getReleaseMethod)(config);
3998
4142
  // NOTE: only the resolved image url is used, not the `need`: release
3999
- // jobs have no `needs` on purpose — their stage ordering already
4000
- // guarantees the image build job (setup stage) ran, and adding a
4001
- // `needs` would switch gitlab to DAG mode, so the release job would no
4002
- // longer wait for the build/deploy stages.
4143
+ // jobs have no `needs` on the whole pipeline on purpose — where they
4144
+ // must wait for it, the wait is expressed by enumerating the jobs
4145
+ // explicitly (see the executor below).
4003
4146
  const releaseImage = images.resolveRef({
4004
4147
  catladderImage: method.image,
4005
4148
  }).image;
4006
- return {
4007
- ["create release"]: {
4008
- stage: "release",
4009
- image: releaseImage,
4010
- script: [method.script],
4011
- after_script: [EXPIRED_TOKEN_HELP],
4012
- rules: [
4013
- ...baseReleaseRules,
4014
- {
4015
- if: rules_1.RULE_CONDITION_MAIN_BRANCH,
4016
- when: ((_a = config.releases) === null || _a === void 0 ? void 0 : _a.when) === "auto" ? "on_success" : "manual",
4017
- },
4018
- {
4019
- if: rules_1.RULE_CONDITION_HOTFIX_BRANCH,
4020
- when: "manual",
4021
- },
4022
- ],
4023
- },
4149
+ const releaseJobBase = {
4150
+ stage: "release",
4151
+ image: releaseImage,
4152
+ script: [method.script],
4153
+ after_script: [EXPIRED_TOKEN_HELP],
4154
+ };
4155
+ const forceReleaseJob = {
4024
4156
  ["⚠️ force create release"]: {
4025
- stage: "release",
4026
- image: releaseImage,
4027
- script: [method.script],
4028
- after_script: [EXPIRED_TOKEN_HELP],
4157
+ ...releaseJobBase,
4029
4158
  needs: [],
4159
+ allow_failure: true,
4030
4160
  // force semantics of the method's script (e.g. changesets:
4031
4161
  // release a patch bump even without pending changesets)
4032
4162
  ...(method.forceReleaseVariables
4033
4163
  ? { variables: method.forceReleaseVariables }
4034
4164
  : {}),
4165
+ rules: manualReleaseRules,
4166
+ },
4167
+ };
4168
+ // informational MR check of the release method (e.g. the changeset
4169
+ // check): warns via allow_failure, the report lands in the MR
4170
+ // widget as an exposed artifact
4171
+ const checkJob = method.checkScript
4172
+ ? {
4173
+ ["🦋 changeset check"]: {
4174
+ stage: "test",
4175
+ image: releaseImage,
4176
+ script: [method.checkScript],
4177
+ allow_failure: true,
4178
+ artifacts: {
4179
+ paths: ["changeset-report.md"],
4180
+ expose_as: "changeset report",
4181
+ // the report must survive the warning exit
4182
+ when: "always",
4183
+ },
4184
+ rules: [
4185
+ rules_1.RULE_NEVER_ON_AGENT_TRIGGER,
4186
+ rules_1.RULE_NEVER_ON_SCHEDULE,
4187
+ rules_1.RULE_IS_MERGE_REQUEST,
4188
+ ],
4189
+ },
4190
+ }
4191
+ : {};
4192
+ if (((_a = config.releases) === null || _a === void 0 ? void 0 : _a.when) === "auto") {
4193
+ return {
4194
+ [exports.RELEASE_BUTTON_JOB_NAME]: {
4195
+ ...releaseJobBase,
4196
+ rules: [
4197
+ ...baseReleaseRules,
4198
+ {
4199
+ if: rules_1.RULE_CONDITION_MAIN_BRANCH,
4200
+ when: "on_success",
4201
+ },
4202
+ {
4203
+ if: rules_1.RULE_CONDITION_HOTFIX_BRANCH,
4204
+ when: "manual",
4205
+ // per-rule: only the unclicked hotfix button must not
4206
+ // block — a failed automatic release stays loud
4207
+ allow_failure: true,
4208
+ },
4209
+ ],
4210
+ },
4211
+ ...forceReleaseJob,
4212
+ ...checkJob,
4213
+ };
4214
+ }
4215
+ const executorNeeds = [
4216
+ { job: exports.RELEASE_BUTTON_JOB_NAME },
4217
+ // optional: hotfix pipelines (and rules-filtered jobs) contain a
4218
+ // different job subset
4219
+ ...mainBranchJobNames.map((name) => ({ job: name, optional: true })),
4220
+ ];
4221
+ if (executorNeeds.length > GITLAB_MAX_NEEDS) {
4222
+ console.warn(`⚠️ the main-branch pipeline has more than ${GITLAB_MAX_NEEDS} jobs — ` +
4223
+ `the "${exports.RELEASE_BUTTON_JOB_NAME}" job falls back to being playable ` +
4224
+ `only once the pipeline finished (gitlab caps needs: at ${GITLAB_MAX_NEEDS})`);
4225
+ return {
4226
+ [exports.RELEASE_BUTTON_JOB_NAME]: {
4227
+ ...releaseJobBase,
4228
+ allow_failure: true,
4229
+ rules: manualReleaseRules,
4230
+ },
4231
+ ...forceReleaseJob,
4232
+ ...checkJob,
4233
+ };
4234
+ }
4235
+ return {
4236
+ // the queue button: playable from the first second of the pipeline.
4237
+ // It only records the intent to release — the executor below does
4238
+ // the actual work once every other job succeeded. Clicking after the
4239
+ // pipeline finished works the same (the executor starts right away).
4240
+ [exports.RELEASE_BUTTON_JOB_NAME]: {
4241
+ stage: "release",
4242
+ image: releaseImage,
4243
+ script: [
4244
+ `echo "release queued — '${exports.RELEASE_EXECUTOR_JOB_NAME}' runs it as soon as every other job in this pipeline succeeded"`,
4245
+ ],
4246
+ needs: [],
4247
+ // allow_failure keeps the unclicked button from blocking the
4248
+ // pipeline AND makes the executor's needs treat it as optional
4249
+ // (skipped executor instead of a forever-waiting pipeline)
4250
+ allow_failure: true,
4251
+ rules: manualReleaseRules,
4252
+ },
4253
+ // the executor: never played manually. Skipped when the button was
4254
+ // not clicked; otherwise starts automatically as soon as the button
4255
+ // and every other (non-manual) job of the pipeline succeeded, and
4256
+ // is skipped when any of them failed.
4257
+ [exports.RELEASE_EXECUTOR_JOB_NAME]: {
4258
+ ...releaseJobBase,
4259
+ needs: executorNeeds,
4035
4260
  rules: [
4036
4261
  ...baseReleaseRules,
4037
4262
  {
4038
4263
  if: rules_1.RULE_CONDITION_MAIN_BRANCH,
4039
- when: "manual",
4264
+ when: "on_success",
4040
4265
  },
4041
4266
  {
4042
4267
  if: rules_1.RULE_CONDITION_HOTFIX_BRANCH,
4043
- when: "manual",
4268
+ when: "on_success",
4044
4269
  },
4045
4270
  ],
4046
4271
  },
4047
- // informational MR check of the release method (e.g. the changeset
4048
- // check): warns via allow_failure, the report lands in the MR
4049
- // widget as an exposed artifact
4050
- ...(method.checkScript
4051
- ? {
4052
- ["🦋 changeset check"]: {
4053
- stage: "test",
4054
- image: releaseImage,
4055
- script: [method.checkScript],
4056
- allow_failure: true,
4057
- artifacts: {
4058
- paths: ["changeset-report.md"],
4059
- expose_as: "changeset report",
4060
- // the report must survive the warning exit
4061
- when: "always",
4062
- },
4063
- rules: [
4064
- rules_1.RULE_NEVER_ON_AGENT_TRIGGER,
4065
- rules_1.RULE_NEVER_ON_SCHEDULE,
4066
- rules_1.RULE_IS_MERGE_REQUEST,
4067
- ],
4068
- },
4069
- }
4070
- : {}),
4272
+ ...forceReleaseJob,
4273
+ ...checkJob,
4071
4274
  };
4072
4275
  };
4073
4276
  exports.getGitlabReleaseJobs = getGitlabReleaseJobs;
@@ -6981,7 +7184,7 @@ exports.makeSecretEnvVarMapping = makeSecretEnvVarMapping;
6981
7184
  "use strict";
6982
7185
 
6983
7186
  Object.defineProperty(exports, "__esModule", ({ value: true }));
6984
- exports.computeCustomImageHash = void 0;
7187
+ exports.computeInlineImageHash = exports.computeCustomImageHash = void 0;
6985
7188
  const crypto_1 = __nccwpck_require__(76982);
6986
7189
  const fs_1 = __nccwpck_require__(79896);
6987
7190
  const path_1 = __nccwpck_require__(16928);
@@ -7002,6 +7205,29 @@ function listFilesRecursive(dir) {
7002
7205
  })
7003
7206
  .sort();
7004
7207
  }
7208
+ /**
7209
+ * the parts every image definition hashes on top of its content:
7210
+ * files outside the definition, and the build args
7211
+ */
7212
+ function hashExtras(hash, config) {
7213
+ const watchedPaths = [];
7214
+ // Hash extra paths (for files outside the dir)
7215
+ if (config.hashExtraPaths) {
7216
+ // Extra paths are literal file paths (not globs) for simplicity
7217
+ const extraFiles = [...config.hashExtraPaths].sort();
7218
+ for (const file of extraFiles) {
7219
+ hash.update(file);
7220
+ hash.update((0, fs_1.readFileSync)(file));
7221
+ }
7222
+ watchedPaths.push(...config.hashExtraPaths);
7223
+ }
7224
+ // Hash build args (sorted keys for determinism)
7225
+ if (config.buildArgs) {
7226
+ const sorted = Object.entries(config.buildArgs).sort(([a], [b]) => a.localeCompare(b));
7227
+ hash.update(JSON.stringify(sorted));
7228
+ }
7229
+ return watchedPaths;
7230
+ }
7005
7231
  function computeCustomImageHash(config) {
7006
7232
  var _a;
7007
7233
  const hash = (0, crypto_1.createHash)("sha256");
@@ -7022,27 +7248,29 @@ function computeCustomImageHash(config) {
7022
7248
  }
7023
7249
  watchedPaths.push(`${extraDir}/**/*`);
7024
7250
  }
7025
- // Hash extra paths (for files outside the dir)
7026
- if (config.hashExtraPaths) {
7027
- // Extra paths are literal file paths (not globs) for simplicity
7028
- const extraFiles = [...config.hashExtraPaths].sort();
7029
- for (const file of extraFiles) {
7030
- hash.update(file);
7031
- hash.update((0, fs_1.readFileSync)(file));
7032
- }
7033
- watchedPaths.push(...config.hashExtraPaths);
7034
- }
7035
- // Hash build args (sorted keys for determinism)
7036
- if (config.buildArgs) {
7037
- const sorted = Object.entries(config.buildArgs).sort(([a], [b]) => a.localeCompare(b));
7038
- hash.update(JSON.stringify(sorted));
7039
- }
7251
+ watchedPaths.push(...hashExtras(hash, config));
7040
7252
  return {
7041
7253
  hash: hash.digest("hex").slice(0, 12),
7042
7254
  watchedPaths,
7043
7255
  };
7044
7256
  }
7045
7257
  exports.computeCustomImageHash = computeCustomImageHash;
7258
+ /**
7259
+ * the hash of an image defined by an inline Dockerfile: its content
7260
+ * stands in for the definition dir. The build context is deliberately
7261
+ * NOT hashed (it may be the whole repository) — `hashExtraPaths` is how
7262
+ * context files opt into the hash.
7263
+ */
7264
+ function computeInlineImageHash(config) {
7265
+ const hash = (0, crypto_1.createHash)("sha256");
7266
+ hash.update(config.dockerfileContent);
7267
+ const watchedPaths = hashExtras(hash, config);
7268
+ return {
7269
+ hash: hash.digest("hex").slice(0, 12),
7270
+ watchedPaths,
7271
+ };
7272
+ }
7273
+ exports.computeInlineImageHash = computeInlineImageHash;
7046
7274
  //# sourceMappingURL=hash.js.map
7047
7275
 
7048
7276
  /***/ }),
@@ -7053,7 +7281,7 @@ exports.computeCustomImageHash = computeCustomImageHash;
7053
7281
  "use strict";
7054
7282
 
7055
7283
  Object.defineProperty(exports, "__esModule", ({ value: true }));
7056
- exports.JobImagesPlan = exports.GENERATED_IMAGES_FOLDER = void 0;
7284
+ exports.JobImagesPlan = exports.PROJECT_IMAGES_FOLDER = exports.GENERATED_IMAGES_FOLDER = void 0;
7057
7285
  const fs_1 = __nccwpck_require__(79896);
7058
7286
  const path_1 = __nccwpck_require__(16928);
7059
7287
  const ciVariables_1 = __nccwpck_require__(72700);
@@ -7061,6 +7289,7 @@ const docker_1 = __nccwpck_require__(83777);
7061
7289
  const runner_1 = __nccwpck_require__(71065);
7062
7290
  const gitlab_1 = __nccwpck_require__(37393);
7063
7291
  const hash_1 = __nccwpck_require__(34092);
7292
+ const projectImages_1 = __nccwpck_require__(55537);
7064
7293
  const shippedImages_1 = __nccwpck_require__(28687);
7065
7294
  exports.GENERATED_IMAGES_FOLDER = ".catladder-generated/images";
7066
7295
  /**
@@ -7068,7 +7297,44 @@ exports.GENERATED_IMAGES_FOLDER = ".catladder-generated/images";
7068
7297
  * catladder image, so building images never depends on one (bootstrap)
7069
7298
  */
7070
7299
  const BUILDER_IMAGE = "docker.io/docker:29.5.1";
7071
- const ensureJobName = (name) => `🐳 job image ${name}`;
7300
+ /**
7301
+ * every planned image is consumed the same way: its url, plus an
7302
+ * optional dependency on its build job
7303
+ */
7304
+ const toResolvedJobImage = ({ imageRef, jobName, }) => ({
7305
+ image: imageRef,
7306
+ fromRepoRegistry: true,
7307
+ need: {
7308
+ job: jobName,
7309
+ artifacts: false,
7310
+ optional: true,
7311
+ global: true,
7312
+ },
7313
+ });
7314
+ /**
7315
+ * materializes an image definition directory into the generated files
7316
+ * (used for the definitions catladder ships)
7317
+ */
7318
+ const materializeDir = (sourceDir, targetDir) => listFilesRecursive(sourceDir).map((file) => ({
7319
+ path: (0, path_1.join)(targetDir, file),
7320
+ content: (0, fs_1.readFileSync)((0, path_1.join)(sourceDir, file), "utf-8"),
7321
+ }));
7322
+ // built-in (catladder-shipped) images carry the catladder marker in
7323
+ // their job name; project-declared images get the short plain name
7324
+ const ensureJobName = (name) => `🐳 catladder image ${name}`;
7325
+ const projectEnsureJobName = (name) => `🐳 image ${name}`;
7326
+ /**
7327
+ * the config file whose content feeds the project image hashes
7328
+ * (`buildArgs`, `hashExtraPaths`): the gitlab change detection has to
7329
+ * watch it, because a config-only change alters the image tag without
7330
+ * touching the image dir
7331
+ */
7332
+ const CONFIG_FILE = "catladder.ts";
7333
+ /**
7334
+ * where inline project Dockerfiles are materialized — a subfolder, so a
7335
+ * project image can carry the same name as a shipped one
7336
+ */
7337
+ exports.PROJECT_IMAGES_FOLDER = `${exports.GENERATED_IMAGES_FOLDER}/project`;
7072
7338
  /**
7073
7339
  * plans the catladder job images of one backend run: resolves image
7074
7340
  * references, collects the image build jobs, and the image definitions
@@ -7079,33 +7345,30 @@ const ensureJobName = (name) => `🐳 job image ${name}`;
7079
7345
  * Reference-driven: only images actually used by a job are built.
7080
7346
  */
7081
7347
  class JobImagesPlan {
7082
- constructor(pipelineType) {
7348
+ constructor(pipelineType, projectImages = {}) {
7083
7349
  this.pipelineType = pipelineType;
7350
+ this.projectImages = projectImages;
7084
7351
  this.used = new Map();
7352
+ this.usedProject = new Map();
7085
7353
  }
7086
7354
  /**
7087
7355
  * resolves a job image (marker or concrete) to a concrete image url
7088
7356
  * plus the build job dependency
7089
7357
  */
7090
7358
  resolve(image) {
7091
- if (!(0, runner_1.isCatladderImageRef)(image)) {
7092
- return { image };
7359
+ if ((0, runner_1.isCatladderImageRef)(image)) {
7360
+ return this.resolveRef(image);
7093
7361
  }
7094
- return this.resolveRef(image);
7362
+ if ((0, projectImages_1.isProjectImageRef)(image)) {
7363
+ return this.resolveProjectRef(image);
7364
+ }
7365
+ return { image };
7095
7366
  }
7096
7367
  resolveRef(ref) {
7097
- const name = ref.catladderImage;
7098
- const { hash } = this.use(name);
7099
- return {
7100
- image: `${this.registryImageRef()}/catladder/${name}:${hash}`,
7101
- fromRepoRegistry: true,
7102
- need: {
7103
- job: ensureJobName(name),
7104
- artifacts: false,
7105
- optional: true,
7106
- global: true,
7107
- },
7108
- };
7368
+ return toResolvedJobImage(this.use(ref.catladderImage));
7369
+ }
7370
+ resolveProjectRef(ref) {
7371
+ return toResolvedJobImage(this.useProject(ref.image));
7109
7372
  }
7110
7373
  /**
7111
7374
  * the registry image prefix. On github, container images can't use
@@ -7116,6 +7379,85 @@ class JobImagesPlan {
7116
7379
  ? "ghcr.io/${{ github.repository }}"
7117
7380
  : `${(0, ciVariables_1.getCiVariable)({ pipelineType: this.pipelineType }, "registryImage")}`;
7118
7381
  }
7382
+ useProject(name) {
7383
+ const existing = this.usedProject.get(name);
7384
+ if (existing) {
7385
+ return existing;
7386
+ }
7387
+ const imageConfig = this.projectImages[name];
7388
+ if (!imageConfig) {
7389
+ const declared = Object.keys(this.projectImages);
7390
+ throw new Error(`unknown job image "${name}" — ${declared.length > 0
7391
+ ? `images declared in config.images: ${declared.join(", ")}`
7392
+ : "no images are declared in config.images"}`);
7393
+ }
7394
+ const source = this.planProjectSource(name, imageConfig);
7395
+ const planned = {
7396
+ ...source,
7397
+ jobName: projectEnsureJobName(name),
7398
+ buildArgs: imageConfig.buildArgs,
7399
+ // `job-images/` keeps the project's job images apart from its
7400
+ // deployable component images (registry root) and build caches
7401
+ imageRef: `${this.registryImageRef()}/job-images/${name}:${source.hash}`,
7402
+ };
7403
+ this.usedProject.set(name, planned);
7404
+ return planned;
7405
+ }
7406
+ /**
7407
+ * the source-dependent half of a project image: an inline Dockerfile
7408
+ * is materialized into the generated files and hashed by its content,
7409
+ * a directory is hashed and watched in place
7410
+ */
7411
+ planProjectSource(name, imageConfig) {
7412
+ var _a, _b;
7413
+ if (imageConfig.dockerfile !== undefined) {
7414
+ const content = toDockerfileContent(imageConfig.dockerfile);
7415
+ const dockerfilePath = `${exports.PROJECT_IMAGES_FOLDER}/${name}/Dockerfile`;
7416
+ const { hash, watchedPaths } = (0, hash_1.computeInlineImageHash)({
7417
+ dockerfileContent: content,
7418
+ hashExtraPaths: imageConfig.hashExtraPaths,
7419
+ buildArgs: imageConfig.buildArgs,
7420
+ });
7421
+ return {
7422
+ hash,
7423
+ dockerfilePath,
7424
+ // an inline definition carries no directory of its own — the
7425
+ // repository root is the sensible default context
7426
+ contextDir: (_a = imageConfig.context) !== null && _a !== void 0 ? _a : ".",
7427
+ watchedPaths: [
7428
+ `${exports.PROJECT_IMAGES_FOLDER}/${name}/**/*`,
7429
+ ...watchedPaths,
7430
+ CONFIG_FILE,
7431
+ ],
7432
+ files: [{ path: dockerfilePath, content }],
7433
+ };
7434
+ }
7435
+ const dir = imageConfig.dir;
7436
+ // fail at generation time with the offending image named: an
7437
+ // unreadable dir would otherwise surface as a bare fs ENOENT, and a
7438
+ // missing Dockerfile only as a docker build failure in CI
7439
+ if (!(0, fs_1.existsSync)(dir)) {
7440
+ throw new Error(`job image "${name}": the directory "${dir}" does not exist (config.images.${name}.dir, relative to the repository root)`);
7441
+ }
7442
+ if (!(0, fs_1.existsSync)((0, path_1.join)(dir, "Dockerfile"))) {
7443
+ throw new Error(`job image "${name}": no Dockerfile in "${dir}" (config.images.${name}.dir)`);
7444
+ }
7445
+ const { hash, watchedPaths } = (0, hash_1.computeCustomImageHash)({
7446
+ dir,
7447
+ hashExtraPaths: imageConfig.hashExtraPaths,
7448
+ buildArgs: imageConfig.buildArgs,
7449
+ });
7450
+ return {
7451
+ hash,
7452
+ dockerfilePath: `${dir}/Dockerfile`,
7453
+ contextDir: (_b = imageConfig.context) !== null && _b !== void 0 ? _b : dir,
7454
+ // the dir is committed in the project — watched in place, nothing
7455
+ // is materialized. The config file is watched too: buildArgs /
7456
+ // hashExtraPaths changes alter the tag without touching the dir
7457
+ watchedPaths: [...watchedPaths, CONFIG_FILE],
7458
+ files: [],
7459
+ };
7460
+ }
7119
7461
  use(name) {
7120
7462
  var _a;
7121
7463
  const existing = this.used.get(name);
@@ -7130,27 +7472,43 @@ class JobImagesPlan {
7130
7472
  dir: (0, shippedImages_1.getShippedImageDir)(name),
7131
7473
  extraDirs: dependencies.map((dependency) => (0, shippedImages_1.getShippedImageDir)(dependency)),
7132
7474
  });
7133
- const entry = { hash };
7134
- this.used.set(name, entry);
7135
- return entry;
7475
+ const targetDir = `${exports.GENERATED_IMAGES_FOLDER}/${name}`;
7476
+ const planned = {
7477
+ hash,
7478
+ jobName: ensureJobName(name),
7479
+ imageRef: `${this.registryImageRef()}/catladder/${name}:${hash}`,
7480
+ dockerfilePath: `${targetDir}/Dockerfile`,
7481
+ // the jobs family INCLUDEs sibling images, so it builds from the
7482
+ // images root; the others from their own dir
7483
+ contextDir: shippedImages_1.RUNNER_IMAGE_BUILD_CONTEXT[name] === "root"
7484
+ ? exports.GENERATED_IMAGES_FOLDER
7485
+ : targetDir,
7486
+ watchedPaths: [name, ...dependencies].map((dir) => `${exports.GENERATED_IMAGES_FOLDER}/${dir}/**/*`),
7487
+ // pipelines build from committed, reviewable files (and gitlab's
7488
+ // rules:changes can watch them)
7489
+ files: materializeDir((0, shippedImages_1.getShippedImageDir)(name), targetDir),
7490
+ };
7491
+ this.used.set(name, planned);
7492
+ return planned;
7136
7493
  }
7137
7494
  /**
7138
7495
  * the image build jobs for all used images
7139
7496
  */
7140
7497
  getEnsureJobs() {
7141
- return [...this.used.entries()].map(([name, { hash }]) => this.createEnsureJob(name, hash));
7498
+ return [...this.used.values(), ...this.usedProject.values()].map((planned) => this.createImageBuildJob(planned));
7142
7499
  }
7143
- createEnsureJob(name, hash) {
7144
- var _a;
7500
+ createImageBuildJob({ jobName, imageRef, watchedPaths, dockerfilePath, contextDir, buildArgs, }) {
7501
+ const buildArgFlags = Object.entries(buildArgs !== null && buildArgs !== void 0 ? buildArgs : {})
7502
+ .map(([key, value]) => ` --build-arg '${key}=${value}'`)
7503
+ .join("");
7504
+ const buildCommand = `DOCKER_BUILDKIT=1 docker build -t ${imageRef}${buildArgFlags} -f ${dockerfilePath} ${contextDir}`;
7145
7505
  const ctx = { pipelineType: this.pipelineType };
7146
7506
  const registryUser = (0, ciVariables_1.getCiVariable)(ctx, "registryUser");
7147
7507
  const jobToken = (0, ciVariables_1.getCiVariable)(ctx, "jobToken");
7148
7508
  const registry = (0, ciVariables_1.getCiVariable)(ctx, "registry");
7149
- const imageRef = `${this.registryImageRef()}/catladder/${name}:${hash}`;
7150
- const watchedDirs = [name, ...((_a = shippedImages_1.RUNNER_IMAGE_DEPENDENCIES[name]) !== null && _a !== void 0 ? _a : [])];
7151
7509
  const { services, runnerVariables } = (0, docker_1.getDockerJobBaseProps)();
7152
7510
  return {
7153
- name: ensureJobName(name),
7511
+ name: jobName,
7154
7512
  stage: "setup",
7155
7513
  image: BUILDER_IMAGE,
7156
7514
  services,
@@ -7160,7 +7518,9 @@ class JobImagesPlan {
7160
7518
  // no equivalent and always runs (the existence check is fast)
7161
7519
  rules: [
7162
7520
  {
7163
- changes: watchedDirs.map((dir) => `${exports.GENERATED_IMAGES_FOLDER}/${dir}/**/*`),
7521
+ // a fresh array per job: the yaml serializer anchors repeated
7522
+ // objects, and a shared reference would reshuffle anchors
7523
+ changes: [...watchedPaths],
7164
7524
  },
7165
7525
  ],
7166
7526
  variables: {},
@@ -7174,31 +7534,33 @@ class JobImagesPlan {
7174
7534
  ` exit 0`,
7175
7535
  `fi`,
7176
7536
  ]),
7177
- ...(0, gitlab_1.collapseableSection)("docker-build", "Building image")([
7178
- `DOCKER_BUILDKIT=1 docker build -t ${imageRef} -f ${exports.GENERATED_IMAGES_FOLDER}/${name}/Dockerfile ${shippedImages_1.RUNNER_IMAGE_BUILD_CONTEXT[name] === "root"
7179
- ? exports.GENERATED_IMAGES_FOLDER
7180
- : `${exports.GENERATED_IMAGES_FOLDER}/${name}`}`,
7181
- ]),
7537
+ ...(0, gitlab_1.collapseableSection)("docker-build", "Building image")([buildCommand]),
7182
7538
  ...(0, gitlab_1.collapseableSection)("docker-push", "Pushing image")([`docker push ${imageRef}`]),
7183
7539
  ],
7184
7540
  };
7185
7541
  }
7186
7542
  /**
7187
- * the image definitions of all used images, materialized into the
7188
- * generated files (so pipelines build from committed, reviewable
7189
- * files and gitlab's rules:changes can watch them)
7543
+ * the image definitions to materialize into the generated files (so
7544
+ * pipelines build from committed, reviewable files and gitlab's
7545
+ * rules:changes can watch them): the definitions catladder ships and
7546
+ * the project's inline Dockerfiles. A project image directory is
7547
+ * already committed and contributes nothing.
7190
7548
  */
7191
7549
  getGeneratedFiles() {
7192
- return [...this.used.keys()].flatMap((name) => {
7193
- const dir = (0, shippedImages_1.getShippedImageDir)(name);
7194
- return listFilesRecursive(dir).map((file) => ({
7195
- path: (0, path_1.join)(exports.GENERATED_IMAGES_FOLDER, name, file),
7196
- content: (0, fs_1.readFileSync)((0, path_1.join)(dir, file), "utf-8"),
7197
- }));
7198
- });
7550
+ return [...this.used.values(), ...this.usedProject.values()].flatMap(({ files }) => files);
7199
7551
  }
7200
7552
  }
7201
7553
  exports.JobImagesPlan = JobImagesPlan;
7554
+ /**
7555
+ * an inline Dockerfile is written as given — as one string, or as lines
7556
+ * joined with newlines (always newline-terminated)
7557
+ */
7558
+ const toDockerfileContent = (dockerfile) => {
7559
+ const content = Array.isArray(dockerfile)
7560
+ ? dockerfile.join("\n")
7561
+ : dockerfile;
7562
+ return content.endsWith("\n") ? content : `${content}\n`;
7563
+ };
7202
7564
  const IGNORED_FILES = new Set([".DS_Store", "Thumbs.db"]);
7203
7565
  const listFilesRecursive = (dir) => (0, fs_1.readdirSync)(dir, { recursive: true, encoding: "utf-8" })
7204
7566
  .filter((entry) => {
@@ -7212,6 +7574,22 @@ const listFilesRecursive = (dir) => (0, fs_1.readdirSync)(dir, { recursive: true
7212
7574
 
7213
7575
  /***/ }),
7214
7576
 
7577
+ /***/ 55537:
7578
+ /***/ ((__unused_webpack_module, exports) => {
7579
+
7580
+ "use strict";
7581
+
7582
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
7583
+ exports.isProjectImageRef = void 0;
7584
+ const isProjectImageRef = (value) => typeof value === "object" &&
7585
+ value !== null &&
7586
+ "image" in value &&
7587
+ typeof value.image === "string";
7588
+ exports.isProjectImageRef = isProjectImageRef;
7589
+ //# sourceMappingURL=projectImages.js.map
7590
+
7591
+ /***/ }),
7592
+
7215
7593
  /***/ 28687:
7216
7594
  /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
7217
7595
 
@@ -9925,6 +10303,8 @@ __exportStar(__nccwpck_require__(68906), exports);
9925
10303
  __exportStar(__nccwpck_require__(23040), exports);
9926
10304
  __exportStar(__nccwpck_require__(35086), exports);
9927
10305
  __exportStar(__nccwpck_require__(71065), exports);
10306
+ __exportStar(__nccwpck_require__(55537), exports);
10307
+ __exportStar(__nccwpck_require__(47864), exports);
9928
10308
  __exportStar(__nccwpck_require__(80769), exports);
9929
10309
  __exportStar(__nccwpck_require__(7732), exports);
9930
10310
  __exportStar(__nccwpck_require__(66526), exports);