@catladder/cli 5.0.1 → 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.
- 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/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/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 +115 -13
- package/dist/bundles/cli/index.js +749 -431
- package/dist/bundles/skills/catladder-cli/references/commands.md +13 -0
- package/dist/bundles/skills/catladder-deploys/SKILL.md +7 -0
- package/dist/bundles/skills/catladder-migrate-ci-backend/SKILL.md +7 -2
- package/dist/bundles/skills/catladder-pipelines/SKILL.md +27 -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/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/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 +32 -0
- package/dist/skills/catladder-cli/references/commands.md +13 -0
- package/dist/skills/catladder-deploys/SKILL.md +7 -0
- package/dist/skills/catladder-migrate-ci-backend/SKILL.md +7 -2
- package/dist/skills/catladder-pipelines/SKILL.md +27 -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/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/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/package.json
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
import {
|
|
8
8
|
collectSecretTargets,
|
|
9
9
|
getConfiguredGithubRepo,
|
|
10
|
-
} from "../../../../../
|
|
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
|
|
59
|
+
if (!(e instanceof GitlabNotFoundError)) {
|
|
59
60
|
throw e;
|
|
60
61
|
}
|
|
61
|
-
// not
|
|
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 "../../../../../
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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("
|
|
172
|
+
instance.log("storing the cluster credentials...");
|
|
162
173
|
|
|
163
|
-
await
|
|
174
|
+
await writeSecretsAndMirror(
|
|
164
175
|
instance,
|
|
165
|
-
vars,
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
};
|
package/src/commands/index.ts
CHANGED
|
@@ -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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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 "
|
|
6
|
-
import { writeSecretsAndMirror } from "
|
|
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
|
|
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 {
|
|
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
|
-
};
|