@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/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,
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { readdir, readFile } from "fs/promises";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import type { Config } from "@catladder/pipeline";
|
|
4
|
+
import {
|
|
5
|
+
GHCR_REPOSITORY_EXPRESSION,
|
|
6
|
+
getEnabledPipelineTypes,
|
|
7
|
+
getPipelineGitRemote,
|
|
8
|
+
} from "@catladder/pipeline";
|
|
9
|
+
import { getConfiguredGithubRepo } from "../../../../../secrets";
|
|
10
|
+
import type { DoctorReport } from "./DoctorReport";
|
|
11
|
+
|
|
12
|
+
const WORKFLOWS_FOLDER = ".github/workflows";
|
|
13
|
+
const GENERATED_FILE_PREFIX = "catladder-";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* the generated workflow files, or an empty list when none exist yet
|
|
17
|
+
*/
|
|
18
|
+
const readGeneratedWorkflows = async (): Promise<string[]> => {
|
|
19
|
+
const entries = await readdir(WORKFLOWS_FOLDER).catch(() => []);
|
|
20
|
+
return Promise.all(
|
|
21
|
+
entries
|
|
22
|
+
.filter((file) => file.startsWith(GENERATED_FILE_PREFIX))
|
|
23
|
+
.map((file) => readFile(join(WORKFLOWS_FOLDER, file), "utf8")),
|
|
24
|
+
);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* ghcr image paths must be lowercase, but `${{ github.repository }}`
|
|
29
|
+
* interpolates github's display casing — so a repository whose owner or
|
|
30
|
+
* name contains an uppercase character needs a lowercased literal in the
|
|
31
|
+
* generated workflows. Generation bakes that in, which means committed
|
|
32
|
+
* workflows generated by an older catladder (or hand-edited ones) still
|
|
33
|
+
* carry the expression and fail every docker job with
|
|
34
|
+
* `repository name must be lowercase`.
|
|
35
|
+
*
|
|
36
|
+
* Unlike the rest of the github checks this needs no `gh` login — only
|
|
37
|
+
* the git remote and the committed files.
|
|
38
|
+
*/
|
|
39
|
+
export const checkGithubImageCasing = async (
|
|
40
|
+
report: DoctorReport,
|
|
41
|
+
config: Config,
|
|
42
|
+
) => {
|
|
43
|
+
if (!getEnabledPipelineTypes(config).includes("github")) return;
|
|
44
|
+
|
|
45
|
+
const repo = await getConfiguredGithubRepo();
|
|
46
|
+
if (!repo || repo === repo.toLowerCase()) {
|
|
47
|
+
// an all-lowercase repository keeps the expression on purpose (it
|
|
48
|
+
// stays correct in forks), and an unresolvable remote is reported
|
|
49
|
+
// by checkGithub already
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
report.section("github image names");
|
|
54
|
+
|
|
55
|
+
const workflows = await readGeneratedWorkflows();
|
|
56
|
+
if (workflows.length === 0) {
|
|
57
|
+
report.warn(
|
|
58
|
+
`no generated workflows in ${WORKFLOWS_FOLDER} to check`,
|
|
59
|
+
"run: catenv",
|
|
60
|
+
);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const stale = workflows.filter((content) =>
|
|
65
|
+
content.includes(GHCR_REPOSITORY_EXPRESSION),
|
|
66
|
+
);
|
|
67
|
+
if (stale.length > 0) {
|
|
68
|
+
report.fail(
|
|
69
|
+
`'${repo}' is mixed-case, but ${stale.length}/${workflows.length} generated workflows still use ${GHCR_REPOSITORY_EXPRESSION} — ghcr rejects uppercase image paths, so every docker job fails with "repository name must be lowercase"`,
|
|
70
|
+
"run: catenv (and commit the regenerated workflows)",
|
|
71
|
+
);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const uppercase = workflows.flatMap((content) =>
|
|
76
|
+
[...content.matchAll(/ghcr\.io\/\S*[A-Z]\S*/g)].map(([match]) => match),
|
|
77
|
+
);
|
|
78
|
+
if (uppercase.length > 0) {
|
|
79
|
+
report.fail(
|
|
80
|
+
`uppercase ghcr image paths in the generated workflows: ${[...new Set(uppercase)].join(", ")}`,
|
|
81
|
+
`run: catenv — if it persists, set pipelines.github.repository (the '${getPipelineGitRemote(config, "github")}' remote resolves to '${repo}')`,
|
|
82
|
+
);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
report.ok(`ghcr image paths lowercased for '${repo}'`);
|
|
87
|
+
};
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
import type { IO } from "../../../../../core/types";
|
|
8
8
|
import { checkCloudRun } from "./checkCloudRun";
|
|
9
9
|
import { checkGithub } from "./checkGithub";
|
|
10
|
+
import { checkGithubImageCasing } from "./checkGithubImageCasing";
|
|
10
11
|
import { checkGitlab } from "./checkGitlab";
|
|
11
12
|
import { checkStore } from "./checkStore";
|
|
12
13
|
import { DoctorReport } from "./DoctorReport";
|
|
@@ -53,6 +54,7 @@ export const doctorProject = async (
|
|
|
53
54
|
await checkGitlab(instance, report, contexts);
|
|
54
55
|
}
|
|
55
56
|
await checkCloudRun(report, contexts);
|
|
57
|
+
await checkGithubImageCasing(report, config);
|
|
56
58
|
await checkGithub(report, config);
|
|
57
59
|
|
|
58
60
|
report.summarize();
|
|
@@ -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,
|