@catladder/cli 0.0.0-next-6ea918c7 → 0.0.0-next-e3ccf7d4
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/bundles/catenv/index.js +33 -8
- package/dist/bundles/cli/index.js +114 -25
- package/dist/cli/src/apps/cli/commands/project/commandConfigSecrets.js +1 -1
- package/dist/cli/src/apps/cli/commands/project/commandConfigSecrets.js.map +1 -1
- package/dist/cli/src/commands/project/commandSecretsSyncGithub.d.ts +17 -3
- package/dist/cli/src/commands/project/commandSecretsSyncGithub.js +60 -11
- package/dist/cli/src/commands/project/commandSecretsSyncGithub.js.map +1 -1
- package/dist/cli/src/utils/github.d.ts +7 -2
- package/dist/cli/src/utils/github.js +21 -6
- package/dist/cli/src/utils/github.js.map +1 -1
- package/dist/pipeline/src/backends/github/createGithubJobs.js +32 -7
- package/dist/pipeline/src/backends/github/createGithubJobs.js.map +1 -1
- package/dist/pipeline/src/constants.js +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/apps/cli/commands/project/commandConfigSecrets.ts +7 -1
- package/src/commands/project/commandSecretsSyncGithub.ts +81 -9
- package/src/utils/github.ts +50 -5
package/package.json
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
parseChoice,
|
|
18
18
|
} from "../../../../config/getProjectConfig";
|
|
19
19
|
import {
|
|
20
|
+
collectSecretTargets,
|
|
20
21
|
getConfiguredGithubRepo,
|
|
21
22
|
pushSecretsToGithub,
|
|
22
23
|
} from "../../../../commands/project/commandSecretsSyncGithub";
|
|
@@ -292,7 +293,12 @@ const mirrorSecretsToCiBackends = async (
|
|
|
292
293
|
return;
|
|
293
294
|
}
|
|
294
295
|
instance.log(`mirroring ${secrets.length} secrets to github ('${repo}')...`);
|
|
295
|
-
await pushSecretsToGithub(
|
|
296
|
+
await pushSecretsToGithub(
|
|
297
|
+
instance,
|
|
298
|
+
repo,
|
|
299
|
+
secrets,
|
|
300
|
+
await collectSecretTargets(config),
|
|
301
|
+
);
|
|
296
302
|
};
|
|
297
303
|
|
|
298
304
|
export const projectConfigSecrets = async (io: IO, envComponent?: string) => {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import type { Config } from "@catladder/pipeline";
|
|
1
2
|
import {
|
|
2
3
|
getEnabledPipelineTypes,
|
|
3
4
|
getPipelineGitRemote,
|
|
4
5
|
getSecretVarName,
|
|
6
|
+
GithubBackend,
|
|
5
7
|
} from "@catladder/pipeline";
|
|
6
8
|
import {
|
|
7
9
|
getAllComponentsWithAllEnvsHierarchical,
|
|
@@ -11,6 +13,7 @@ import {
|
|
|
11
13
|
import { defineCommand } from "../../core/defineCommand";
|
|
12
14
|
import type { IO } from "../../core/types";
|
|
13
15
|
import {
|
|
16
|
+
ensureGithubEnvironment,
|
|
14
17
|
getGithubRepoFromRemote,
|
|
15
18
|
isGhAuthenticated,
|
|
16
19
|
setGithubSecret,
|
|
@@ -58,22 +61,86 @@ export const collectSecretsFromVault = async (io: IO, envFilter?: string[]) => {
|
|
|
58
61
|
return { secrets, missing };
|
|
59
62
|
};
|
|
60
63
|
|
|
64
|
+
/** marker for repo-level (environment-less) targets */
|
|
65
|
+
const REPO_LEVEL = "";
|
|
66
|
+
|
|
61
67
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
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
|
|
64
110
|
*/
|
|
65
111
|
export const pushSecretsToGithub = async (
|
|
66
112
|
io: IO,
|
|
67
113
|
repo: string,
|
|
68
114
|
secrets: { name: string; value: string; kind: "secret" | "variable" }[],
|
|
115
|
+
targets: Map<string, Set<string>>,
|
|
69
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
|
+
|
|
70
129
|
for (const { name, value, kind } of secrets) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
io.log(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
+
}
|
|
77
144
|
}
|
|
78
145
|
}
|
|
79
146
|
};
|
|
@@ -163,7 +230,12 @@ export const commandSecretsSyncGithub = defineCommand({
|
|
|
163
230
|
throw new Error("abort");
|
|
164
231
|
}
|
|
165
232
|
|
|
166
|
-
await pushSecretsToGithub(
|
|
233
|
+
await pushSecretsToGithub(
|
|
234
|
+
ctx,
|
|
235
|
+
repo,
|
|
236
|
+
secrets,
|
|
237
|
+
await collectSecretTargets(config),
|
|
238
|
+
);
|
|
167
239
|
ctx.log("");
|
|
168
240
|
ctx.log(
|
|
169
241
|
"done! 😻 the github pipeline can use them as ${{ secrets.<NAME> }}",
|
package/src/utils/github.ts
CHANGED
|
@@ -39,11 +39,21 @@ const ghSetViaStdin = (
|
|
|
39
39
|
repo: string,
|
|
40
40
|
name: string,
|
|
41
41
|
value: string,
|
|
42
|
+
environment?: string,
|
|
42
43
|
): Promise<void> =>
|
|
43
44
|
new Promise<void>((resolve, reject) => {
|
|
44
|
-
const child = spawn(
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
const child = spawn(
|
|
46
|
+
"gh",
|
|
47
|
+
[
|
|
48
|
+
kind,
|
|
49
|
+
"set",
|
|
50
|
+
name,
|
|
51
|
+
"--repo",
|
|
52
|
+
repo,
|
|
53
|
+
...(environment ? ["--env", environment] : []),
|
|
54
|
+
],
|
|
55
|
+
{ stdio: ["pipe", "ignore", "pipe"] },
|
|
56
|
+
);
|
|
47
57
|
let stderr = "";
|
|
48
58
|
child.stderr?.on("data", (data) => (stderr += data));
|
|
49
59
|
child.on("error", reject);
|
|
@@ -63,7 +73,8 @@ export const setGithubSecret = async (
|
|
|
63
73
|
repo: string,
|
|
64
74
|
name: string,
|
|
65
75
|
value: string,
|
|
66
|
-
|
|
76
|
+
environment?: string,
|
|
77
|
+
): Promise<void> => ghSetViaStdin("secret", repo, name, value, environment);
|
|
67
78
|
|
|
68
79
|
/**
|
|
69
80
|
* sets a github actions repository variable (plain, unmasked) via the
|
|
@@ -73,4 +84,38 @@ export const setGithubVariable = async (
|
|
|
73
84
|
repo: string,
|
|
74
85
|
name: string,
|
|
75
86
|
value: string,
|
|
76
|
-
|
|
87
|
+
environment?: string,
|
|
88
|
+
): Promise<void> => ghSetViaStdin("variable", repo, name, value, environment);
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* creates the github environment when it does not exist (idempotent —
|
|
92
|
+
* PUT with an empty body leaves existing settings untouched)
|
|
93
|
+
*/
|
|
94
|
+
export const ensureGithubEnvironment = async (
|
|
95
|
+
repo: string,
|
|
96
|
+
environment: string,
|
|
97
|
+
): Promise<void> =>
|
|
98
|
+
new Promise<void>((resolve, reject) => {
|
|
99
|
+
const child = spawn(
|
|
100
|
+
"gh",
|
|
101
|
+
[
|
|
102
|
+
"api",
|
|
103
|
+
"-X",
|
|
104
|
+
"PUT",
|
|
105
|
+
`repos/${repo}/environments/${encodeURIComponent(environment)}`,
|
|
106
|
+
],
|
|
107
|
+
{ stdio: ["ignore", "ignore", "pipe"] },
|
|
108
|
+
);
|
|
109
|
+
let stderr = "";
|
|
110
|
+
child.stderr?.on("data", (data) => (stderr += data));
|
|
111
|
+
child.on("error", reject);
|
|
112
|
+
child.on("close", (code) =>
|
|
113
|
+
code === 0
|
|
114
|
+
? resolve()
|
|
115
|
+
: reject(
|
|
116
|
+
new Error(
|
|
117
|
+
`creating github environment ${environment} failed: ${stderr.trim()}`,
|
|
118
|
+
),
|
|
119
|
+
),
|
|
120
|
+
);
|
|
121
|
+
});
|