@catladder/cli 5.0.0 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) 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/setup/setupAccessTokens.js +2 -2
  4. package/dist/apps/cli/src/apps/cli/commands/project/setup/setupAccessTokens.js.map +1 -1
  5. package/dist/apps/cli/src/apps/cli/commands/project/setup/setupGithub.js +2 -2
  6. package/dist/apps/cli/src/apps/cli/commands/project/setup/setupGithub.js.map +1 -1
  7. package/dist/apps/cli/src/apps/cli/commands/project/setup/setupKubernetes.js +23 -7
  8. package/dist/apps/cli/src/apps/cli/commands/project/setup/setupKubernetes.js.map +1 -1
  9. package/dist/apps/cli/src/commands/index.d.ts +1 -0
  10. package/dist/apps/cli/src/commands/index.js +4 -1
  11. package/dist/apps/cli/src/commands/index.js.map +1 -1
  12. package/dist/apps/cli/src/commands/project/commandConfigSecrets.js +2 -1
  13. package/dist/apps/cli/src/commands/project/commandConfigSecrets.js.map +1 -1
  14. package/dist/apps/cli/src/commands/project/commandMrPin.d.ts +21 -0
  15. package/dist/apps/cli/src/commands/project/commandMrPin.js +119 -0
  16. package/dist/apps/cli/src/commands/project/commandMrPin.js.map +1 -0
  17. package/dist/apps/cli/src/commands/project/commandSecretsSyncGithub.d.ts +1 -42
  18. package/dist/apps/cli/src/commands/project/commandSecretsSyncGithub.js +5 -124
  19. package/dist/apps/cli/src/commands/project/commandSecretsSyncGithub.js.map +1 -1
  20. package/dist/apps/cli/src/commands/project/secrets/commandSecretsPush.js +2 -1
  21. package/dist/apps/cli/src/commands/project/secrets/commandSecretsPush.js.map +1 -1
  22. package/dist/apps/cli/src/commands/project/secrets/commandSecretsSet.js +2 -2
  23. package/dist/apps/cli/src/commands/project/secrets/commandSecretsSet.js.map +1 -1
  24. package/dist/apps/cli/src/commands/project/secrets/document.d.ts +1 -12
  25. package/dist/apps/cli/src/commands/project/secrets/document.js +1 -79
  26. package/dist/apps/cli/src/commands/project/secrets/document.js.map +1 -1
  27. package/dist/apps/cli/src/gcloud/serviceAccounts.js +15 -4
  28. package/dist/apps/cli/src/gcloud/serviceAccounts.js.map +1 -1
  29. package/dist/apps/cli/src/release/changesetsReleaseJob.js +3 -0
  30. package/dist/apps/cli/src/release/changesetsReleaseJob.js.map +1 -1
  31. package/dist/apps/cli/src/release/releaseEntry.d.ts +12 -0
  32. package/dist/apps/cli/src/release/releaseEntry.js +71 -0
  33. package/dist/apps/cli/src/release/releaseEntry.js.map +1 -0
  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/gitlab.d.ts +10 -0
  44. package/dist/apps/cli/src/utils/gitlab.js +30 -5
  45. package/dist/apps/cli/src/utils/gitlab.js.map +1 -1
  46. package/dist/apps/cli/src/vault/BitwardenVault.d.ts +6 -0
  47. package/dist/apps/cli/src/vault/BitwardenVault.js +11 -1
  48. package/dist/apps/cli/src/vault/BitwardenVault.js.map +1 -1
  49. package/dist/apps/cli/src/vault/GitlabVault.d.ts +2 -2
  50. package/dist/apps/cli/src/vault/GitlabVault.js +3 -2
  51. package/dist/apps/cli/src/vault/GitlabVault.js.map +1 -1
  52. package/dist/apps/cli/src/vault/VaultManager.d.ts +2 -2
  53. package/dist/apps/cli/src/vault/VaultManager.js +2 -2
  54. package/dist/apps/cli/src/vault/VaultManager.js.map +1 -1
  55. package/dist/apps/cli/src/vault/types.d.ts +16 -2
  56. package/dist/bundles/catci/index.js +4 -4
  57. package/dist/bundles/catenv/index.js +115 -13
  58. package/dist/bundles/cli/index.js +749 -431
  59. package/dist/bundles/runner-images/semantic-release/Dockerfile +1 -0
  60. package/dist/bundles/runner-images/semantic-release/scripts/semanticRelease +14 -4
  61. package/dist/bundles/skills/catladder-cli/references/commands.md +13 -0
  62. package/dist/bundles/skills/catladder-deploys/SKILL.md +7 -0
  63. package/dist/bundles/skills/catladder-migrate-ci-backend/SKILL.md +7 -2
  64. package/dist/bundles/skills/catladder-pipelines/SKILL.md +27 -1
  65. package/dist/bundles/skills/catladder-releases/SKILL.md +16 -2
  66. package/dist/bundles/skills/catladder-secrets/SKILL.md +16 -0
  67. package/dist/packages/pipeline/src/autoStop.d.ts +22 -0
  68. package/dist/packages/pipeline/src/autoStop.js +30 -0
  69. package/dist/packages/pipeline/src/autoStop.js.map +1 -0
  70. package/dist/packages/pipeline/src/backends/gitlab/GitlabBackend.js +2 -0
  71. package/dist/packages/pipeline/src/backends/gitlab/GitlabBackend.js.map +1 -1
  72. package/dist/packages/pipeline/src/backends/gitlab/createGitlabPipeline.d.ts +4 -1
  73. package/dist/packages/pipeline/src/backends/gitlab/createGitlabPipeline.js +21 -1
  74. package/dist/packages/pipeline/src/backends/gitlab/createGitlabPipeline.js.map +1 -1
  75. package/dist/packages/pipeline/src/deploy/base/deploy.js +8 -2
  76. package/dist/packages/pipeline/src/deploy/base/deploy.js.map +1 -1
  77. package/dist/packages/pipeline/src/index.d.ts +1 -0
  78. package/dist/packages/pipeline/src/index.js +1 -0
  79. package/dist/packages/pipeline/src/index.js.map +1 -1
  80. package/dist/packages/pipeline/src/types/config.d.ts +32 -0
  81. package/dist/runner-images/semantic-release/Dockerfile +1 -0
  82. package/dist/runner-images/semantic-release/scripts/semanticRelease +14 -4
  83. package/dist/skills/catladder-cli/references/commands.md +13 -0
  84. package/dist/skills/catladder-deploys/SKILL.md +7 -0
  85. package/dist/skills/catladder-migrate-ci-backend/SKILL.md +7 -2
  86. package/dist/skills/catladder-pipelines/SKILL.md +27 -1
  87. package/dist/skills/catladder-releases/SKILL.md +16 -2
  88. package/dist/skills/catladder-secrets/SKILL.md +16 -0
  89. package/dist/tsconfig.tsbuildinfo +1 -1
  90. package/package.json +1 -1
  91. package/src/apps/cli/commands/project/doctor/checkGithub.ts +1 -1
  92. package/src/apps/cli/commands/project/setup/setupAccessTokens.ts +3 -2
  93. package/src/apps/cli/commands/project/setup/setupGithub.ts +1 -1
  94. package/src/apps/cli/commands/project/setup/setupKubernetes.ts +31 -20
  95. package/src/commands/index.ts +1 -0
  96. package/src/commands/project/commandConfigSecrets.ts +1 -1
  97. package/src/commands/project/commandMrPin.ts +184 -0
  98. package/src/commands/project/commandSecretsSyncGithub.ts +7 -154
  99. package/src/commands/project/secrets/commandSecretsPush.ts +2 -5
  100. package/src/commands/project/secrets/commandSecretsSet.ts +2 -2
  101. package/src/commands/project/secrets/document.ts +2 -127
  102. package/src/gcloud/serviceAccounts.ts +16 -7
  103. package/src/release/__tests__/releaseEntry.test.ts +88 -0
  104. package/src/release/changesetsReleaseJob.ts +3 -0
  105. package/src/release/releaseEntry.ts +86 -0
  106. package/src/secrets/__tests__/writeSecrets.test.ts +147 -0
  107. package/src/secrets/github.ts +157 -0
  108. package/src/secrets/index.ts +2 -0
  109. package/src/secrets/writeSecrets.ts +138 -0
  110. package/src/utils/gitlab.ts +34 -7
  111. package/src/vault/BitwardenVault.ts +10 -1
  112. package/src/vault/GitlabVault.ts +9 -2
  113. package/src/vault/VaultManager.ts +3 -2
  114. package/src/vault/__tests__/BitwardenVault.test.ts +88 -0
  115. package/src/vault/types.ts +17 -1
package/package.json CHANGED
@@ -53,7 +53,7 @@
53
53
  }
54
54
  ],
55
55
  "license": "MIT",
56
- "version": "5.0.0",
56
+ "version": "5.1.0",
57
57
  "scripts": {
58
58
  "lint": "eslint \"src/**/*.ts\"",
59
59
  "lint:fix": "eslint \"src/**/*.ts\" --fix",
@@ -7,7 +7,7 @@ import {
7
7
  import {
8
8
  collectSecretTargets,
9
9
  getConfiguredGithubRepo,
10
- } from "../../../../../commands/project/commandSecretsSyncGithub";
10
+ } from "../../../../../secrets";
11
11
  import { mapWithConcurrency } from "../../../../../utils/concurrency";
12
12
  import {
13
13
  ghApiJson,
@@ -4,6 +4,7 @@ import {
4
4
  doGitlabRequest,
5
5
  doGitlabRequestAllPages,
6
6
  getProjectInfo,
7
+ GitlabNotFoundError,
7
8
  } from "../../../../../utils/gitlab";
8
9
 
9
10
  const TOKEN_NAME = "semantic-release";
@@ -55,10 +56,10 @@ export const setupAccessTokens = async (instance: IO) => {
55
56
  "PUT",
56
57
  );
57
58
  } catch (e) {
58
- if (e.message !== "not found") {
59
+ if (!(e instanceof GitlabNotFoundError)) {
59
60
  throw e;
60
61
  }
61
- // not found
62
+ // the variable does not exist yet: create it
62
63
  await doGitlabRequest(
63
64
  instance,
64
65
  `projects/${projectId}/variables`,
@@ -9,7 +9,7 @@ import { mkdtemp, readFile, rm } from "fs/promises";
9
9
  import { tmpdir } from "os";
10
10
  import { join } from "path";
11
11
  import { promisify } from "util";
12
- import { getConfiguredGithubRepo } from "../../../../../commands/project/commandSecretsSyncGithub";
12
+ import { getConfiguredGithubRepo } from "../../../../../secrets";
13
13
  import type { IO } from "../../../../../core/types";
14
14
  import {
15
15
  ghApiJson,
@@ -1,27 +1,26 @@
1
- import type { ComponentContext } from "@catladder/pipeline";
1
+ import type { ComponentContext, Config } from "@catladder/pipeline";
2
2
  import {
3
+ getEnabledPipelineTypes,
3
4
  getFullKubernetesClusterName,
4
5
  isOfDeployType,
5
6
  } from "@catladder/pipeline";
7
+ import { getProjectConfig } from "../../../../../config/getProjectConfig";
6
8
  import type { IO } from "../../../../../core/types";
7
9
  import { exec } from "child-process-promise";
10
+ import { writeSecretsAndMirror } from "../../../../../secrets";
8
11
  import { connectToCluster } from "../../../../../utils/cluster";
9
- import {
10
- doGitlabRequest,
11
- getProjectInfo,
12
- upsertAllVariables,
13
- } from "../../../../../utils/gitlab";
12
+ import { doGitlabRequest, getProjectInfo } from "../../../../../utils/gitlab";
14
13
  import ensureNamespace from "../utils/ensureNamespace";
15
14
 
16
- export const setupKubernetes = async (
17
- instance: IO,
18
- context: ComponentContext,
19
- ) => {
20
- const deployConfig = context.deploy?.config;
21
- if (!isOfDeployType(deployConfig, "kubernetes")) {
22
- throw new Error("cannot run setupKubernetes on non-kubernetes deployments");
15
+ /**
16
+ * the cluster pulls the images from the gitlab container registry, and
17
+ * a deploy token is how it authenticates there. Nothing to do when the
18
+ * project has no gitlab pipeline (the images live elsewhere then).
19
+ */
20
+ const ensureGitlabDeployToken = async (instance: IO, config: Config | null) => {
21
+ if (!config || !getEnabledPipelineTypes(config).includes("gitlab")) {
22
+ return;
23
23
  }
24
-
25
24
  const { id: projectId } = await getProjectInfo(instance);
26
25
  const deploy_tokens = await doGitlabRequest(
27
26
  instance,
@@ -48,6 +47,18 @@ export const setupKubernetes = async (
48
47
  "POST",
49
48
  );
50
49
  }
50
+ };
51
+
52
+ export const setupKubernetes = async (
53
+ instance: IO,
54
+ context: ComponentContext,
55
+ ) => {
56
+ const deployConfig = context.deploy?.config;
57
+ if (!isOfDeployType(deployConfig, "kubernetes")) {
58
+ throw new Error("cannot run setupKubernetes on non-kubernetes deployments");
59
+ }
60
+
61
+ await ensureGitlabDeployToken(instance, await getProjectConfig());
51
62
 
52
63
  const fullName = getFullKubernetesClusterName(deployConfig.cluster);
53
64
  instance.log(`cluster: ${fullName}`);
@@ -158,14 +169,14 @@ EOF
158
169
  instance.log("service accounts created / updated!");
159
170
 
160
171
  instance.log("");
161
- instance.log("pusing secrets to gitlab...");
172
+ instance.log("storing the cluster credentials...");
162
173
 
163
- await upsertAllVariables(
174
+ await writeSecretsAndMirror(
164
175
  instance,
165
- vars,
166
- context.env,
167
- context.name,
168
- false, // no backup
176
+ [{ env: context.env, componentName: context.name, secrets: vars }],
177
+ // the service account token is recreated on every run — the
178
+ // previous one is worth nothing
179
+ { backup: false },
169
180
  );
170
181
  instance.log("done!");
171
182
  };
@@ -29,6 +29,7 @@ export { commandDoctor } from "./project/commandDoctor";
29
29
  export { commandCiJobOpen, commandCiJobLog } from "./project/commandGitlabCi";
30
30
  export { commandProjectRestoreDb } from "./project/commandProjectRestoreDb";
31
31
  export { commandSecurityEvaluate } from "./project/commandSecurityEvaluate";
32
+ export { commandMrPin, commandMrUnpin } from "./project/commandMrPin";
32
33
 
33
34
  // Project k8s commands
34
35
  export { commandNamespace } from "./project/k8s/commandNamespace";
@@ -2,11 +2,11 @@ import { stripIndents } from "common-tags";
2
2
  import { defineCommand } from "../../core/defineCommand";
3
3
  import { editAsFile } from "../../utils/editAsFile";
4
4
  import { delay } from "../../utils/promise";
5
+ import { writeSecretsAndMirror } from "../../secrets";
5
6
  import {
6
7
  buildSecretsDocument,
7
8
  checkSecretsDocument,
8
9
  collectSecretsWrites,
9
- writeSecretsAndMirror,
10
10
  } from "./secrets/document";
11
11
  import {
12
12
  formatScope,
@@ -0,0 +1,184 @@
1
+ import { exec } from "child-process-promise";
2
+ import {
3
+ getAutoStopConfig,
4
+ getEnabledPipelineTypes,
5
+ } from "@catladder/pipeline";
6
+ import { getProjectConfig } from "../../config/getProjectConfig";
7
+ import { defineCommand } from "../../core/defineCommand";
8
+ import type { IO } from "../../core/types";
9
+ import { doGitlabRequest, getProjectInfo } from "../../utils/gitlab";
10
+
11
+ const PIN_LABEL_COLOR = "#428fdc";
12
+
13
+ type GitlabMr = {
14
+ iid: number;
15
+ title: string;
16
+ web_url: string;
17
+ labels: string[];
18
+ };
19
+
20
+ /**
21
+ * the configured pin label — errors when the project cannot pin
22
+ * (github-only, or pinning disabled)
23
+ */
24
+ const getPinLabel = async (): Promise<string> => {
25
+ const config = await getProjectConfig();
26
+ if (!config) {
27
+ throw new Error("no catladder config found");
28
+ }
29
+ if (!getEnabledPipelineTypes(config).includes("gitlab")) {
30
+ throw new Error(
31
+ "pinning review apps needs the gitlab pipeline — github has no auto-stop, review apps live until the pull request is closed",
32
+ );
33
+ }
34
+ const { pinLabel } = getAutoStopConfig(config);
35
+ if (pinLabel === false) {
36
+ throw new Error(
37
+ "review-app pinning is disabled in this project (autoStop.pinLabel: false)",
38
+ );
39
+ }
40
+ return pinLabel;
41
+ };
42
+
43
+ const findMr = async (
44
+ io: IO,
45
+ projectId: string,
46
+ mrIid: number | undefined,
47
+ ): Promise<GitlabMr> => {
48
+ if (mrIid) {
49
+ return doGitlabRequest(io, `projects/${projectId}/merge_requests/${mrIid}`);
50
+ }
51
+ const branch = (await exec("git rev-parse --abbrev-ref HEAD")).stdout.trim();
52
+ const mrs = await doGitlabRequest<GitlabMr[]>(
53
+ io,
54
+ `projects/${projectId}/merge_requests?state=opened&source_branch=${encodeURIComponent(branch)}`,
55
+ );
56
+ if (!mrs?.length) {
57
+ throw new Error(
58
+ `no open merge request found for branch '${branch}' — pass one with --mr <iid>`,
59
+ );
60
+ }
61
+ return mrs[0];
62
+ };
63
+
64
+ const ensurePinLabelExists = async (
65
+ io: IO,
66
+ projectId: string,
67
+ pinLabel: string,
68
+ ) => {
69
+ const existing = await doGitlabRequest<Array<{ name: string }>>(
70
+ io,
71
+ `projects/${projectId}/labels?search=${encodeURIComponent(pinLabel)}`,
72
+ );
73
+ if (existing?.some((label) => label.name === pinLabel)) {
74
+ return;
75
+ }
76
+ await doGitlabRequest(
77
+ io,
78
+ `projects/${projectId}/labels`,
79
+ {
80
+ name: pinLabel,
81
+ color: PIN_LABEL_COLOR,
82
+ description:
83
+ "review apps of this MR are pinned — deploys use auto_stop_in: never (managed by catladder)",
84
+ },
85
+ "POST",
86
+ );
87
+ io.log(`created the '${pinLabel}' label in the project`);
88
+ };
89
+
90
+ export const commandMrPin = defineCommand({
91
+ name: "mr pin",
92
+ description:
93
+ "pin the review apps of a merge request (disable their auto-stop) by adding the pin label; merging or closing the MR still stops them",
94
+ group: "mr",
95
+ inputs: {
96
+ mr: {
97
+ type: "number",
98
+ message:
99
+ "merge request IID (defaults to the open MR of the current branch)",
100
+ positional: true,
101
+ required: false,
102
+ },
103
+ pipeline: {
104
+ type: "boolean",
105
+ message:
106
+ "trigger a merge-request pipeline so the pin takes effect immediately",
107
+ required: false,
108
+ },
109
+ },
110
+ execute: async (ctx) => {
111
+ const pinLabel = await getPinLabel();
112
+ const { id: projectId } = await getProjectInfo(ctx);
113
+ const mr = await findMr(ctx, projectId, await ctx.get("mr"));
114
+
115
+ await ensurePinLabelExists(ctx, projectId, pinLabel);
116
+
117
+ if (mr.labels?.includes(pinLabel)) {
118
+ ctx.log(`already pinned: mr!${mr.iid} carries the '${pinLabel}' label`);
119
+ } else {
120
+ await doGitlabRequest(
121
+ ctx,
122
+ `projects/${projectId}/merge_requests/${mr.iid}`,
123
+ { add_labels: pinLabel },
124
+ "PUT",
125
+ );
126
+ ctx.log(
127
+ `📌 pinned mr!${mr.iid} "${mr.title}" — label '${pinLabel}' added`,
128
+ );
129
+ }
130
+
131
+ // the label only affects pipelines created after it was set, so
132
+ // trigger one — otherwise the currently deployed review apps keep
133
+ // their old auto-stop timer until the next push
134
+ if ((await ctx.get("pipeline")) ?? true) {
135
+ const pipeline = await doGitlabRequest<{ web_url: string }>(
136
+ ctx,
137
+ `projects/${projectId}/merge_requests/${mr.iid}/pipelines`,
138
+ {},
139
+ "POST",
140
+ );
141
+ ctx.log(
142
+ `🚀 triggered a pipeline so the pin takes effect now: ${pipeline.web_url}`,
143
+ );
144
+ } else {
145
+ ctx.log("the pin takes effect with the next pipeline of the MR");
146
+ }
147
+ },
148
+ });
149
+
150
+ export const commandMrUnpin = defineCommand({
151
+ name: "mr unpin",
152
+ description:
153
+ "unpin the review apps of a merge request — the auto-stop timer re-arms with the next deploy",
154
+ group: "mr",
155
+ inputs: {
156
+ mr: {
157
+ type: "number",
158
+ message:
159
+ "merge request IID (defaults to the open MR of the current branch)",
160
+ positional: true,
161
+ required: false,
162
+ },
163
+ },
164
+ execute: async (ctx) => {
165
+ const pinLabel = await getPinLabel();
166
+ const { id: projectId } = await getProjectInfo(ctx);
167
+ const mr = await findMr(ctx, projectId, await ctx.get("mr"));
168
+
169
+ if (!mr.labels?.includes(pinLabel)) {
170
+ ctx.log(`mr!${mr.iid} is not pinned (no '${pinLabel}' label)`);
171
+ return;
172
+ }
173
+ await doGitlabRequest(
174
+ ctx,
175
+ `projects/${projectId}/merge_requests/${mr.iid}`,
176
+ { remove_labels: pinLabel },
177
+ "PUT",
178
+ );
179
+ ctx.log(`unpinned mr!${mr.iid} — label '${pinLabel}' removed`);
180
+ ctx.log(
181
+ "the deployed review apps stay pinned until the next deploy re-arms their auto-stop timer",
182
+ );
183
+ },
184
+ });
@@ -1,163 +1,16 @@
1
- import type { Config } from "@catladder/pipeline";
2
1
  import {
3
2
  getEnabledPipelineTypes,
4
3
  getPipelineGitRemote,
5
- getSecretVarName,
6
- GithubBackend,
7
4
  } from "@catladder/pipeline";
8
- import {
9
- getAllComponentsWithAllEnvsHierarchical,
10
- getEnvironment,
11
- getProjectConfig,
12
- } from "../../config/getProjectConfig";
5
+ import { getProjectConfig } from "../../config/getProjectConfig";
13
6
  import { defineCommand } from "../../core/defineCommand";
14
- import type { IO } from "../../core/types";
15
7
  import {
16
- ensureGithubEnvironment,
17
- getGithubRepoFromRemote,
18
- isGhAuthenticated,
19
- setGithubSecret,
20
- setGithubVariable,
21
- } from "../../utils/github";
22
-
23
- /**
24
- * all secrets declared in the config, with their values read from the
25
- * vault (github secrets are write-only and can never act as a source)
26
- */
27
- export const collectSecretsFromVault = async (io: IO, envFilter?: string[]) => {
28
- const envAndComponents = await getAllComponentsWithAllEnvsHierarchical();
29
-
30
- const kinds = new Map<string, "secret" | "variable">();
31
- for (const [componentName, envs] of Object.entries(envAndComponents)) {
32
- for (const env of envs) {
33
- if (envFilter && !envFilter.includes(env)) {
34
- continue;
35
- }
36
- const { secretEnvVarKeys, jobOnlyVars } = await getEnvironment(
37
- env,
38
- componentName,
39
- );
40
- const keys = [
41
- ...secretEnvVarKeys,
42
- ...jobOnlyVars.build.secretEnvVarKeys,
43
- ...jobOnlyVars.deploy.secretEnvVarKeys,
44
- ];
45
- keys.forEach(({ key, kind }) =>
46
- kinds.set(getSecretVarName(env, componentName, key), kind ?? "secret"),
47
- );
48
- }
49
- }
50
-
51
- const names = [...kinds.keys()];
52
- const values = await (await io.getVaultManager()).readSecrets(names, io);
53
- const secrets = names
54
- .filter((name) => values[name] !== undefined)
55
- .map((name) => ({
56
- name,
57
- value: values[name],
58
- kind: kinds.get(name) ?? ("secret" as const),
59
- }));
60
- const missing = names.filter((name) => values[name] === undefined);
61
- return { secrets, missing };
62
- };
63
-
64
- /** marker for repo-level (environment-less) targets */
65
- const REPO_LEVEL = "";
66
-
67
- /**
68
- * derives which github environment(s) each secret/variable must live in
69
- * by scanning the GENERATED workflows: a `${{ secrets.X }}` / `${{
70
- * vars.X }}` reference in a job's env targets that job's environment
71
- * (or the repo level when the job has none). Deriving from the real
72
- * output means the sync can never drift from what the workflows expect.
73
- *
74
- * Environment secrets are the answer to github's cap of 100 secrets
75
- * per REPO (a multi-env project easily exceeds it): the cap applies
76
- * per environment instead.
77
- */
78
- export const collectSecretTargets = async (
79
- config: Config,
80
- ): Promise<Map<string, Set<string>>> => {
81
- const workflows = await new GithubBackend().createWorkflows(config);
82
- const targets = new Map<string, Set<string>>();
83
- for (const workflow of Object.values(workflows)) {
84
- for (const job of Object.values(workflow.jobs)) {
85
- const environment =
86
- typeof job.environment === "object"
87
- ? job.environment.name
88
- : job.environment;
89
- for (const value of Object.values(job.env ?? {})) {
90
- if (typeof value !== "string") continue;
91
- for (const match of value.matchAll(
92
- /\$\{\{\s*(?:secrets|vars)\.([A-Za-z0-9_]+)\s*\}\}/g,
93
- )) {
94
- const name = match[1];
95
- if (!targets.has(name)) {
96
- targets.set(name, new Set());
97
- }
98
- targets.get(name)!.add(environment ?? REPO_LEVEL);
99
- }
100
- }
101
- }
102
- }
103
- return targets;
104
- };
105
-
106
- /**
107
- * pushes vault values to github via the gh cli: sensitive ones as
108
- * secrets (masked), "variable"-kind ones as variables (plain) — each
109
- * into the environment(s) whose jobs reference them
110
- */
111
- export const pushSecretsToGithub = async (
112
- io: IO,
113
- repo: string,
114
- secrets: { name: string; value: string; kind: "secret" | "variable" }[],
115
- targets: Map<string, Set<string>>,
116
- ) => {
117
- const environments = [
118
- ...new Set(
119
- [...targets.values()].flatMap((envs) =>
120
- [...envs].filter((env) => env !== REPO_LEVEL),
121
- ),
122
- ),
123
- ];
124
- for (const environment of environments) {
125
- await ensureGithubEnvironment(repo, environment);
126
- io.log(`🌍 environment ${environment}`);
127
- }
128
-
129
- for (const { name, value, kind } of secrets) {
130
- const envs = targets.get(name);
131
- if (!envs) {
132
- io.log(`⏭️ ${name} (not referenced by any generated workflow)`);
133
- continue;
134
- }
135
- for (const environment of envs) {
136
- const suffix = environment === REPO_LEVEL ? "" : ` → ${environment}`;
137
- if (kind === "variable") {
138
- await setGithubVariable(repo, name, value, environment || undefined);
139
- io.log(`📖 ${name} (variable)${suffix}`);
140
- } else {
141
- await setGithubSecret(repo, name, value, environment || undefined);
142
- io.log(`✅ ${name}${suffix}`);
143
- }
144
- }
145
- }
146
- };
147
-
148
- /**
149
- * resolves the github repository from the configured git remote
150
- * (`pipelines.github.gitRemote`, defaults to origin)
151
- */
152
- export const getConfiguredGithubRepo = async (): Promise<
153
- string | undefined
154
- > => {
155
- const config = await getProjectConfig();
156
- if (!config) {
157
- return undefined;
158
- }
159
- return getGithubRepoFromRemote(getPipelineGitRemote(config, "github"));
160
- };
8
+ collectSecretsFromVault,
9
+ collectSecretTargets,
10
+ getConfiguredGithubRepo,
11
+ pushSecretsToGithub,
12
+ } from "../../secrets";
13
+ import { isGhAuthenticated } from "../../utils/github";
161
14
 
162
15
  /**
163
16
  * copies the catladder secrets from the vault to github actions
@@ -2,12 +2,9 @@ import { readFile } from "fs-extra";
2
2
  import { parse } from "yaml";
3
3
  import { defineCommand } from "../../../core/defineCommand";
4
4
  import { readStdin } from "../../../utils/readStdin";
5
+ import { writeSecretsAndMirror } from "../../../secrets";
5
6
  import type { SecretsDocument } from "./document";
6
- import {
7
- checkSecretsDocument,
8
- collectSecretsWrites,
9
- writeSecretsAndMirror,
10
- } from "./document";
7
+ import { checkSecretsDocument, collectSecretsWrites } from "./document";
11
8
  import {
12
9
  getScopedSecretKeys,
13
10
  resolveSecretsScope,
@@ -2,8 +2,8 @@ import { readFile } from "fs-extra";
2
2
  import { defineCommand } from "../../../core/defineCommand";
3
3
  import type { IO } from "../../../core/types";
4
4
  import { readStdin } from "../../../utils/readStdin";
5
- import type { SecretsWrite } from "./document";
6
- import { writeSecretsAndMirror } from "./document";
5
+ import type { SecretsWrite } from "../../../secrets";
6
+ import { writeSecretsAndMirror } from "../../../secrets";
7
7
  import {
8
8
  getScopedSecretKeys,
9
9
  resolveSecretsScope,
@@ -1,21 +1,6 @@
1
- import type { Config } from "@catladder/pipeline";
2
- import {
3
- getEnabledPipelineTypes,
4
- getSecretVarName,
5
- getVaultConfig,
6
- } from "@catladder/pipeline";
7
- import {
8
- getEnvironment,
9
- getProjectConfig,
10
- } from "../../../config/getProjectConfig";
1
+ import { getSecretVarName } from "@catladder/pipeline";
11
2
  import type { IO } from "../../../core/types";
12
- import { isGhAuthenticated } from "../../../utils/github";
13
- import { upsertAllVariables } from "../../../utils/gitlab";
14
- import {
15
- collectSecretTargets,
16
- getConfiguredGithubRepo,
17
- pushSecretsToGithub,
18
- } from "../commandSecretsSyncGithub";
3
+ import type { SecretsWrite } from "../../../secrets";
19
4
  import type { ScopedSecretKeys } from "./scope";
20
5
 
21
6
  /** placeholder for secrets that have no value in the vault yet */
@@ -31,13 +16,6 @@ export type SecretsDocument = {
31
16
  };
32
17
  };
33
18
 
34
- /** secrets of one env of one component, ready to be written */
35
- export type SecretsWrite = {
36
- env: string;
37
- componentName: string;
38
- secrets: Record<string, unknown>;
39
- };
40
-
41
19
  /* for convenience, parse json values. that makes it easier to edit secrets that are objects */
42
20
  const parseJsonValue = (value: string): unknown => {
43
21
  try {
@@ -164,106 +142,3 @@ export const collectSecretsWrites = (
164
142
  }
165
143
  return { writes, skipped };
166
144
  };
167
-
168
- /**
169
- * writes secrets to the vault (the source of truth) and mirrors them
170
- * to every enabled CI backend — which backends exist is invisible to
171
- * the caller
172
- */
173
- export const writeSecretsAndMirror = async (
174
- io: IO,
175
- writes: SecretsWrite[],
176
- ): Promise<void> => {
177
- const config = await getProjectConfig();
178
- if (!config) {
179
- throw new Error("no catladder config found");
180
- }
181
-
182
- io.log("");
183
- io.log("writing secrets to the vault, please wait...");
184
- io.log("");
185
- for (const { env, componentName, secrets } of writes) {
186
- io.log("writing " + env + ":" + componentName + "...\n");
187
- await (
188
- await io.getVaultManager()
189
- ).writeSecrets(env, componentName, secrets);
190
- io.log("");
191
- io.log("✅ " + env + ":" + componentName);
192
- io.log("--------------------------------\n");
193
- }
194
-
195
- await mirrorSecretsToCiBackends(io, config, writes);
196
- };
197
-
198
- /**
199
- * the vault is the source of truth; every enabled CI backend receives
200
- * a mirrored copy of the written secrets (gitlab variables, github
201
- * secrets), so all CI systems stay in sync
202
- */
203
- const mirrorSecretsToCiBackends = async (
204
- io: IO,
205
- config: Config,
206
- writes: SecretsWrite[],
207
- ) => {
208
- const enabled = getEnabledPipelineTypes(config);
209
-
210
- // gitlab mirror (unless gitlab is the vault itself, then it is
211
- // already written)
212
- if (getVaultConfig(config).type !== "gitlab" && enabled.includes("gitlab")) {
213
- for (const { env, componentName, secrets } of writes) {
214
- await upsertAllVariables(io, secrets, env, componentName);
215
- }
216
- io.log("✅ mirrored to gitlab");
217
- }
218
-
219
- if (!enabled.includes("github")) {
220
- return;
221
- }
222
- const kinds = new Map<string, "secret" | "variable">();
223
- for (const { env, componentName } of writes) {
224
- const { secretEnvVarKeys, jobOnlyVars } = await getEnvironment(
225
- env,
226
- componentName,
227
- );
228
- [
229
- ...secretEnvVarKeys,
230
- ...jobOnlyVars.build.secretEnvVarKeys,
231
- ...jobOnlyVars.deploy.secretEnvVarKeys,
232
- ].forEach(({ key, kind }) =>
233
- kinds.set(getSecretVarName(env, componentName, key), kind ?? "secret"),
234
- );
235
- }
236
- const secrets = writes.flatMap(({ env, componentName, secrets }) =>
237
- Object.entries(secrets).map(([key, value]) => {
238
- const name = getSecretVarName(env, componentName, key);
239
- return {
240
- name,
241
- value: typeof value === "string" ? value : JSON.stringify(value ?? ""),
242
- kind: kinds.get(name) ?? ("secret" as const),
243
- };
244
- }),
245
- );
246
- if (secrets.length === 0) {
247
- return;
248
- }
249
- if (!(await isGhAuthenticated())) {
250
- io.log(
251
- "⚠️ github pipeline is enabled, but the github cli (gh) is not authenticated — run `project secrets-sync-github` later to mirror the secrets",
252
- );
253
- return;
254
- }
255
- const repo = await getConfiguredGithubRepo();
256
- if (!repo) {
257
- io.log(
258
- "⚠️ github pipeline is enabled, but no github repository was found — run `project secrets-sync-github` later to mirror the secrets",
259
- );
260
- return;
261
- }
262
- io.log(`mirroring ${secrets.length} secrets to github ('${repo}')...`);
263
- await pushSecretsToGithub(
264
- io,
265
- repo,
266
- secrets,
267
- await collectSecretTargets(config),
268
- );
269
- };