@catladder/cli 5.2.0 → 5.3.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 (101) hide show
  1. package/dist/apps/cli/src/commands/project/commandPortForward.js +1 -1
  2. package/dist/apps/cli/src/commands/project/commandPortForward.js.map +1 -1
  3. package/dist/apps/cli/src/commands/project/secrets/scope.js +25 -2
  4. package/dist/apps/cli/src/commands/project/secrets/scope.js.map +1 -1
  5. package/dist/apps/cli/src/config/getProjectConfig.js +4 -1
  6. package/dist/apps/cli/src/config/getProjectConfig.js.map +1 -1
  7. package/dist/apps/cli/src/release/changesetCheckJob.js +17 -10
  8. package/dist/apps/cli/src/release/changesetCheckJob.js.map +1 -1
  9. package/dist/apps/cli/src/secrets/github.js +4 -2
  10. package/dist/apps/cli/src/secrets/github.js.map +1 -1
  11. package/dist/bundles/catci/index.js +1 -1
  12. package/dist/bundles/catenv/index.js +402 -111
  13. package/dist/bundles/cli/index.js +432 -116
  14. package/dist/bundles/skills/catladder-config/SKILL.md +34 -2
  15. package/dist/bundles/skills/catladder-deploys/SKILL.md +13 -1
  16. package/dist/bundles/skills/catladder-deploys/references/deploy-types.md +1 -1
  17. package/dist/bundles/skills/catladder-migrate-release-method/SKILL.md +4 -3
  18. package/dist/bundles/skills/catladder-pipelines/SKILL.md +10 -1
  19. package/dist/bundles/skills/catladder-releases/SKILL.md +12 -7
  20. package/dist/bundles/skills/catladder-secrets/SKILL.md +6 -0
  21. package/dist/packages/pipeline/src/backends/github/GithubBackend.js +23 -3
  22. package/dist/packages/pipeline/src/backends/github/GithubBackend.js.map +1 -1
  23. package/dist/packages/pipeline/src/backends/github/createGithubJobs.js +2 -2
  24. package/dist/packages/pipeline/src/backends/github/createGithubJobs.js.map +1 -1
  25. package/dist/packages/pipeline/src/backends/gitlab/GitlabBackend.js +18 -2
  26. package/dist/packages/pipeline/src/backends/gitlab/GitlabBackend.js.map +1 -1
  27. package/dist/packages/pipeline/src/backends/gitlab/createGitlabJobs.js +9 -4
  28. package/dist/packages/pipeline/src/backends/gitlab/createGitlabJobs.js.map +1 -1
  29. package/dist/packages/pipeline/src/backends/gitlab/createGitlabPipeline.d.ts +6 -1
  30. package/dist/packages/pipeline/src/backends/gitlab/createGitlabPipeline.js +8 -1
  31. package/dist/packages/pipeline/src/backends/gitlab/createGitlabPipeline.js.map +1 -1
  32. package/dist/packages/pipeline/src/backends/gitlab/gitlabReleaseJobs.js +5 -3
  33. package/dist/packages/pipeline/src/backends/gitlab/gitlabReleaseJobs.js.map +1 -1
  34. package/dist/packages/pipeline/src/config/configruedEnvs.d.ts +9 -2
  35. package/dist/packages/pipeline/src/config/configruedEnvs.js +54 -5
  36. package/dist/packages/pipeline/src/config/configruedEnvs.js.map +1 -1
  37. package/dist/packages/pipeline/src/context/createAllComponentsContext.d.ts +2 -2
  38. package/dist/packages/pipeline/src/context/createComponentContext.d.ts +2 -2
  39. package/dist/packages/pipeline/src/context/createWorkspaceContext.d.ts +2 -2
  40. package/dist/packages/pipeline/src/context/getEnvConfig.js +35 -19
  41. package/dist/packages/pipeline/src/context/getEnvConfig.js.map +1 -1
  42. package/dist/packages/pipeline/src/context/getEnvInherit.d.ts +20 -0
  43. package/dist/packages/pipeline/src/context/getEnvInherit.js +72 -0
  44. package/dist/packages/pipeline/src/context/getEnvInherit.js.map +1 -0
  45. package/dist/packages/pipeline/src/context/getEnvInstance.d.ts +4 -0
  46. package/dist/packages/pipeline/src/context/{getReviewSlug.js → getEnvInstance.js} +13 -9
  47. package/dist/packages/pipeline/src/context/getEnvInstance.js.map +1 -0
  48. package/dist/packages/pipeline/src/context/getEnvOn.d.ts +13 -0
  49. package/dist/packages/pipeline/src/context/getEnvOn.js +28 -0
  50. package/dist/packages/pipeline/src/context/getEnvOn.js.map +1 -0
  51. package/dist/packages/pipeline/src/context/getEnvType.d.ts +8 -2
  52. package/dist/packages/pipeline/src/context/getEnvType.js +27 -5
  53. package/dist/packages/pipeline/src/context/getEnvType.js.map +1 -1
  54. package/dist/packages/pipeline/src/context/getEnvironment.js +3 -0
  55. package/dist/packages/pipeline/src/context/getEnvironment.js.map +1 -1
  56. package/dist/packages/pipeline/src/context/getEnvironmentContext.js +18 -8
  57. package/dist/packages/pipeline/src/context/getEnvironmentContext.js.map +1 -1
  58. package/dist/packages/pipeline/src/context/getEnvironmentVariables.d.ts +1 -1
  59. package/dist/packages/pipeline/src/context/getEnvironmentVariables.js +4 -4
  60. package/dist/packages/pipeline/src/context/getEnvironmentVariables.js.map +1 -1
  61. package/dist/packages/pipeline/src/context/index.d.ts +1 -0
  62. package/dist/packages/pipeline/src/context/index.js +1 -0
  63. package/dist/packages/pipeline/src/context/index.js.map +1 -1
  64. package/dist/packages/pipeline/src/deploy/base/deploy.js +11 -1
  65. package/dist/packages/pipeline/src/deploy/base/deploy.js.map +1 -1
  66. package/dist/packages/pipeline/src/deploy/cloudRun/artifactsRegistry.js +5 -4
  67. package/dist/packages/pipeline/src/deploy/cloudRun/artifactsRegistry.js.map +1 -1
  68. package/dist/packages/pipeline/src/deploy/cloudRun/cleanup.js +8 -2
  69. package/dist/packages/pipeline/src/deploy/cloudRun/cleanup.js.map +1 -1
  70. package/dist/packages/pipeline/src/deploy/cloudRun/index.js +2 -2
  71. package/dist/packages/pipeline/src/deploy/cloudRun/index.js.map +1 -1
  72. package/dist/packages/pipeline/src/deploy/cloudRun/utils/getServiceName.js +2 -2
  73. package/dist/packages/pipeline/src/deploy/cloudRun/utils/getServiceName.js.map +1 -1
  74. package/dist/packages/pipeline/src/deploy/kubernetes/index.js +3 -3
  75. package/dist/packages/pipeline/src/deploy/kubernetes/index.js.map +1 -1
  76. package/dist/packages/pipeline/src/deploy/pages/deployJob.js +3 -2
  77. package/dist/packages/pipeline/src/deploy/pages/deployJob.js.map +1 -1
  78. package/dist/packages/pipeline/src/deploy/types/googleCloudRun.d.ts +9 -0
  79. package/dist/packages/pipeline/src/types/config.d.ts +126 -9
  80. package/dist/packages/pipeline/src/types/config.js +11 -10
  81. package/dist/packages/pipeline/src/types/config.js.map +1 -1
  82. package/dist/packages/pipeline/src/types/context.d.ts +22 -3
  83. package/dist/packages/pipeline/src/types/context.js.map +1 -1
  84. package/dist/packages/pipeline/src/types/environmentContext.d.ts +29 -0
  85. package/dist/skills/catladder-config/SKILL.md +34 -2
  86. package/dist/skills/catladder-deploys/SKILL.md +13 -1
  87. package/dist/skills/catladder-deploys/references/deploy-types.md +1 -1
  88. package/dist/skills/catladder-migrate-release-method/SKILL.md +4 -3
  89. package/dist/skills/catladder-pipelines/SKILL.md +10 -1
  90. package/dist/skills/catladder-releases/SKILL.md +12 -7
  91. package/dist/skills/catladder-secrets/SKILL.md +6 -0
  92. package/dist/tsconfig.tsbuildinfo +1 -1
  93. package/package.json +1 -1
  94. package/src/commands/project/commandPortForward.ts +1 -1
  95. package/src/commands/project/secrets/scope.ts +38 -2
  96. package/src/config/getProjectConfig.ts +9 -1
  97. package/src/release/__tests__/changesetCheckJob.test.ts +68 -0
  98. package/src/release/changesetCheckJob.ts +18 -10
  99. package/src/secrets/github.ts +8 -5
  100. package/dist/packages/pipeline/src/context/getReviewSlug.d.ts +0 -4
  101. package/dist/packages/pipeline/src/context/getReviewSlug.js.map +0 -1
package/package.json CHANGED
@@ -53,7 +53,7 @@
53
53
  }
54
54
  ],
55
55
  "license": "MIT",
56
- "version": "5.2.0",
56
+ "version": "5.3.1",
57
57
  "scripts": {
58
58
  "lint": "eslint \"src/**/*.ts\"",
59
59
  "lint:fix": "eslint \"src/**/*.ts\" --fix",
@@ -59,7 +59,7 @@ export const commandPortForward = defineCommand({
59
59
 
60
60
  const { fullName } = context.environment;
61
61
  let serviceName: string = fullName.toString();
62
- if (context.environment.envType === "review") {
62
+ if (context.environment.instance.type === "review") {
63
63
  const mr = await ctx.get("mr");
64
64
  serviceName = serviceName
65
65
  .toString()
@@ -1,6 +1,8 @@
1
+ import { getSecretsEnv } from "@catladder/pipeline";
1
2
  import {
2
3
  getAllComponentsWithAllEnvsHierarchical,
3
4
  getEnvironment,
5
+ getProjectConfig,
4
6
  } from "../../../config/getProjectConfig";
5
7
 
6
8
  /** which envs of which components a secrets command operates on */
@@ -8,6 +10,26 @@ export type SecretsScope = {
8
10
  [componentName: string]: string[];
9
11
  };
10
12
 
13
+ /**
14
+ * envs that share another env's secrets (`environments.<name>.inherit`)
15
+ * have no secret store of their own — the secrets commands operate on
16
+ * the source env instead
17
+ */
18
+ const withoutSharedSecretEnvs = async (
19
+ all: SecretsScope,
20
+ ): Promise<SecretsScope> => {
21
+ const config = await getProjectConfig();
22
+ const environments = config?.environments;
23
+ return Object.fromEntries(
24
+ Object.entries(all)
25
+ .map(([componentName, envs]): [string, string[]] => [
26
+ componentName,
27
+ envs.filter((env) => getSecretsEnv(environments, env) === env),
28
+ ])
29
+ .filter(([, envs]) => envs.length > 0),
30
+ );
31
+ };
32
+
11
33
  /**
12
34
  * resolves a scope expression to component → envs:
13
35
  * - (empty): all components, all envs
@@ -18,7 +40,9 @@ export type SecretsScope = {
18
40
  export const resolveSecretsScope = async (
19
41
  scope?: string,
20
42
  ): Promise<SecretsScope> => {
21
- const all = await getAllComponentsWithAllEnvsHierarchical();
43
+ const all = await withoutSharedSecretEnvs(
44
+ await getAllComponentsWithAllEnvsHierarchical(),
45
+ );
22
46
  if (Object.keys(all).length === 0) {
23
47
  throw new Error("no catladder config found");
24
48
  }
@@ -28,6 +52,16 @@ export const resolveSecretsScope = async (
28
52
 
29
53
  const [env, componentName] = scope.split(":").map((x) => x || null);
30
54
 
55
+ if (env) {
56
+ const config = await getProjectConfig();
57
+ const secretsEnv = getSecretsEnv(config?.environments, env);
58
+ if (secretsEnv !== env) {
59
+ throw new Error(
60
+ `environment "${env}" shares its secrets with "${secretsEnv}" (environments.${env}.inherit) — manage them via "${secretsEnv}"`,
61
+ );
62
+ }
63
+ }
64
+
31
65
  if (componentName && !(componentName in all)) {
32
66
  throw new Error(
33
67
  `unknown component "${componentName}" — components: ${Object.keys(all).join(", ")}`,
@@ -59,7 +93,9 @@ export const resolveSecretsScope = async (
59
93
  * (one component, all envs) forms
60
94
  */
61
95
  export const secretsScopeChoices = async (): Promise<string[]> => {
62
- const all = await getAllComponentsWithAllEnvsHierarchical();
96
+ const all = await withoutSharedSecretEnvs(
97
+ await getAllComponentsWithAllEnvsHierarchical(),
98
+ );
63
99
  const pairs = Object.entries(all).flatMap(([componentName, envs]) =>
64
100
  envs.map((env) => `${env}:${componentName}`),
65
101
  );
@@ -8,6 +8,7 @@ import {
8
8
  getAllEnvs,
9
9
  getEnvironment as _getEnvironment,
10
10
  createComponentContext,
11
+ getSecretsEnv,
11
12
  getSecretVarName,
12
13
  } from "@catladder/pipeline";
13
14
 
@@ -125,7 +126,14 @@ export const getGitlabVar = async (
125
126
  componentName: string,
126
127
  variableName: string,
127
128
  ) => {
128
- const rawVariableName = getSecretVarName(env, componentName, variableName);
129
+ const config = await getProjectConfig();
130
+ // an env sharing another env's secrets (inherit) reads that env's variables
131
+ const secretsEnv = getSecretsEnv(config?.environments, env);
132
+ const rawVariableName = getSecretVarName(
133
+ secretsEnv,
134
+ componentName,
135
+ variableName,
136
+ );
129
137
  return await getVariableValueByRawName(io, rawVariableName);
130
138
  };
131
139
 
@@ -0,0 +1,68 @@
1
+ import { mkdtemp, readFile, rm } from "fs/promises";
2
+ import { tmpdir } from "os";
3
+ import { join } from "path";
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+ import { CHANGESET_REPORT_FILE, changesetCheckJob } from "../changesetCheckJob";
6
+
7
+ // the merge request's diff against its target branch: the only git
8
+ // output the check parses
9
+ const gitMock = vi.hoisted(() => ({ addedFiles: [] as string[] }));
10
+
11
+ vi.mock("../releaseGit", () => ({
12
+ ensureReleaseHistory: vi.fn(async () => undefined),
13
+ getLastReleaseTag: vi.fn(async () => "v1.2.3"),
14
+ git: vi.fn(async (command: string) =>
15
+ command === "diff" ? gitMock.addedFiles.join("\n") : "",
16
+ ),
17
+ }));
18
+
19
+ vi.mock("../changesetsReleaseJob", () => ({
20
+ readPendingChangesets: vi.fn(async () => []),
21
+ }));
22
+
23
+ let cwd: string;
24
+ let workDir: string;
25
+
26
+ beforeEach(async () => {
27
+ cwd = process.cwd();
28
+ workDir = await mkdtemp(join(tmpdir(), "changeset-check-"));
29
+ process.chdir(workDir);
30
+ process.exitCode = undefined;
31
+ vi.spyOn(console, "log").mockImplementation(() => undefined);
32
+ vi.stubEnv("GITHUB_ACTIONS", undefined as any);
33
+ vi.stubEnv("GL_TOKEN", undefined as any);
34
+ vi.stubEnv("CI_MERGE_REQUEST_TARGET_BRANCH_NAME", "main");
35
+ });
36
+
37
+ afterEach(async () => {
38
+ process.chdir(cwd);
39
+ process.exitCode = undefined;
40
+ vi.unstubAllEnvs();
41
+ vi.restoreAllMocks();
42
+ await rm(workDir, { recursive: true, force: true });
43
+ });
44
+
45
+ describe("changesetCheckJob on gitlab", () => {
46
+ it("exits 0 when the merge request adds no changeset — a failed job would never resolve its exposed report", async () => {
47
+ gitMock.addedFiles = ["src/index.ts"];
48
+
49
+ await changesetCheckJob();
50
+
51
+ expect(process.exitCode).toBeUndefined();
52
+ const report = await readFile(CHANGESET_REPORT_FILE, "utf-8");
53
+ expect(report).toContain("adds no changeset");
54
+ expect(console.log).toHaveBeenCalledWith(
55
+ expect.stringContaining("⚠️ this merge request adds no changeset"),
56
+ );
57
+ });
58
+
59
+ it("exits 0 and reports the changesets the merge request adds", async () => {
60
+ gitMock.addedFiles = [".changeset/brave-cats.md"];
61
+
62
+ await changesetCheckJob();
63
+
64
+ expect(process.exitCode).toBeUndefined();
65
+ const report = await readFile(CHANGESET_REPORT_FILE, "utf-8");
66
+ expect(report).toContain("adds 1 changeset: `brave-cats.md`");
67
+ });
68
+ });
@@ -4,8 +4,9 @@
4
4
  * reports which changesets the merge request adds, what is pending and
5
5
  * what version merging would release. The report goes to the job log,
6
6
  * to `changeset-report.md` (exposed as an artifact on gitlab) and to a
7
- * sticky MR/PR comment where a token allows it. Exits 1 (the job runs
8
- * with allow_failure) when the merge request adds no changeset.
7
+ * sticky MR/PR comment where a token allows it. Always exits 0: a
8
+ * missing changeset is a warning carried by the report, not a job
9
+ * failure (see the end of {@link changesetCheckJob}).
9
10
  */
10
11
  import { existsSync } from "fs";
11
12
  import { writeFile } from "fs/promises";
@@ -203,11 +204,17 @@ export const changesetCheckJob = async () => {
203
204
  return;
204
205
  }
205
206
  if (!result.addsChangeset) {
207
+ // the job stays green on both platforms — the report, the log and
208
+ // the sticky comment carry the "no changeset" warning instead:
209
+ // - github has no "warning" job state: exiting non-zero would show
210
+ // the job as failed (a red ❌ and "exit code 1"), which reads
211
+ // like something broke.
212
+ // - gitlab only resolves the exposed artifact (the MR widget's
213
+ // "changeset report" link) of a SUCCESSFUL job: for a failed
214
+ // allow_failure job `exposed_artifacts.json` answers 204 forever
215
+ // and the widget spins on "Loading artifacts" — exactly on the
216
+ // MRs that need the report.
206
217
  if (onGithub()) {
207
- // github has no "warning" job state: exiting non-zero would show
208
- // the job as failed (a red ❌ and "exit code 1"), which reads like
209
- // something broke. A warning annotation plus the step summary and
210
- // the sticky PR comment carry the signal instead.
211
218
  console.log(
212
219
  "::warning title=No changeset::this pull request adds no changeset — " +
213
220
  "if the change is user-facing, add one so it appears in the next release's changelog",
@@ -215,10 +222,11 @@ export const changesetCheckJob = async () => {
215
222
  appendStepSummary(
216
223
  result.markdown.replace(`${CHANGESET_CHECK_MARKER}\n`, ""),
217
224
  );
218
- return;
225
+ } else {
226
+ console.log(
227
+ "⚠️ this merge request adds no changeset — if the change is user-facing, " +
228
+ "add one so it appears in the next release's changelog (see the changeset report)",
229
+ );
219
230
  }
220
- // gitlab: exit 1 turns the (allow_failure) job yellow so reviewers
221
- // notice
222
- process.exitCode = 1;
223
231
  }
224
232
  };
@@ -30,17 +30,20 @@ export const collectSecretsFromVault = async (io: IO, envFilter?: string[]) => {
30
30
  if (envFilter && !envFilter.includes(env)) {
31
31
  continue;
32
32
  }
33
- const { secretEnvVarKeys, jobOnlyVars } = await getEnvironment(
34
- env,
35
- componentName,
36
- );
33
+ const { secretEnvVarKeys, jobOnlyVars, secretsEnv } =
34
+ await getEnvironment(env, componentName);
37
35
  const keys = [
38
36
  ...secretEnvVarKeys,
39
37
  ...jobOnlyVars.build.secretEnvVarKeys,
40
38
  ...jobOnlyVars.deploy.secretEnvVarKeys,
41
39
  ];
40
+ // secretsEnv: an env sharing another env's secrets (inherit)
41
+ // references that env's variable names
42
42
  keys.forEach(({ key, kind }) =>
43
- kinds.set(getSecretVarName(env, componentName, key), kind ?? "secret"),
43
+ kinds.set(
44
+ getSecretVarName(secretsEnv ?? env, componentName, key),
45
+ kind ?? "secret",
46
+ ),
44
47
  );
45
48
  }
46
49
  }
@@ -1,4 +0,0 @@
1
- import { type StringOrBashExpression } from "../../../bash/src/index.js";
2
- import type { PipelineType } from "../types";
3
- import type { EnvConfigWithComponent } from "../types/config";
4
- export declare const getReviewSlug: (envConfig: EnvConfigWithComponent, env: string, pipelineType?: PipelineType) => StringOrBashExpression | null;
@@ -1 +0,0 @@
1
- {"version":3,"file":"getReviewSlug.js","sourceRoot":"","sources":["../../../../../../../packages/pipeline/src/context/getReviewSlug.ts"],"names":[],"mappings":";;;AAAA,0CAA8E;AAE9E,yFAAyF;AAGzF,6CAA0C;AAC1C,MAAM,WAAW,GAAkC;IACjD,OAAO,EAAE,qBAAqB;IAC9B,MAAM,EAAE,IAAI,qBAAc,CACxB,0JAA0J,CAC3J;IACD,uEAAuE;IACvE,MAAM,EAAE,IAAI,qBAAc,CACxB,uEAAuE,CACxE;CACF,CAAC;AAEK,MAAM,aAAa,GAAG,CAC3B,SAAiC,EACjC,GAAW,EACX,YAA2B,EACI,EAAE;IACjC,MAAM,OAAO,GAAG,IAAA,uBAAU,EAAC,GAAG,EAAE,SAAS,CAAC,CAAC;IAC3C,IAAI,OAAO,KAAK,QAAQ,EAAE,CAAC;QACzB,OAAO,IAAA,gEAAgC,EAAC,WAAW,EAAE,YAAY,CAAC,CAAC;IACrE,CAAC;IACD,OAAO,IAAI,CAAC,CAAC,oBAAoB;AACnC,CAAC,CAAC;AAVW,QAAA,aAAa,iBAUxB"}