@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
@@ -57,6 +57,21 @@ export const getGitlabToken = async (io: IO | null) => {
57
57
  return getPreference(TOKEN_KEY);
58
58
  };
59
59
 
60
+ /**
61
+ * a gitlab api call that came back 404. Carries the path so a caller
62
+ * that wants to treat "does not exist yet" as normal can say so
63
+ * (`instanceof`) instead of matching on a message.
64
+ */
65
+ export class GitlabNotFoundError extends Error {
66
+ constructor(
67
+ readonly path: string,
68
+ readonly host: string,
69
+ ) {
70
+ super(`gitlab api: ${path} not found on ${host}`);
71
+ this.name = "GitlabNotFoundError";
72
+ }
73
+ }
74
+
60
75
  type Method = "GET" | "PUT" | "POST" | "DELETE";
61
76
  export const doGitlabRequest = async <T = any>(
62
77
  io: IO | null,
@@ -87,7 +102,7 @@ export const doGitlabRequest = async <T = any>(
87
102
  return null;
88
103
  }
89
104
  if (result.status === 404) {
90
- throw new Error("not found");
105
+ throw new GitlabNotFoundError(path, gitRemoteHost);
91
106
  }
92
107
 
93
108
  throw new Error(
@@ -124,12 +139,24 @@ export const doGitlabRequestAllPages = async <T = any>(
124
139
  export const getProjectInfo = async (
125
140
  io: IO | null,
126
141
  ): Promise<{ id: string; web_url: string }> => {
127
- const { gitRemotePath } = await getGitRemoteHostAndPath();
128
- const project = await doGitlabRequest(
129
- io,
130
- `projects/${encodeURIComponent(gitRemotePath)}`,
131
- );
132
- return project;
142
+ const { gitRemoteHost, gitRemotePath } = await getGitRemoteHostAndPath();
143
+ try {
144
+ return await doGitlabRequest(
145
+ io,
146
+ `projects/${encodeURIComponent(gitRemotePath)}`,
147
+ );
148
+ } catch (e) {
149
+ if (e instanceof GitlabNotFoundError) {
150
+ // the classic: the remote is not a gitlab at all (a github one,
151
+ // say), so the request goes to a host without /api/v4 and every
152
+ // path answers 404
153
+ throw new Error(
154
+ `no gitlab project '${gitRemotePath}' on '${gitRemoteHost}'. ` +
155
+ "Check the git remote and `pipelines` in catladder.ts — this command talks to the gitlab api.",
156
+ );
157
+ }
158
+ throw e;
159
+ }
133
160
  };
134
161
 
135
162
  type GitlabVariable = {
@@ -108,6 +108,12 @@ export class BitwardenVault implements SecretsVault {
108
108
  return secrets;
109
109
  }
110
110
 
111
+ /**
112
+ * upserts into the item's yaml note. The note is rewritten as a
113
+ * whole, so the values that are not part of this write have to be
114
+ * carried over — a single-key write (`secrets-set`, a credential
115
+ * provisioned by `project setup`) must not drop the rest.
116
+ */
111
117
  async writeSecrets(
112
118
  io: IO,
113
119
  env: string,
@@ -130,7 +136,10 @@ export class BitwardenVault implements SecretsVault {
130
136
  ])
131
137
  ).find((item) => item.name === name);
132
138
 
133
- const notes = stringify(secrets);
139
+ const previous = existing?.notes
140
+ ? ((parse(existing.notes) as Record<string, unknown> | null) ?? {})
141
+ : {};
142
+ const notes = stringify({ ...previous, ...secrets });
134
143
 
135
144
  if (existing) {
136
145
  await runBw(bwSession, [
@@ -1,6 +1,6 @@
1
1
  import type { IO } from "../core/types";
2
2
  import { getAllVariables, upsertAllVariables } from "../utils/gitlab";
3
- import type { SecretsVault } from "./types";
3
+ import type { SecretsVault, WriteSecretsOptions } from "./types";
4
4
 
5
5
  /**
6
6
  * the legacy (and default) vault: the gitlab project variables double
@@ -19,7 +19,14 @@ export class GitlabVault implements SecretsVault {
19
19
  env: string,
20
20
  componentName: string,
21
21
  secrets: Record<string, unknown>,
22
+ options: WriteSecretsOptions = {},
22
23
  ): Promise<void> {
23
- await upsertAllVariables(io, secrets, env, componentName);
24
+ await upsertAllVariables(
25
+ io,
26
+ secrets,
27
+ env,
28
+ componentName,
29
+ options.backup ?? true,
30
+ );
24
31
  }
25
32
  }
@@ -13,7 +13,7 @@ import {
13
13
  writeSecretsCache,
14
14
  } from "./cache";
15
15
  import { GitlabVault } from "./GitlabVault";
16
- import type { SecretsMode, SecretsVault } from "./types";
16
+ import type { SecretsMode, SecretsVault, WriteSecretsOptions } from "./types";
17
17
 
18
18
  /**
19
19
  * only secrets of this env are cached locally — the other envs'
@@ -119,11 +119,12 @@ export class VaultManager {
119
119
  componentName: string,
120
120
  secrets: Record<string, unknown>,
121
121
  io: IO | null = this.io,
122
+ options: WriteSecretsOptions = {},
122
123
  ): Promise<void> {
123
124
  if (!io) {
124
125
  throw new Error("writing to the vault requires an interactive context");
125
126
  }
126
- await this.vault.writeSecrets(io, env, componentName, secrets);
127
+ await this.vault.writeSecrets(io, env, componentName, secrets, options);
127
128
  if (env === CACHED_ENV) {
128
129
  await updateSecretsCache(
129
130
  this.vault.id,
@@ -0,0 +1,88 @@
1
+ import type { Config } from "@catladder/pipeline";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { parse } from "yaml";
4
+ import type { IO } from "../../core/types";
5
+ import { BitwardenVault } from "../BitwardenVault";
6
+
7
+ const { getBwSession, runBw, runBwJson } = vi.hoisted(() => ({
8
+ getBwSession: vi.fn(),
9
+ runBw: vi.fn(),
10
+ runBwJson: vi.fn(),
11
+ }));
12
+ vi.mock("../bw", () => ({ getBwSession, runBw, runBwJson }));
13
+
14
+ const io = { log: vi.fn() } as unknown as IO;
15
+ const config = { customerName: "pan", appName: "app" } as Config;
16
+ const ITEM_NAME = "pan/app/dev/www/secrets.yml";
17
+
18
+ /** the item the vault would edit, or none at all */
19
+ const bwContains = (item?: Record<string, unknown>) =>
20
+ runBwJson.mockImplementation(async (_session: string, args: string[]) =>
21
+ args[1] === "collections"
22
+ ? [{ id: "collection-id", name: "catladder", organizationId: "org-id" }]
23
+ : item
24
+ ? [item]
25
+ : [],
26
+ );
27
+
28
+ /** the notes of the `bw edit|create item` payload, parsed back */
29
+ const writtenNotes = () => {
30
+ const [, args] = runBw.mock.calls[0];
31
+ const payload = JSON.parse(
32
+ Buffer.from(args[args.length - 1], "base64").toString("utf-8"),
33
+ );
34
+ return parse(payload.notes);
35
+ };
36
+
37
+ beforeEach(() => {
38
+ vi.clearAllMocks();
39
+ getBwSession.mockResolvedValue("session");
40
+ });
41
+
42
+ describe("BitwardenVault.writeSecrets", () => {
43
+ it("merges into the existing note instead of replacing it", async () => {
44
+ bwContains({
45
+ id: "item-id",
46
+ name: ITEM_NAME,
47
+ notes: "API_KEY: sesame\nSMTP_PASSWORD: hunter2\n",
48
+ });
49
+
50
+ await new BitwardenVault(config, {}, false).writeSecrets(io, "dev", "www", {
51
+ // the shape `project setup` writes: one provisioned credential,
52
+ // nothing else — the other secrets must survive it
53
+ GCLOUD_DEPLOY_credentialsKey: '{"type":"service_account"}',
54
+ });
55
+
56
+ expect(runBw.mock.calls[0][1].slice(0, 3)).toEqual([
57
+ "edit",
58
+ "item",
59
+ "item-id",
60
+ ]);
61
+ expect(writtenNotes()).toEqual({
62
+ API_KEY: "sesame",
63
+ SMTP_PASSWORD: "hunter2",
64
+ GCLOUD_DEPLOY_credentialsKey: '{"type":"service_account"}',
65
+ });
66
+ });
67
+
68
+ it("overwrites the values it is given", async () => {
69
+ bwContains({ id: "item-id", name: ITEM_NAME, notes: "API_KEY: old\n" });
70
+
71
+ await new BitwardenVault(config, {}, false).writeSecrets(io, "dev", "www", {
72
+ API_KEY: "rotated",
73
+ });
74
+
75
+ expect(writtenNotes()).toEqual({ API_KEY: "rotated" });
76
+ });
77
+
78
+ it("creates the item when the env/component has no note yet", async () => {
79
+ bwContains();
80
+
81
+ await new BitwardenVault(config, {}, false).writeSecrets(io, "dev", "www", {
82
+ API_KEY: "sesame",
83
+ });
84
+
85
+ expect(runBw.mock.calls[0][1].slice(0, 2)).toEqual(["create", "item"]);
86
+ expect(writtenNotes()).toEqual({ API_KEY: "sesame" });
87
+ });
88
+ });
@@ -20,16 +20,32 @@ export type SecretsVault = {
20
20
 
21
21
  /**
22
22
  * upserts the secrets of one env and component
23
- * (keys as declared in the config, not the raw variable names)
23
+ * (keys as declared in the config, not the raw variable names).
24
+ * Upsert, not replace: values that are not part of `secrets` stay
25
+ * untouched.
24
26
  */
25
27
  writeSecrets(
26
28
  io: IO,
27
29
  env: string,
28
30
  componentName: string,
29
31
  secrets: Record<string, unknown>,
32
+ options?: WriteSecretsOptions,
30
33
  ): Promise<void>;
31
34
  };
32
35
 
36
+ export type WriteSecretsOptions = {
37
+ /**
38
+ * keep the previous value around where the store supports it (the
39
+ * gitlab store writes `<KEY>_backup_<timestamp>` variables) —
40
+ * a hand-written secret's only undo.
41
+ *
42
+ * Machine-provisioned credentials (service account keys, kubernetes
43
+ * tokens) are regenerated on demand and their old value is worthless,
44
+ * so `project setup` writes them with `backup: false`.
45
+ */
46
+ backup?: boolean;
47
+ };
48
+
33
49
  /**
34
50
  * how secret resolution may interact with the vault:
35
51
  * - auto (default): contact the vault when the local cache is