@catladder/cli 5.0.1 → 5.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/dist/apps/cli/src/apps/cli/commands/project/doctor/checkGithub.js +3 -3
  2. package/dist/apps/cli/src/apps/cli/commands/project/doctor/checkGithub.js.map +1 -1
  3. package/dist/apps/cli/src/apps/cli/commands/project/doctor/checkGithubImageCasing.d.ts +15 -0
  4. package/dist/apps/cli/src/apps/cli/commands/project/doctor/checkGithubImageCasing.js +60 -0
  5. package/dist/apps/cli/src/apps/cli/commands/project/doctor/checkGithubImageCasing.js.map +1 -0
  6. package/dist/apps/cli/src/apps/cli/commands/project/doctor/index.js +2 -0
  7. package/dist/apps/cli/src/apps/cli/commands/project/doctor/index.js.map +1 -1
  8. package/dist/apps/cli/src/apps/cli/commands/project/setup/setupAccessTokens.js +2 -2
  9. package/dist/apps/cli/src/apps/cli/commands/project/setup/setupAccessTokens.js.map +1 -1
  10. package/dist/apps/cli/src/apps/cli/commands/project/setup/setupGithub.js +2 -2
  11. package/dist/apps/cli/src/apps/cli/commands/project/setup/setupGithub.js.map +1 -1
  12. package/dist/apps/cli/src/apps/cli/commands/project/setup/setupKubernetes.js +23 -7
  13. package/dist/apps/cli/src/apps/cli/commands/project/setup/setupKubernetes.js.map +1 -1
  14. package/dist/apps/cli/src/commands/index.d.ts +1 -0
  15. package/dist/apps/cli/src/commands/index.js +4 -1
  16. package/dist/apps/cli/src/commands/index.js.map +1 -1
  17. package/dist/apps/cli/src/commands/project/commandConfigSecrets.js +2 -1
  18. package/dist/apps/cli/src/commands/project/commandConfigSecrets.js.map +1 -1
  19. package/dist/apps/cli/src/commands/project/commandMrPin.d.ts +21 -0
  20. package/dist/apps/cli/src/commands/project/commandMrPin.js +119 -0
  21. package/dist/apps/cli/src/commands/project/commandMrPin.js.map +1 -0
  22. package/dist/apps/cli/src/commands/project/commandSecretsSyncGithub.d.ts +1 -42
  23. package/dist/apps/cli/src/commands/project/commandSecretsSyncGithub.js +5 -124
  24. package/dist/apps/cli/src/commands/project/commandSecretsSyncGithub.js.map +1 -1
  25. package/dist/apps/cli/src/commands/project/secrets/commandSecretsPush.js +2 -1
  26. package/dist/apps/cli/src/commands/project/secrets/commandSecretsPush.js.map +1 -1
  27. package/dist/apps/cli/src/commands/project/secrets/commandSecretsSet.js +2 -2
  28. package/dist/apps/cli/src/commands/project/secrets/commandSecretsSet.js.map +1 -1
  29. package/dist/apps/cli/src/commands/project/secrets/document.d.ts +1 -12
  30. package/dist/apps/cli/src/commands/project/secrets/document.js +1 -79
  31. package/dist/apps/cli/src/commands/project/secrets/document.js.map +1 -1
  32. package/dist/apps/cli/src/gcloud/serviceAccounts.js +15 -4
  33. package/dist/apps/cli/src/gcloud/serviceAccounts.js.map +1 -1
  34. package/dist/apps/cli/src/secrets/github.d.ts +41 -0
  35. package/dist/apps/cli/src/secrets/github.js +127 -0
  36. package/dist/apps/cli/src/secrets/github.js.map +1 -0
  37. package/dist/apps/cli/src/secrets/index.d.ts +2 -0
  38. package/dist/apps/cli/src/secrets/index.js +19 -0
  39. package/dist/apps/cli/src/secrets/index.js.map +1 -0
  40. package/dist/apps/cli/src/secrets/writeSecrets.d.ts +18 -0
  41. package/dist/apps/cli/src/secrets/writeSecrets.js +88 -0
  42. package/dist/apps/cli/src/secrets/writeSecrets.js.map +1 -0
  43. package/dist/apps/cli/src/utils/github.js +2 -2
  44. package/dist/apps/cli/src/utils/github.js.map +1 -1
  45. package/dist/apps/cli/src/utils/gitlab.d.ts +10 -0
  46. package/dist/apps/cli/src/utils/gitlab.js +30 -5
  47. package/dist/apps/cli/src/utils/gitlab.js.map +1 -1
  48. package/dist/apps/cli/src/vault/BitwardenVault.d.ts +6 -0
  49. package/dist/apps/cli/src/vault/BitwardenVault.js +11 -1
  50. package/dist/apps/cli/src/vault/BitwardenVault.js.map +1 -1
  51. package/dist/apps/cli/src/vault/GitlabVault.d.ts +2 -2
  52. package/dist/apps/cli/src/vault/GitlabVault.js +3 -2
  53. package/dist/apps/cli/src/vault/GitlabVault.js.map +1 -1
  54. package/dist/apps/cli/src/vault/VaultManager.d.ts +2 -2
  55. package/dist/apps/cli/src/vault/VaultManager.js +2 -2
  56. package/dist/apps/cli/src/vault/VaultManager.js.map +1 -1
  57. package/dist/apps/cli/src/vault/types.d.ts +16 -2
  58. package/dist/bundles/catenv/index.js +265 -28
  59. package/dist/bundles/cli/index.js +851 -329
  60. package/dist/bundles/skills/catladder-cli/references/commands.md +13 -0
  61. package/dist/bundles/skills/catladder-config/SKILL.md +3 -1
  62. package/dist/bundles/skills/catladder-deploys/SKILL.md +7 -0
  63. package/dist/bundles/skills/catladder-migrate-ci-backend/SKILL.md +18 -2
  64. package/dist/bundles/skills/catladder-pipelines/SKILL.md +51 -1
  65. package/dist/bundles/skills/catladder-secrets/SKILL.md +16 -0
  66. package/dist/packages/pipeline/src/autoStop.d.ts +22 -0
  67. package/dist/packages/pipeline/src/autoStop.js +30 -0
  68. package/dist/packages/pipeline/src/autoStop.js.map +1 -0
  69. package/dist/packages/pipeline/src/backends/github/GithubBackend.js +11 -5
  70. package/dist/packages/pipeline/src/backends/github/GithubBackend.js.map +1 -1
  71. package/dist/packages/pipeline/src/backends/github/ciVariables.d.ts +13 -2
  72. package/dist/packages/pipeline/src/backends/github/ciVariables.js +18 -5
  73. package/dist/packages/pipeline/src/backends/github/ciVariables.js.map +1 -1
  74. package/dist/packages/pipeline/src/backends/github/createGithubJobs.js +1 -1
  75. package/dist/packages/pipeline/src/backends/github/createGithubJobs.js.map +1 -1
  76. package/dist/packages/pipeline/src/backends/github/ghcr.d.ts +29 -0
  77. package/dist/packages/pipeline/src/backends/github/ghcr.js +40 -0
  78. package/dist/packages/pipeline/src/backends/github/ghcr.js.map +1 -0
  79. package/dist/packages/pipeline/src/backends/github/index.d.ts +2 -0
  80. package/dist/packages/pipeline/src/backends/github/index.js +2 -0
  81. package/dist/packages/pipeline/src/backends/github/index.js.map +1 -1
  82. package/dist/packages/pipeline/src/backends/github/registryImage.d.ts +20 -0
  83. package/dist/packages/pipeline/src/backends/github/registryImage.js +49 -0
  84. package/dist/packages/pipeline/src/backends/github/registryImage.js.map +1 -0
  85. package/dist/packages/pipeline/src/backends/gitlab/GitlabBackend.js +2 -0
  86. package/dist/packages/pipeline/src/backends/gitlab/GitlabBackend.js.map +1 -1
  87. package/dist/packages/pipeline/src/backends/gitlab/createGitlabPipeline.d.ts +4 -1
  88. package/dist/packages/pipeline/src/backends/gitlab/createGitlabPipeline.js +21 -1
  89. package/dist/packages/pipeline/src/backends/gitlab/createGitlabPipeline.js.map +1 -1
  90. package/dist/packages/pipeline/src/customImages/jobImagesPlan.d.ts +15 -3
  91. package/dist/packages/pipeline/src/customImages/jobImagesPlan.js +14 -3
  92. package/dist/packages/pipeline/src/customImages/jobImagesPlan.js.map +1 -1
  93. package/dist/packages/pipeline/src/deploy/base/deploy.js +8 -2
  94. package/dist/packages/pipeline/src/deploy/base/deploy.js.map +1 -1
  95. package/dist/packages/pipeline/src/index.d.ts +1 -0
  96. package/dist/packages/pipeline/src/index.js +1 -0
  97. package/dist/packages/pipeline/src/index.js.map +1 -1
  98. package/dist/packages/pipeline/src/types/config.d.ts +44 -0
  99. package/dist/skills/catladder-cli/references/commands.md +13 -0
  100. package/dist/skills/catladder-config/SKILL.md +3 -1
  101. package/dist/skills/catladder-deploys/SKILL.md +7 -0
  102. package/dist/skills/catladder-migrate-ci-backend/SKILL.md +18 -2
  103. package/dist/skills/catladder-pipelines/SKILL.md +51 -1
  104. package/dist/skills/catladder-secrets/SKILL.md +16 -0
  105. package/dist/tsconfig.tsbuildinfo +1 -1
  106. package/package.json +1 -1
  107. package/src/apps/cli/commands/project/doctor/checkGithub.ts +1 -1
  108. package/src/apps/cli/commands/project/doctor/checkGithubImageCasing.ts +87 -0
  109. package/src/apps/cli/commands/project/doctor/index.ts +2 -0
  110. package/src/apps/cli/commands/project/setup/setupAccessTokens.ts +3 -2
  111. package/src/apps/cli/commands/project/setup/setupGithub.ts +1 -1
  112. package/src/apps/cli/commands/project/setup/setupKubernetes.ts +31 -20
  113. package/src/commands/index.ts +1 -0
  114. package/src/commands/project/commandConfigSecrets.ts +1 -1
  115. package/src/commands/project/commandMrPin.ts +184 -0
  116. package/src/commands/project/commandSecretsSyncGithub.ts +7 -154
  117. package/src/commands/project/secrets/commandSecretsPush.ts +2 -5
  118. package/src/commands/project/secrets/commandSecretsSet.ts +2 -2
  119. package/src/commands/project/secrets/document.ts +2 -127
  120. package/src/gcloud/serviceAccounts.ts +16 -7
  121. package/src/secrets/__tests__/writeSecrets.test.ts +147 -0
  122. package/src/secrets/github.ts +157 -0
  123. package/src/secrets/index.ts +2 -0
  124. package/src/secrets/writeSecrets.ts +138 -0
  125. package/src/utils/github.ts +2 -4
  126. package/src/utils/gitlab.ts +34 -7
  127. package/src/vault/BitwardenVault.ts +10 -1
  128. package/src/vault/GitlabVault.ts +9 -2
  129. package/src/vault/VaultManager.ts +3 -2
  130. package/src/vault/__tests__/BitwardenVault.test.ts +88 -0
  131. package/src/vault/types.ts +17 -1
@@ -17,9 +17,23 @@ export type SecretsVault = {
17
17
  readAllSecrets(io: IO | null): Promise<Record<string, string>>;
18
18
  /**
19
19
  * upserts the secrets of one env and component
20
- * (keys as declared in the config, not the raw variable names)
20
+ * (keys as declared in the config, not the raw variable names).
21
+ * Upsert, not replace: values that are not part of `secrets` stay
22
+ * untouched.
21
23
  */
22
- writeSecrets(io: IO, env: string, componentName: string, secrets: Record<string, unknown>): Promise<void>;
24
+ writeSecrets(io: IO, env: string, componentName: string, secrets: Record<string, unknown>, options?: WriteSecretsOptions): Promise<void>;
25
+ };
26
+ export type WriteSecretsOptions = {
27
+ /**
28
+ * keep the previous value around where the store supports it (the
29
+ * gitlab store writes `<KEY>_backup_<timestamp>` variables) —
30
+ * a hand-written secret's only undo.
31
+ *
32
+ * Machine-provisioned credentials (service account keys, kubernetes
33
+ * tokens) are regenerated on demand and their old value is worthless,
34
+ * so `project setup` writes them with `backup: false`.
35
+ */
36
+ backup?: boolean;
23
37
  };
24
38
  /**
25
39
  * how secret resolution may interact with the vault:
@@ -978,7 +978,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
978
978
  return (mod && mod.__esModule) ? mod : { "default": mod };
979
979
  };
980
980
  Object.defineProperty(exports, "__esModule", ({ value: true }));
981
- exports.upsertAllVariables = exports.clearBackups = exports.getVariableValueByRawName = exports.getAllVariables = exports.getProjectInfo = exports.doGitlabRequestAllPages = exports.doGitlabRequest = exports.getGitlabToken = exports.setupGitlabToken = exports.hasGitlabToken = void 0;
981
+ exports.upsertAllVariables = exports.clearBackups = exports.getVariableValueByRawName = exports.getAllVariables = exports.getProjectInfo = exports.doGitlabRequestAllPages = exports.doGitlabRequest = exports.GitlabNotFoundError = exports.getGitlabToken = exports.setupGitlabToken = exports.hasGitlabToken = void 0;
982
982
  const pipeline_1 = __nccwpck_require__(53695);
983
983
  const lodash_es_1 = __nccwpck_require__(50210);
984
984
  const memoizee_1 = __importDefault(__nccwpck_require__(6575));
@@ -1030,6 +1030,20 @@ const getGitlabToken = async (io) => {
1030
1030
  return (0, preferences_1.getPreference)(TOKEN_KEY);
1031
1031
  };
1032
1032
  exports.getGitlabToken = getGitlabToken;
1033
+ /**
1034
+ * a gitlab api call that came back 404. Carries the path so a caller
1035
+ * that wants to treat "does not exist yet" as normal can say so
1036
+ * (`instanceof`) instead of matching on a message.
1037
+ */
1038
+ class GitlabNotFoundError extends Error {
1039
+ constructor(path, host) {
1040
+ super(`gitlab api: ${path} not found on ${host}`);
1041
+ this.path = path;
1042
+ this.host = host;
1043
+ this.name = "GitlabNotFoundError";
1044
+ }
1045
+ }
1046
+ exports.GitlabNotFoundError = GitlabNotFoundError;
1033
1047
  const doGitlabRequest = async (io, path, data = undefined, method = "GET") => {
1034
1048
  const [rootToken, { gitRemoteHost }] = await Promise.all([
1035
1049
  (0, exports.getGitlabToken)(io),
@@ -1051,7 +1065,7 @@ const doGitlabRequest = async (io, path, data = undefined, method = "GET") => {
1051
1065
  return null;
1052
1066
  }
1053
1067
  if (result.status === 404) {
1054
- throw new Error("not found");
1068
+ throw new GitlabNotFoundError(path, gitRemoteHost);
1055
1069
  }
1056
1070
  throw new Error(`Could not send request to gitlab api ${path}: ${result.status} "${result.statusText}".\nResponse: ${JSON.stringify(await result.json(), null, 2)}`);
1057
1071
  };
@@ -1077,9 +1091,20 @@ const doGitlabRequestAllPages = async (io, path) => {
1077
1091
  };
1078
1092
  exports.doGitlabRequestAllPages = doGitlabRequestAllPages;
1079
1093
  const getProjectInfo = async (io) => {
1080
- const { gitRemotePath } = await (0, gitProjectInformation_1.getGitRemoteHostAndPath)();
1081
- const project = await (0, exports.doGitlabRequest)(io, `projects/${encodeURIComponent(gitRemotePath)}`);
1082
- return project;
1094
+ const { gitRemoteHost, gitRemotePath } = await (0, gitProjectInformation_1.getGitRemoteHostAndPath)();
1095
+ try {
1096
+ return await (0, exports.doGitlabRequest)(io, `projects/${encodeURIComponent(gitRemotePath)}`);
1097
+ }
1098
+ catch (e) {
1099
+ if (e instanceof GitlabNotFoundError) {
1100
+ // the classic: the remote is not a gitlab at all (a github one,
1101
+ // say), so the request goes to a host without /api/v4 and every
1102
+ // path answers 404
1103
+ throw new Error(`no gitlab project '${gitRemotePath}' on '${gitRemoteHost}'. ` +
1104
+ "Check the git remote and `pipelines` in catladder.ts — this command talks to the gitlab api.");
1105
+ }
1106
+ throw e;
1107
+ }
1083
1108
  };
1084
1109
  exports.getProjectInfo = getProjectInfo;
1085
1110
  exports.getAllVariables = (0, memoizee_1.default)(async (io, n = 5) => {
@@ -1400,7 +1425,14 @@ class BitwardenVault {
1400
1425
  }
1401
1426
  return secrets;
1402
1427
  }
1428
+ /**
1429
+ * upserts into the item's yaml note. The note is rewritten as a
1430
+ * whole, so the values that are not part of this write have to be
1431
+ * carried over — a single-key write (`secrets-set`, a credential
1432
+ * provisioned by `project setup`) must not drop the rest.
1433
+ */
1403
1434
  async writeSecrets(io, env, componentName, secrets) {
1435
+ var _a;
1404
1436
  const { config, collectionName } = this;
1405
1437
  const bwSession = await this.session(io);
1406
1438
  const collection = await findCollection(bwSession, collectionName);
@@ -1413,7 +1445,10 @@ class BitwardenVault {
1413
1445
  "--search",
1414
1446
  name,
1415
1447
  ])).find((item) => item.name === name);
1416
- const notes = (0, yaml_1.stringify)(secrets);
1448
+ const previous = (existing === null || existing === void 0 ? void 0 : existing.notes)
1449
+ ? ((_a = (0, yaml_1.parse)(existing.notes)) !== null && _a !== void 0 ? _a : {})
1450
+ : {};
1451
+ const notes = (0, yaml_1.stringify)({ ...previous, ...secrets });
1417
1452
  if (existing) {
1418
1453
  await (0, bw_1.runBw)(bwSession, [
1419
1454
  "edit",
@@ -1467,8 +1502,9 @@ class GitlabVault {
1467
1502
  const variables = await (0, gitlab_1.getAllVariables)(io);
1468
1503
  return Object.fromEntries(variables.map((v) => [v.key, v.value]));
1469
1504
  }
1470
- async writeSecrets(io, env, componentName, secrets) {
1471
- await (0, gitlab_1.upsertAllVariables)(io, secrets, env, componentName);
1505
+ async writeSecrets(io, env, componentName, secrets, options = {}) {
1506
+ var _a;
1507
+ await (0, gitlab_1.upsertAllVariables)(io, secrets, env, componentName, (_a = options.backup) !== null && _a !== void 0 ? _a : true);
1472
1508
  }
1473
1509
  }
1474
1510
  exports.GitlabVault = GitlabVault;
@@ -1554,11 +1590,11 @@ class VaultManager {
1554
1590
  * writes secrets of one env/component to the vault and updates the
1555
1591
  * local cache (write-through, "local" env only)
1556
1592
  */
1557
- async writeSecrets(env, componentName, secrets, io = this.io) {
1593
+ async writeSecrets(env, componentName, secrets, io = this.io, options = {}) {
1558
1594
  if (!io) {
1559
1595
  throw new Error("writing to the vault requires an interactive context");
1560
1596
  }
1561
- await this.vault.writeSecrets(io, env, componentName, secrets);
1597
+ await this.vault.writeSecrets(io, env, componentName, secrets, options);
1562
1598
  if (env === CACHED_ENV) {
1563
1599
  await (0, cache_1.updateSecretsCache)(this.vault.id, env, componentName, Object.fromEntries(Object.entries(secrets).map(([key, value]) => [
1564
1600
  key,
@@ -2557,6 +2593,43 @@ exports.getAgentSkillTargets = getAgentSkillTargets;
2557
2593
 
2558
2594
  /***/ }),
2559
2595
 
2596
+ /***/ 72792:
2597
+ /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
2598
+
2599
+ "use strict";
2600
+
2601
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
2602
+ exports.getPinLabelRegexSource = exports.getAutoStopConfig = exports.DEFAULT_PIN_LABEL = exports.DEFAULT_DEV_AUTO_STOP = exports.DEFAULT_REVIEW_AUTO_STOP = exports.REVIEW_AUTO_STOP_VARIABLE = void 0;
2603
+ const lodash_es_1 = __nccwpck_require__(50210);
2604
+ /**
2605
+ * pipeline-level variable holding the `auto_stop_in` of review
2606
+ * environments. Review deploy jobs reference it instead of a literal,
2607
+ * so the pin label's workflow rule can override it to `never` for the
2608
+ * whole pipeline (all components at once).
2609
+ */
2610
+ exports.REVIEW_AUTO_STOP_VARIABLE = "CL_REVIEW_AUTO_STOP";
2611
+ exports.DEFAULT_REVIEW_AUTO_STOP = "1 week";
2612
+ exports.DEFAULT_DEV_AUTO_STOP = "4 weeks";
2613
+ exports.DEFAULT_PIN_LABEL = "catladder::pin-review";
2614
+ const getAutoStopConfig = (config) => {
2615
+ var _a, _b, _c, _d, _e, _f;
2616
+ return ({
2617
+ review: (_b = (_a = config.autoStop) === null || _a === void 0 ? void 0 : _a.review) !== null && _b !== void 0 ? _b : exports.DEFAULT_REVIEW_AUTO_STOP,
2618
+ dev: (_d = (_c = config.autoStop) === null || _c === void 0 ? void 0 : _c.dev) !== null && _d !== void 0 ? _d : exports.DEFAULT_DEV_AUTO_STOP,
2619
+ pinLabel: (_f = (_e = config.autoStop) === null || _e === void 0 ? void 0 : _e.pinLabel) !== null && _f !== void 0 ? _f : exports.DEFAULT_PIN_LABEL,
2620
+ });
2621
+ };
2622
+ exports.getAutoStopConfig = getAutoStopConfig;
2623
+ /**
2624
+ * matches the pin label inside `$CI_MERGE_REQUEST_LABELS`
2625
+ * (comma-separated) — anchored so `pin` never matches `no-pin`
2626
+ */
2627
+ const getPinLabelRegexSource = (pinLabel) => `(^|,)${(0, lodash_es_1.escapeRegExp)(pinLabel).replaceAll("/", "\\/")}(,|$)`;
2628
+ exports.getPinLabelRegexSource = getPinLabelRegexSource;
2629
+ //# sourceMappingURL=autoStop.js.map
2630
+
2631
+ /***/ }),
2632
+
2560
2633
  /***/ 48476:
2561
2634
  /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
2562
2635
 
@@ -2569,6 +2642,7 @@ const path_1 = __nccwpck_require__(16928);
2569
2642
  const createAllJobs_1 = __nccwpck_require__(83477);
2570
2643
  const config_1 = __nccwpck_require__(75591);
2571
2644
  const ciVariables_1 = __nccwpck_require__(17300);
2645
+ const registryImage_1 = __nccwpck_require__(24333);
2572
2646
  const index_1 = __nccwpck_require__(62853);
2573
2647
  const createGithubJobs_1 = __nccwpck_require__(73474);
2574
2648
  const jobImagesPlan_1 = __nccwpck_require__(59623);
@@ -2622,7 +2696,10 @@ class GithubBackend {
2622
2696
  await (0, promises_1.rm)(scriptFiles_1.GITHUB_SCRIPTS_FOLDER, { recursive: true, force: true });
2623
2697
  }
2624
2698
  async createFiles(config) {
2625
- const images = this.createImagesPlan(config);
2699
+ // ghcr rejects uppercase image paths, and `${{ github.repository }}`
2700
+ // interpolates github's display casing — a mixed-case owner or repo
2701
+ // needs a lowercased literal instead (see ./ghcr)
2702
+ const images = this.createImagesPlan(config, await (0, registryImage_1.resolveGithubRegistryImage)(config));
2626
2703
  const scripts = new scriptFiles_1.GithubScriptFiles();
2627
2704
  const workflows = await this.createWorkflows(config, images, scripts);
2628
2705
  return [
@@ -2637,8 +2714,8 @@ class GithubBackend {
2637
2714
  ...(0, shippedCatci_1.getCatciGeneratedFiles)(),
2638
2715
  ];
2639
2716
  }
2640
- createImagesPlan(config) {
2641
- return new jobImagesPlan_1.JobImagesPlan(this.type, config.images);
2717
+ createImagesPlan(config, registryImage) {
2718
+ return new jobImagesPlan_1.JobImagesPlan(this.type, config.images, registryImage);
2642
2719
  }
2643
2720
  /**
2644
2721
  * all workflows keyed by file name, mainly for testing purposes
@@ -2647,9 +2724,11 @@ class GithubBackend {
2647
2724
  var _a, _b, _c, _d;
2648
2725
  const workflows = {};
2649
2726
  // per-pipeline-type variables (pipelines.github.runnerVariables);
2650
- // workflow-level env, so job-level runnerVariables take precedence
2727
+ // workflow-level env, so job-level runnerVariables take precedence.
2728
+ // The images plan is the single source of the ghcr namespace, so the
2729
+ // workflow env and the container images can never disagree.
2651
2730
  const workflowEnv = {
2652
- ...ciVariables_1.GITHUB_INJECTED_WORKFLOW_ENV,
2731
+ ...(0, ciVariables_1.getGithubInjectedWorkflowEnv)(images.registryImageRef()),
2653
2732
  ...(0, index_1.getPipelineOptions)(config, this.type).runnerVariables,
2654
2733
  };
2655
2734
  const reviewStopJobs = {};
@@ -2992,12 +3071,13 @@ exports.makeAggregateCheckJob = makeAggregateCheckJob;
2992
3071
  /***/ }),
2993
3072
 
2994
3073
  /***/ 17300:
2995
- /***/ ((__unused_webpack_module, exports) => {
3074
+ /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
2996
3075
 
2997
3076
  "use strict";
2998
3077
 
2999
3078
  Object.defineProperty(exports, "__esModule", ({ value: true }));
3000
- exports.GITHUB_INJECTED_WORKFLOW_ENV = exports.GITHUB_CI_VARIABLES = void 0;
3079
+ exports.GITHUB_INJECTED_WORKFLOW_ENV_NAMES = exports.getGithubInjectedWorkflowEnv = exports.GITHUB_CI_VARIABLES = void 0;
3080
+ const ghcr_1 = __nccwpck_require__(59011);
3001
3081
  /**
3002
3082
  * how the github backend names the predefined CI variables.
3003
3083
  *
@@ -3018,16 +3098,28 @@ exports.GITHUB_CI_VARIABLES = {
3018
3098
  };
3019
3099
  /**
3020
3100
  * the workflow-level env the backend injects to provide the CL_*
3021
- * variables referenced by {@link GITHUB_CI_VARIABLES}
3101
+ * variables referenced by {@link GITHUB_CI_VARIABLES}.
3102
+ *
3103
+ * `registryImage` is the ghcr namespace to push to — the actions
3104
+ * expression for an all-lowercase repository, a lowercased literal for
3105
+ * a mixed-case one (see `./ghcr`).
3022
3106
  */
3023
- exports.GITHUB_INJECTED_WORKFLOW_ENV = {
3107
+ const getGithubInjectedWorkflowEnv = (registryImage = ghcr_1.GHCR_REPOSITORY_EXPRESSION) => ({
3024
3108
  CL_JOB_TOKEN: "${{ github.token }}",
3025
3109
  CL_REGISTRY: "ghcr.io",
3026
- CL_REGISTRY_IMAGE: "ghcr.io/${{ github.repository }}",
3110
+ CL_REGISTRY_IMAGE: registryImage,
3027
3111
  CL_REGISTRY_USER: "${{ github.actor }}",
3028
3112
  // empty outside of pull request events
3029
3113
  CL_PR_NUMBER: "${{ github.event.number }}",
3030
- };
3114
+ });
3115
+ exports.getGithubInjectedWorkflowEnv = getGithubInjectedWorkflowEnv;
3116
+ /**
3117
+ * the names of the injected workflow-level env — the values differ per
3118
+ * repository (see {@link getGithubInjectedWorkflowEnv}), the names never
3119
+ * do, so lookups that only ask "does the workflow already provide this?"
3120
+ * use this instead.
3121
+ */
3122
+ exports.GITHUB_INJECTED_WORKFLOW_ENV_NAMES = new Set(Object.keys((0, exports.getGithubInjectedWorkflowEnv)()));
3031
3123
  //# sourceMappingURL=ciVariables.js.map
3032
3124
 
3033
3125
  /***/ }),
@@ -3254,7 +3346,7 @@ const getSecretsEnv = (context, kinds) => {
3254
3346
  */
3255
3347
  const getReferencedSecretsEnv = (script, kinds) => Object.fromEntries([...script.matchAll(/\$\{?(CL_[A-Za-z0-9_]+)\}?/g)]
3256
3348
  .map((match) => match[1])
3257
- .filter((name) => !(name in ciVariables_1.GITHUB_INJECTED_WORKFLOW_ENV))
3349
+ .filter((name) => !ciVariables_1.GITHUB_INJECTED_WORKFLOW_ENV_NAMES.has(name))
3258
3350
  .map((name) => [name, vaultValueExpression(name, kinds)]));
3259
3351
  const makeGithubJob = (context, job, allJobs, uploadProviderIds, images, scripts, secretKinds = new Map()) => {
3260
3352
  var _a, _b, _c, _d, _e, _f, _g, _h, _j;
@@ -3518,6 +3610,53 @@ exports.makeEnsureImageGithubJobs = makeEnsureImageGithubJobs;
3518
3610
 
3519
3611
  /***/ }),
3520
3612
 
3613
+ /***/ 59011:
3614
+ /***/ ((__unused_webpack_module, exports) => {
3615
+
3616
+ "use strict";
3617
+
3618
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
3619
+ exports.toGhcrRegistryImage = exports.parseGithubRepoFromRemoteUrl = exports.GHCR_REPOSITORY_EXPRESSION = void 0;
3620
+ /**
3621
+ * the ghcr namespace expressed through the actions context — portable
3622
+ * across forks, and what every all-lowercase repository keeps using
3623
+ */
3624
+ exports.GHCR_REPOSITORY_EXPRESSION = "ghcr.io/${{ github.repository }}";
3625
+ /**
3626
+ * parses `owner/name` out of a github remote url (ssh or https, with or
3627
+ * without the `.git` suffix).
3628
+ *
3629
+ * Shared with the cli (`apps/cli/src/utils/github.ts`) so the two can't
3630
+ * drift apart.
3631
+ */
3632
+ const parseGithubRepoFromRemoteUrl = (url) => { var _a; return (_a = url.match(/github\.com[:/]([\w.-]+\/[\w.-]+?)(\.git)?\s*$/)) === null || _a === void 0 ? void 0 : _a[1]; };
3633
+ exports.parseGithubRepoFromRemoteUrl = parseGithubRepoFromRemoteUrl;
3634
+ /**
3635
+ * the ghcr image prefix for an `owner/name` repository.
3636
+ *
3637
+ * The OCI distribution spec only allows `[a-z0-9]` plus `.`, `_` and `-`
3638
+ * separators in a repository name, so a mixed-case owner or repo (very
3639
+ * common for org logins: `AcmeCorp`, …) makes the docker client reject
3640
+ * every tag before it even reaches the network. Lowercasing is not a
3641
+ * workaround: the org `AcmeCorp` owns the namespace `ghcr.io/acmecorp`,
3642
+ * so the lowercased path is the correct address.
3643
+ *
3644
+ * Returns the `${{ github.repository }}` expression when lowercasing
3645
+ * would change nothing — it is equivalent for those repositories, and
3646
+ * unlike a literal it keeps working in a fork (whose token can only
3647
+ * push to the fork's own namespace).
3648
+ */
3649
+ const toGhcrRegistryImage = (repository) => {
3650
+ const lowercased = repository.toLowerCase();
3651
+ return lowercased === repository
3652
+ ? exports.GHCR_REPOSITORY_EXPRESSION
3653
+ : `ghcr.io/${lowercased}`;
3654
+ };
3655
+ exports.toGhcrRegistryImage = toGhcrRegistryImage;
3656
+ //# sourceMappingURL=ghcr.js.map
3657
+
3658
+ /***/ }),
3659
+
3521
3660
  /***/ 25073:
3522
3661
  /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
3523
3662
 
@@ -3827,12 +3966,70 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
3827
3966
  Object.defineProperty(exports, "__esModule", ({ value: true }));
3828
3967
  __exportStar(__nccwpck_require__(48476), exports);
3829
3968
  __exportStar(__nccwpck_require__(17300), exports);
3969
+ __exportStar(__nccwpck_require__(59011), exports);
3970
+ __exportStar(__nccwpck_require__(24333), exports);
3830
3971
  __exportStar(__nccwpck_require__(61071), exports);
3831
3972
  __exportStar(__nccwpck_require__(82145), exports);
3832
3973
  //# sourceMappingURL=index.js.map
3833
3974
 
3834
3975
  /***/ }),
3835
3976
 
3977
+ /***/ 24333:
3978
+ /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
3979
+
3980
+ "use strict";
3981
+
3982
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
3983
+ exports.resolveGithubRegistryImage = exports.githubRegistryImageFromConfig = void 0;
3984
+ const child_process_1 = __nccwpck_require__(35317);
3985
+ const util_1 = __nccwpck_require__(39023);
3986
+ const index_1 = __nccwpck_require__(62853);
3987
+ const ghcr_1 = __nccwpck_require__(59011);
3988
+ const execFile = (0, util_1.promisify)(child_process_1.execFile);
3989
+ /**
3990
+ * the ghcr image prefix derived from the config alone
3991
+ * (`pipelines.github.repository`), without touching git.
3992
+ *
3993
+ * Used where generation must stay pure and hermetic (tests, examples);
3994
+ * {@link resolveGithubRegistryImage} adds the git remote fallback.
3995
+ */
3996
+ const githubRegistryImageFromConfig = (config) => {
3997
+ const { repository } = (0, index_1.getPipelineOptions)(config, "github");
3998
+ return repository ? (0, ghcr_1.toGhcrRegistryImage)(repository) : undefined;
3999
+ };
4000
+ exports.githubRegistryImageFromConfig = githubRegistryImageFromConfig;
4001
+ /**
4002
+ * the ghcr image prefix to generate with: the configured repository, or
4003
+ * the one the github git remote points at.
4004
+ *
4005
+ * Falls back to the `${{ github.repository }}` expression when the
4006
+ * remote is unavailable (a checkout without the remote, a tarball, …).
4007
+ * That is the pre-existing behavior and correct for every all-lowercase
4008
+ * repository; `project doctor` catches the mixed-case case it is not
4009
+ * correct for.
4010
+ */
4011
+ const resolveGithubRegistryImage = async (config) => {
4012
+ const fromConfig = (0, exports.githubRegistryImageFromConfig)(config);
4013
+ if (fromConfig) {
4014
+ return fromConfig;
4015
+ }
4016
+ const remote = (0, index_1.getPipelineGitRemote)(config, "github");
4017
+ try {
4018
+ const { stdout } = await execFile("git", ["remote", "get-url", remote]);
4019
+ const repository = (0, ghcr_1.parseGithubRepoFromRemoteUrl)(stdout);
4020
+ return repository
4021
+ ? (0, ghcr_1.toGhcrRegistryImage)(repository)
4022
+ : ghcr_1.GHCR_REPOSITORY_EXPRESSION;
4023
+ }
4024
+ catch (_a) {
4025
+ return ghcr_1.GHCR_REPOSITORY_EXPRESSION;
4026
+ }
4027
+ };
4028
+ exports.resolveGithubRegistryImage = resolveGithubRegistryImage;
4029
+ //# sourceMappingURL=registryImage.js.map
4030
+
4031
+ /***/ }),
4032
+
3836
4033
  /***/ 61071:
3837
4034
  /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
3838
4035
 
@@ -3935,6 +4132,7 @@ exports.GitlabBackend = void 0;
3935
4132
  const promises_1 = __nccwpck_require__(91943);
3936
4133
  const rules_1 = __nccwpck_require__(78503);
3937
4134
  const config_1 = __nccwpck_require__(75591);
4135
+ const autoStop_1 = __nccwpck_require__(72792);
3938
4136
  const createAllJobs_1 = __nccwpck_require__(83477);
3939
4137
  const jobImagesPlan_1 = __nccwpck_require__(59623);
3940
4138
  const shippedCatci_1 = __nccwpck_require__(27967);
@@ -4120,6 +4318,7 @@ class GitlabBackend {
4120
4318
  // per-pipeline-type variables (pipelines.gitlab.runnerVariables)
4121
4319
  ...(0, index_1.getPipelineOptions)(config, this.type).runnerVariables,
4122
4320
  },
4321
+ autoStop: (0, autoStop_1.getAutoStopConfig)(config),
4123
4322
  });
4124
4323
  }
4125
4324
  }
@@ -4564,6 +4763,7 @@ function getGitlabNeedsForWorkspaceJob(context, { needs }, allJobs) {
4564
4763
  Object.defineProperty(exports, "__esModule", ({ value: true }));
4565
4764
  exports.createGitlabPipelineWithDefaults = void 0;
4566
4765
  const bash_1 = __nccwpck_require__(94143);
4766
+ const autoStop_1 = __nccwpck_require__(72792);
4567
4767
  const rules_1 = __nccwpck_require__(78503);
4568
4768
  /**
4569
4769
  * gitlab requires a top-level default image for jobs that don't set their
@@ -4572,7 +4772,8 @@ const rules_1 = __nccwpck_require__(78503);
4572
4772
  * build-free image (a catladder job image would drag in a build job).
4573
4773
  */
4574
4774
  const DEFAULT_PIPELINE_IMAGE = "node:22";
4575
- const createGitlabPipelineWithDefaults = ({ image, variables, before_script, ...config }) => {
4775
+ const createGitlabPipelineWithDefaults = ({ image, variables, before_script, autoStop, ...config }) => {
4776
+ const pinLabel = autoStop && autoStop.pinLabel;
4576
4777
  return {
4577
4778
  image: image !== null && image !== void 0 ? image : DEFAULT_PIPELINE_IMAGE,
4578
4779
  variables: {
@@ -4581,6 +4782,10 @@ const createGitlabPipelineWithDefaults = ({ image, variables, before_script, ...
4581
4782
  CACHE_COMPRESSION_LEVEL: "fast", // same as above, but for caches
4582
4783
  TRANSFER_METER_FREQUENCY: "5s", // how often we should update the transfer meter for cache upload/download
4583
4784
  GIT_DEPTH: "1", // no need the full depth
4785
+ // review env lifetime — the pin-label workflow rule below
4786
+ // overrides this to "never" (workflow rule variables take
4787
+ // precedence over pipeline-level variables)
4788
+ ...(pinLabel ? { [autoStop_1.REVIEW_AUTO_STOP_VARIABLE]: autoStop.review } : {}),
4584
4789
  ...(variables !== null && variables !== void 0 ? variables : {}),
4585
4790
  },
4586
4791
  before_script: [
@@ -4597,6 +4802,20 @@ const createGitlabPipelineWithDefaults = ({ image, variables, before_script, ...
4597
4802
  PIPELINE_NAME: "Thinking...",
4598
4803
  },
4599
4804
  },
4805
+ // pinned MRs (rules stop at the first match, so this must come
4806
+ // before the plain merge-request rule)
4807
+ ...(pinLabel
4808
+ ? [
4809
+ {
4810
+ if: `${rules_1.RULE_IS_MERGE_REQUEST.if} && $CI_MERGE_REQUEST_LABELS =~ /${(0, autoStop_1.getPinLabelRegexSource)(pinLabel)}/`,
4811
+ variables: {
4812
+ PIPELINE_ICON: "🐱📌",
4813
+ PIPELINE_NAME: "mr$CI_MERGE_REQUEST_IID - $CI_MERGE_REQUEST_TITLE",
4814
+ [autoStop_1.REVIEW_AUTO_STOP_VARIABLE]: "never",
4815
+ },
4816
+ },
4817
+ ]
4818
+ : []),
4600
4819
  {
4601
4820
  if: rules_1.RULE_IS_MERGE_REQUEST.if,
4602
4821
  variables: {
@@ -7920,6 +8139,7 @@ exports.JobImagesPlan = exports.PROJECT_IMAGES_FOLDER = exports.GENERATED_IMAGES
7920
8139
  const fs_1 = __nccwpck_require__(79896);
7921
8140
  const path_1 = __nccwpck_require__(16928);
7922
8141
  const ciVariables_1 = __nccwpck_require__(91215);
8142
+ const ghcr_1 = __nccwpck_require__(59011);
7923
8143
  const docker_1 = __nccwpck_require__(12354);
7924
8144
  const runner_1 = __nccwpck_require__(92146);
7925
8145
  const gitlab_1 = __nccwpck_require__(86962);
@@ -7980,9 +8200,16 @@ exports.PROJECT_IMAGES_FOLDER = `${exports.GENERATED_IMAGES_FOLDER}/project`;
7980
8200
  * Reference-driven: only images actually used by a job are built.
7981
8201
  */
7982
8202
  class JobImagesPlan {
7983
- constructor(pipelineType, projectImages = {}) {
8203
+ constructor(pipelineType, projectImages = {},
8204
+ /**
8205
+ * the registry image prefix to generate with, overriding the
8206
+ * pipeline type's default. On github this carries the lowercased
8207
+ * ghcr namespace (see `resolveGithubRegistryImage`).
8208
+ */
8209
+ registryImage) {
7984
8210
  this.pipelineType = pipelineType;
7985
8211
  this.projectImages = projectImages;
8212
+ this.registryImage = registryImage;
7986
8213
  this.used = new Map();
7987
8214
  this.usedProject = new Map();
7988
8215
  }
@@ -8007,11 +8234,14 @@ class JobImagesPlan {
8007
8234
  }
8008
8235
  /**
8009
8236
  * the registry image prefix. On github, container images can't use
8010
- * runtime env vars — the expression form is required.
8237
+ * runtime env vars — a literal or an actions expression is required.
8011
8238
  */
8012
8239
  registryImageRef() {
8240
+ if (this.registryImage) {
8241
+ return this.registryImage;
8242
+ }
8013
8243
  return this.pipelineType === "github"
8014
- ? "ghcr.io/${{ github.repository }}"
8244
+ ? ghcr_1.GHCR_REPOSITORY_EXPRESSION
8015
8245
  : `${(0, ciVariables_1.getCiVariable)({ pipelineType: this.pipelineType }, "registryImage")}`;
8016
8246
  }
8017
8247
  useProject(name) {
@@ -8311,6 +8541,7 @@ const docker_1 = __nccwpck_require__(12354);
8311
8541
  const context_1 = __nccwpck_require__(73938);
8312
8542
  const jobs_1 = __nccwpck_require__(98521);
8313
8543
  const utils_1 = __nccwpck_require__(71286);
8544
+ const autoStop_1 = __nccwpck_require__(72792);
8314
8545
  const stop_1 = __nccwpck_require__(35937);
8315
8546
  const variables_1 = __nccwpck_require__(54768);
8316
8547
  exports.DEPLOY_JOB_NAME = "🚀 Deploy";
@@ -8319,10 +8550,15 @@ class DeployJob extends jobs_1.CatladderJob {
8319
8550
  var _a, _b, _c, _d, _e, _f, _g;
8320
8551
  const hasDocker = (0, docker_1.requiresDockerBuild)(context);
8321
8552
  const isStoppable = (0, utils_1.contextIsStoppable)(context);
8553
+ const autoStopConfig = (0, autoStop_1.getAutoStopConfig)(context.fullConfig);
8322
8554
  const autoStop = context.environment.envType === "review"
8323
- ? "1 week"
8555
+ ? // review envs read the lifetime from a pipeline variable, so
8556
+ // the pin label's workflow rule can override it to "never"
8557
+ autoStopConfig.pinLabel === false
8558
+ ? autoStopConfig.review
8559
+ : `$${autoStop_1.REVIEW_AUTO_STOP_VARIABLE}`
8324
8560
  : context.environment.envType === "dev"
8325
- ? "4 weeks"
8561
+ ? autoStopConfig.dev
8326
8562
  : undefined;
8327
8563
  const deployConfig = (_a = context.deploy) === null || _a === void 0 ? void 0 : _a.config;
8328
8564
  // if auto or manual is configured explicitly, use that
@@ -11151,6 +11387,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
11151
11387
  };
11152
11388
  Object.defineProperty(exports, "__esModule", ({ value: true }));
11153
11389
  __exportStar(__nccwpck_require__(46734), exports);
11390
+ __exportStar(__nccwpck_require__(72792), exports);
11154
11391
  __exportStar(__nccwpck_require__(62853), exports);
11155
11392
  __exportStar(__nccwpck_require__(63557), exports);
11156
11393
  __exportStar(__nccwpck_require__(78503), exports);