@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.
- package/dist/apps/cli/src/apps/cli/commands/project/doctor/checkGithub.js +3 -3
- package/dist/apps/cli/src/apps/cli/commands/project/doctor/checkGithub.js.map +1 -1
- package/dist/apps/cli/src/apps/cli/commands/project/doctor/checkGithubImageCasing.d.ts +15 -0
- package/dist/apps/cli/src/apps/cli/commands/project/doctor/checkGithubImageCasing.js +60 -0
- package/dist/apps/cli/src/apps/cli/commands/project/doctor/checkGithubImageCasing.js.map +1 -0
- package/dist/apps/cli/src/apps/cli/commands/project/doctor/index.js +2 -0
- package/dist/apps/cli/src/apps/cli/commands/project/doctor/index.js.map +1 -1
- package/dist/apps/cli/src/apps/cli/commands/project/setup/setupAccessTokens.js +2 -2
- package/dist/apps/cli/src/apps/cli/commands/project/setup/setupAccessTokens.js.map +1 -1
- package/dist/apps/cli/src/apps/cli/commands/project/setup/setupGithub.js +2 -2
- package/dist/apps/cli/src/apps/cli/commands/project/setup/setupGithub.js.map +1 -1
- package/dist/apps/cli/src/apps/cli/commands/project/setup/setupKubernetes.js +23 -7
- package/dist/apps/cli/src/apps/cli/commands/project/setup/setupKubernetes.js.map +1 -1
- package/dist/apps/cli/src/commands/index.d.ts +1 -0
- package/dist/apps/cli/src/commands/index.js +4 -1
- package/dist/apps/cli/src/commands/index.js.map +1 -1
- package/dist/apps/cli/src/commands/project/commandConfigSecrets.js +2 -1
- package/dist/apps/cli/src/commands/project/commandConfigSecrets.js.map +1 -1
- package/dist/apps/cli/src/commands/project/commandMrPin.d.ts +21 -0
- package/dist/apps/cli/src/commands/project/commandMrPin.js +119 -0
- package/dist/apps/cli/src/commands/project/commandMrPin.js.map +1 -0
- package/dist/apps/cli/src/commands/project/commandSecretsSyncGithub.d.ts +1 -42
- package/dist/apps/cli/src/commands/project/commandSecretsSyncGithub.js +5 -124
- package/dist/apps/cli/src/commands/project/commandSecretsSyncGithub.js.map +1 -1
- package/dist/apps/cli/src/commands/project/secrets/commandSecretsPush.js +2 -1
- package/dist/apps/cli/src/commands/project/secrets/commandSecretsPush.js.map +1 -1
- package/dist/apps/cli/src/commands/project/secrets/commandSecretsSet.js +2 -2
- package/dist/apps/cli/src/commands/project/secrets/commandSecretsSet.js.map +1 -1
- package/dist/apps/cli/src/commands/project/secrets/document.d.ts +1 -12
- package/dist/apps/cli/src/commands/project/secrets/document.js +1 -79
- package/dist/apps/cli/src/commands/project/secrets/document.js.map +1 -1
- package/dist/apps/cli/src/gcloud/serviceAccounts.js +15 -4
- package/dist/apps/cli/src/gcloud/serviceAccounts.js.map +1 -1
- package/dist/apps/cli/src/secrets/github.d.ts +41 -0
- package/dist/apps/cli/src/secrets/github.js +127 -0
- package/dist/apps/cli/src/secrets/github.js.map +1 -0
- package/dist/apps/cli/src/secrets/index.d.ts +2 -0
- package/dist/apps/cli/src/secrets/index.js +19 -0
- package/dist/apps/cli/src/secrets/index.js.map +1 -0
- package/dist/apps/cli/src/secrets/writeSecrets.d.ts +18 -0
- package/dist/apps/cli/src/secrets/writeSecrets.js +88 -0
- package/dist/apps/cli/src/secrets/writeSecrets.js.map +1 -0
- package/dist/apps/cli/src/utils/github.js +2 -2
- package/dist/apps/cli/src/utils/github.js.map +1 -1
- package/dist/apps/cli/src/utils/gitlab.d.ts +10 -0
- package/dist/apps/cli/src/utils/gitlab.js +30 -5
- package/dist/apps/cli/src/utils/gitlab.js.map +1 -1
- package/dist/apps/cli/src/vault/BitwardenVault.d.ts +6 -0
- package/dist/apps/cli/src/vault/BitwardenVault.js +11 -1
- package/dist/apps/cli/src/vault/BitwardenVault.js.map +1 -1
- package/dist/apps/cli/src/vault/GitlabVault.d.ts +2 -2
- package/dist/apps/cli/src/vault/GitlabVault.js +3 -2
- package/dist/apps/cli/src/vault/GitlabVault.js.map +1 -1
- package/dist/apps/cli/src/vault/VaultManager.d.ts +2 -2
- package/dist/apps/cli/src/vault/VaultManager.js +2 -2
- package/dist/apps/cli/src/vault/VaultManager.js.map +1 -1
- package/dist/apps/cli/src/vault/types.d.ts +16 -2
- package/dist/bundles/catenv/index.js +265 -28
- package/dist/bundles/cli/index.js +851 -329
- package/dist/bundles/skills/catladder-cli/references/commands.md +13 -0
- package/dist/bundles/skills/catladder-config/SKILL.md +3 -1
- package/dist/bundles/skills/catladder-deploys/SKILL.md +7 -0
- package/dist/bundles/skills/catladder-migrate-ci-backend/SKILL.md +18 -2
- package/dist/bundles/skills/catladder-pipelines/SKILL.md +51 -1
- package/dist/bundles/skills/catladder-secrets/SKILL.md +16 -0
- package/dist/packages/pipeline/src/autoStop.d.ts +22 -0
- package/dist/packages/pipeline/src/autoStop.js +30 -0
- package/dist/packages/pipeline/src/autoStop.js.map +1 -0
- package/dist/packages/pipeline/src/backends/github/GithubBackend.js +11 -5
- package/dist/packages/pipeline/src/backends/github/GithubBackend.js.map +1 -1
- package/dist/packages/pipeline/src/backends/github/ciVariables.d.ts +13 -2
- package/dist/packages/pipeline/src/backends/github/ciVariables.js +18 -5
- package/dist/packages/pipeline/src/backends/github/ciVariables.js.map +1 -1
- package/dist/packages/pipeline/src/backends/github/createGithubJobs.js +1 -1
- package/dist/packages/pipeline/src/backends/github/createGithubJobs.js.map +1 -1
- package/dist/packages/pipeline/src/backends/github/ghcr.d.ts +29 -0
- package/dist/packages/pipeline/src/backends/github/ghcr.js +40 -0
- package/dist/packages/pipeline/src/backends/github/ghcr.js.map +1 -0
- package/dist/packages/pipeline/src/backends/github/index.d.ts +2 -0
- package/dist/packages/pipeline/src/backends/github/index.js +2 -0
- package/dist/packages/pipeline/src/backends/github/index.js.map +1 -1
- package/dist/packages/pipeline/src/backends/github/registryImage.d.ts +20 -0
- package/dist/packages/pipeline/src/backends/github/registryImage.js +49 -0
- package/dist/packages/pipeline/src/backends/github/registryImage.js.map +1 -0
- package/dist/packages/pipeline/src/backends/gitlab/GitlabBackend.js +2 -0
- package/dist/packages/pipeline/src/backends/gitlab/GitlabBackend.js.map +1 -1
- package/dist/packages/pipeline/src/backends/gitlab/createGitlabPipeline.d.ts +4 -1
- package/dist/packages/pipeline/src/backends/gitlab/createGitlabPipeline.js +21 -1
- package/dist/packages/pipeline/src/backends/gitlab/createGitlabPipeline.js.map +1 -1
- package/dist/packages/pipeline/src/customImages/jobImagesPlan.d.ts +15 -3
- package/dist/packages/pipeline/src/customImages/jobImagesPlan.js +14 -3
- package/dist/packages/pipeline/src/customImages/jobImagesPlan.js.map +1 -1
- package/dist/packages/pipeline/src/deploy/base/deploy.js +8 -2
- package/dist/packages/pipeline/src/deploy/base/deploy.js.map +1 -1
- package/dist/packages/pipeline/src/index.d.ts +1 -0
- package/dist/packages/pipeline/src/index.js +1 -0
- package/dist/packages/pipeline/src/index.js.map +1 -1
- package/dist/packages/pipeline/src/types/config.d.ts +44 -0
- package/dist/skills/catladder-cli/references/commands.md +13 -0
- package/dist/skills/catladder-config/SKILL.md +3 -1
- package/dist/skills/catladder-deploys/SKILL.md +7 -0
- package/dist/skills/catladder-migrate-ci-backend/SKILL.md +18 -2
- package/dist/skills/catladder-pipelines/SKILL.md +51 -1
- package/dist/skills/catladder-secrets/SKILL.md +16 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/apps/cli/commands/project/doctor/checkGithub.ts +1 -1
- package/src/apps/cli/commands/project/doctor/checkGithubImageCasing.ts +87 -0
- package/src/apps/cli/commands/project/doctor/index.ts +2 -0
- package/src/apps/cli/commands/project/setup/setupAccessTokens.ts +3 -2
- package/src/apps/cli/commands/project/setup/setupGithub.ts +1 -1
- package/src/apps/cli/commands/project/setup/setupKubernetes.ts +31 -20
- package/src/commands/index.ts +1 -0
- package/src/commands/project/commandConfigSecrets.ts +1 -1
- package/src/commands/project/commandMrPin.ts +184 -0
- package/src/commands/project/commandSecretsSyncGithub.ts +7 -154
- package/src/commands/project/secrets/commandSecretsPush.ts +2 -5
- package/src/commands/project/secrets/commandSecretsSet.ts +2 -2
- package/src/commands/project/secrets/document.ts +2 -127
- package/src/gcloud/serviceAccounts.ts +16 -7
- package/src/secrets/__tests__/writeSecrets.test.ts +147 -0
- package/src/secrets/github.ts +157 -0
- package/src/secrets/index.ts +2 -0
- package/src/secrets/writeSecrets.ts +138 -0
- package/src/utils/github.ts +2 -4
- package/src/utils/gitlab.ts +34 -7
- package/src/vault/BitwardenVault.ts +10 -1
- package/src/vault/GitlabVault.ts +9 -2
- package/src/vault/VaultManager.ts +3 -2
- package/src/vault/__tests__/BitwardenVault.test.ts +88 -0
- package/src/vault/types.ts +17 -1
package/src/utils/gitlab.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
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, [
|
package/src/vault/GitlabVault.ts
CHANGED
|
@@ -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(
|
|
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
|
+
});
|
package/src/vault/types.ts
CHANGED
|
@@ -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
|