@smoothbricks/cli 0.11.17 → 0.11.18
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/README.md +22 -4
- package/dist/cli.js +10 -7
- package/dist/monorepo/ci-workflow.d.ts +56 -0
- package/dist/monorepo/ci-workflow.d.ts.map +1 -1
- package/dist/monorepo/ci-workflow.js +199 -2
- package/dist/monorepo/managed-files.d.ts +23 -0
- package/dist/monorepo/managed-files.d.ts.map +1 -1
- package/dist/monorepo/managed-files.js +39 -1
- package/dist/release/github-release.d.ts +10 -0
- package/dist/release/github-release.d.ts.map +1 -1
- package/dist/release/github-release.js +11 -5
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +4 -5
- package/dist/secrets/commands.d.ts +52 -13
- package/dist/secrets/commands.d.ts.map +1 -1
- package/dist/secrets/commands.js +220 -34
- package/dist/secrets/index.d.ts +21 -1
- package/dist/secrets/index.d.ts.map +1 -1
- package/dist/secrets/index.js +110 -4
- package/dist/secrets/repository.d.ts +60 -0
- package/dist/secrets/repository.d.ts.map +1 -0
- package/dist/secrets/repository.js +145 -0
- package/dist/wrangler/cloudflare.d.ts +7 -0
- package/dist/wrangler/cloudflare.d.ts.map +1 -1
- package/dist/wrangler/cloudflare.js +10 -0
- package/dist/wrangler/deploy-stage.d.ts.map +1 -1
- package/dist/wrangler/deploy-stage.js +50 -24
- package/dist/wrangler/stage-secrets.d.ts +57 -0
- package/dist/wrangler/stage-secrets.d.ts.map +1 -0
- package/dist/wrangler/stage-secrets.js +178 -0
- package/managed/raw/tooling/direnv/devenv.smoo.nix +9 -1
- package/managed/raw/tooling/git-hooks/pre-push.sh +30 -29
- package/package.json +2 -2
- package/src/cli.ts +33 -10
- package/src/monorepo/__tests__/ci-workflow.test.ts +161 -0
- package/src/monorepo/ci-workflow.ts +277 -2
- package/src/monorepo/managed-files.test.ts +27 -0
- package/src/monorepo/managed-files.ts +56 -2
- package/src/monorepo/package-policy.test.ts +1 -1
- package/src/release/__tests__/github-release.test.ts +8 -4
- package/src/release/github-release.ts +13 -5
- package/src/release/index.ts +4 -4
- package/src/secrets/commands.test.ts +28 -0
- package/src/secrets/commands.ts +237 -35
- package/src/secrets/index.test.ts +118 -1
- package/src/secrets/index.ts +108 -4
- package/src/secrets/repository.test.ts +98 -0
- package/src/secrets/repository.ts +164 -0
- package/src/wrangler/cloudflare.ts +18 -0
- package/src/wrangler/deploy-stage.test.ts +258 -11
- package/src/wrangler/deploy-stage.ts +70 -38
- package/src/wrangler/stage-secrets.ts +146 -0
package/dist/secrets/index.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* payment library's own `publishable_key_mismatch` refusal.
|
|
20
20
|
*/
|
|
21
21
|
import { spawnSync } from 'node:child_process';
|
|
22
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
22
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
23
23
|
import { join } from 'node:path';
|
|
24
24
|
import { repositoryOwnerFromUrl, repositorySecretMapping } from '../lib/secret-names.js';
|
|
25
25
|
import { readPackageJsonObject, repositoryInfo } from '../lib/workspace.js';
|
|
@@ -36,9 +36,17 @@ export function reconcileSecrets(sources) {
|
|
|
36
36
|
// GITHUB_CLIENT_SECRET would report one value as two secrets, one of them
|
|
37
37
|
// permanently "declared by nothing".
|
|
38
38
|
const carriesKnownEnvName = new Set(names.size > 0 ? [...names].map((name) => sources.secretNames[name] ?? name) : []);
|
|
39
|
-
|
|
39
|
+
// A value only an environment holds is still a value: leaving it out of the
|
|
40
|
+
// rows is how an operator ends up hunting for a secret that is already set.
|
|
41
|
+
const addUndeclared = (secret) => {
|
|
40
42
|
if (!carriesKnownEnvName.has(secret))
|
|
41
43
|
names.add(secret);
|
|
44
|
+
};
|
|
45
|
+
for (const secret of sources.repositorySecrets)
|
|
46
|
+
addUndeclared(secret);
|
|
47
|
+
for (const held of Object.values(sources.environmentSecrets ?? {})) {
|
|
48
|
+
for (const secret of held)
|
|
49
|
+
addUndeclared(secret);
|
|
42
50
|
}
|
|
43
51
|
return [...names]
|
|
44
52
|
.sort((left, right) => left.localeCompare(right))
|
|
@@ -52,6 +60,10 @@ export function reconcileSecrets(sources) {
|
|
|
52
60
|
suppliedByWorkflow: sources.workflowSecrets.includes(name),
|
|
53
61
|
fetchableLocally: sources.localCommands.includes(name),
|
|
54
62
|
onRepository: sources.repositorySecrets.includes(sources.secretNames[name] ?? name),
|
|
63
|
+
heldByEnvironment: Object.entries(sources.environmentSecrets ?? {})
|
|
64
|
+
.filter(([, held]) => held.includes(sources.secretNames[name] ?? name))
|
|
65
|
+
.map(([environment]) => environment)
|
|
66
|
+
.sort((left, right) => left.localeCompare(right)),
|
|
55
67
|
}));
|
|
56
68
|
}
|
|
57
69
|
/**
|
|
@@ -60,8 +72,25 @@ export function reconcileSecrets(sources) {
|
|
|
60
72
|
* separately from "declared but not wired into any workflow", because the
|
|
61
73
|
* remedies differ — set a secret, versus declare it in `smoo.github`.
|
|
62
74
|
*/
|
|
63
|
-
export function unsatisfiedSecrets(rows) {
|
|
64
|
-
return rows.filter((row) =>
|
|
75
|
+
export function unsatisfiedSecrets(rows, boundEnvironments = []) {
|
|
76
|
+
return rows.filter((row) => {
|
|
77
|
+
if (!row.suppliedByWorkflow)
|
|
78
|
+
return false;
|
|
79
|
+
if (row.onRepository)
|
|
80
|
+
return false;
|
|
81
|
+
// With no environment bound, the repository is the only scope a job reads.
|
|
82
|
+
// With environments bound, each one can carry the value instead - so the
|
|
83
|
+
// name is satisfied only when every bound environment holds it.
|
|
84
|
+
if (boundEnvironments.length === 0)
|
|
85
|
+
return true;
|
|
86
|
+
return !boundEnvironments.every((environment) => row.heldByEnvironment.includes(environment));
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/** Bound environments that lack their own value and cannot fall back to the repository. */
|
|
90
|
+
export function environmentsMissing(row, boundEnvironments) {
|
|
91
|
+
if (row.onRepository)
|
|
92
|
+
return [];
|
|
93
|
+
return boundEnvironments.filter((environment) => !row.heldByEnvironment.includes(environment));
|
|
65
94
|
}
|
|
66
95
|
/** Worker-declared names no managed workflow passes: a CI deploy will run without them. */
|
|
67
96
|
export function unwiredSecrets(rows) {
|
|
@@ -92,6 +121,83 @@ export function workflowSecretNames(root) {
|
|
|
92
121
|
names.add(smoo.privateNpm.publishTokenEnv);
|
|
93
122
|
return [...names].sort((left, right) => left.localeCompare(right));
|
|
94
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* GitHub Environments the rendered workflows bind, read from the workflow
|
|
126
|
+
* files rather than from config: a binding may be hand-authored inside a
|
|
127
|
+
* `smoo-local` block, and the file is what GitHub executes either way. A job
|
|
128
|
+
* bound to an environment resolves `secrets.X` from that environment first and
|
|
129
|
+
* from the repository second, so these names are the scopes a value can live
|
|
130
|
+
* in. Expressions are skipped - a computed environment names no fixed scope.
|
|
131
|
+
*/
|
|
132
|
+
export function workflowEnvironments(root) {
|
|
133
|
+
const directory = join(root, '.github', 'workflows');
|
|
134
|
+
if (!existsSync(directory))
|
|
135
|
+
return [];
|
|
136
|
+
const environments = new Set();
|
|
137
|
+
for (const entry of readdirSync(directory)) {
|
|
138
|
+
if (!entry.endsWith('.yml') && !entry.endsWith('.yaml'))
|
|
139
|
+
continue;
|
|
140
|
+
for (const bound of environmentBindings(readFileSync(join(directory, entry), 'utf8'))) {
|
|
141
|
+
environments.add(bound);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return [...environments].sort((left, right) => left.localeCompare(right));
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The environments one workflow file binds, in both spellings GitHub accepts:
|
|
148
|
+
* `environment: staging`, and the block form whose `name:` sits under it when
|
|
149
|
+
* the job also records a deployment URL. Only a `name:` indented inside an
|
|
150
|
+
* `environment:` block is a binding - a workflow's, a job's and a step's are
|
|
151
|
+
* not, and reading those as environments would send an operator to set
|
|
152
|
+
* secrets in scopes that do not exist.
|
|
153
|
+
*/
|
|
154
|
+
function environmentBindings(text) {
|
|
155
|
+
const lines = text.split('\n');
|
|
156
|
+
const bound = [];
|
|
157
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
158
|
+
const binding = /^(\s*)environment:(.*)$/.exec(lines[index] ?? '');
|
|
159
|
+
if (!binding)
|
|
160
|
+
continue;
|
|
161
|
+
const indent = (binding[1] ?? '').length;
|
|
162
|
+
const value = binding[2] ?? '';
|
|
163
|
+
// Nothing but a comment after the colon is the block form; anything else
|
|
164
|
+
// is the value itself.
|
|
165
|
+
if (!/^\s*(?:#.*)?$/.test(value)) {
|
|
166
|
+
const name = literalEnvironmentName(value);
|
|
167
|
+
if (name !== null)
|
|
168
|
+
bound.push(name);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
for (let next = index + 1; next < lines.length; next += 1) {
|
|
172
|
+
const line = lines[next] ?? '';
|
|
173
|
+
const content = line.trimStart();
|
|
174
|
+
if (content.length === 0 || content.startsWith('#'))
|
|
175
|
+
continue;
|
|
176
|
+
if (line.length - content.length <= indent)
|
|
177
|
+
break;
|
|
178
|
+
const named = /^name:(.*)$/.exec(content);
|
|
179
|
+
if (!named)
|
|
180
|
+
continue;
|
|
181
|
+
const name = literalEnvironmentName(named[1] ?? '');
|
|
182
|
+
if (name !== null)
|
|
183
|
+
bound.push(name);
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return bound;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* The fixed name a YAML value denotes, or null when it denotes no fixed scope:
|
|
191
|
+
* an expression is computed per run, and a flow mapping or a list is not a
|
|
192
|
+
* name. Names may contain spaces, which is why the renderer quotes them.
|
|
193
|
+
*/
|
|
194
|
+
function literalEnvironmentName(value) {
|
|
195
|
+
const scalar = value.replace(/\s+#.*$/, '').trim();
|
|
196
|
+
if (scalar.length === 0 || scalar.includes('${{'))
|
|
197
|
+
return null;
|
|
198
|
+
const unquoted = /^(['"])(.*)\1$/.exec(scalar)?.[2] ?? scalar;
|
|
199
|
+
return /^[A-Za-z0-9._-][A-Za-z0-9._ -]*$/.test(unquoted) ? unquoted : null;
|
|
200
|
+
}
|
|
95
201
|
/**
|
|
96
202
|
* Env name -> repository secret for every name in play, following the naming
|
|
97
203
|
* convention and honouring the declared exceptions a repository still needs.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which GitHub repository `smoo secrets` reads and writes.
|
|
3
|
+
*
|
|
4
|
+
* `gh` refuses on a checkout with more than one remote ("multiple remotes
|
|
5
|
+
* detected"), which is exactly the public/private mirror layout this CLI's own
|
|
6
|
+
* users run — so the repository has to be resolved here, from the checkout,
|
|
7
|
+
* before `gh` is spawned. The current branch's upstream decides it: a branch
|
|
8
|
+
* that pushes to the private mirror is a branch whose secrets live there. That
|
|
9
|
+
* is a fact the checkout already records, not a preference to configure.
|
|
10
|
+
*/
|
|
11
|
+
export interface RepositoryChoice {
|
|
12
|
+
/** `owner/name`, the form `gh --repo` takes. */
|
|
13
|
+
readonly repo: string;
|
|
14
|
+
/** Why this repository, for the operator to read back. */
|
|
15
|
+
readonly source: string;
|
|
16
|
+
}
|
|
17
|
+
/** `owner/name` from any git remote URL form, or null when it names no repository. */
|
|
18
|
+
export declare function repositorySlugFromUrl(url: string): string | null;
|
|
19
|
+
/**
|
|
20
|
+
* Pure resolution, so the precedence is testable without a checkout.
|
|
21
|
+
*
|
|
22
|
+
* A requested value is either a remote name or an `owner/name` slug; naming a
|
|
23
|
+
* remote is what an operator reaches for first (`-R private-repo`), and
|
|
24
|
+
* refusing it because it lacks a slash would be pedantry.
|
|
25
|
+
*
|
|
26
|
+
* With no request and no upstream - a branch pushed by explicit refspec, which
|
|
27
|
+
* is how the mirror layout is driven - the remote-tracking refs still record
|
|
28
|
+
* where this branch has been pushed. One remote carrying it is an answer; two
|
|
29
|
+
* is a genuine ambiguity worth refusing.
|
|
30
|
+
*/
|
|
31
|
+
export declare function chooseRepository(inputs: {
|
|
32
|
+
readonly remotes: ReadonlyMap<string, string>;
|
|
33
|
+
readonly upstreamRemote: string | null;
|
|
34
|
+
/** Remotes that already carry the current branch, from its remote-tracking refs. */
|
|
35
|
+
readonly remotesCarryingBranch?: readonly string[];
|
|
36
|
+
/** Current branch name, for the refusal to say which branch decided nothing. */
|
|
37
|
+
readonly branch?: string | null;
|
|
38
|
+
readonly requested: string | undefined;
|
|
39
|
+
}): {
|
|
40
|
+
ok: true;
|
|
41
|
+
choice: RepositoryChoice;
|
|
42
|
+
} | {
|
|
43
|
+
ok: false;
|
|
44
|
+
reason: string;
|
|
45
|
+
};
|
|
46
|
+
/** Remote name -> URL, in config order. */
|
|
47
|
+
export declare function readRemotes(root: string): Map<string, string>;
|
|
48
|
+
/** Remotes with a remote-tracking ref for the current branch: where it has been pushed. */
|
|
49
|
+
export declare function readRemotesCarryingBranch(root: string, remotes: ReadonlyMap<string, string>): string[];
|
|
50
|
+
/** The remote the current branch tracks, or null when it tracks nothing. */
|
|
51
|
+
export declare function readUpstreamRemote(root: string): string | null;
|
|
52
|
+
/** The repository to operate on, resolved from the checkout. */
|
|
53
|
+
export declare function resolveRepository(root: string, requested: string | undefined): {
|
|
54
|
+
ok: true;
|
|
55
|
+
choice: RepositoryChoice;
|
|
56
|
+
} | {
|
|
57
|
+
ok: false;
|
|
58
|
+
reason: string;
|
|
59
|
+
};
|
|
60
|
+
//# sourceMappingURL=repository.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repository.d.ts","sourceRoot":"","sources":["../../src/secrets/repository.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,MAAM,WAAW,gBAAgB;IAC/B,gDAAgD;IAChD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,0DAA0D;IAC1D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,sFAAsF;AACtF,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAehE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE;IACvC,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9C,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IACvC,oFAAoF;IACpF,QAAQ,CAAC,qBAAqB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnD,gFAAgF;IAChF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;CACxC,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,gBAAgB,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAoDzE;AAOD,2CAA2C;AAC3C,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAS7D;AAED,2FAA2F;AAC3F,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,EAAE,CAUtG;AAED,4EAA4E;AAC5E,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAI9D;AAED,gEAAgE;AAChE,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,GAAG,SAAS,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,gBAAgB,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAUxE"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which GitHub repository `smoo secrets` reads and writes.
|
|
3
|
+
*
|
|
4
|
+
* `gh` refuses on a checkout with more than one remote ("multiple remotes
|
|
5
|
+
* detected"), which is exactly the public/private mirror layout this CLI's own
|
|
6
|
+
* users run — so the repository has to be resolved here, from the checkout,
|
|
7
|
+
* before `gh` is spawned. The current branch's upstream decides it: a branch
|
|
8
|
+
* that pushes to the private mirror is a branch whose secrets live there. That
|
|
9
|
+
* is a fact the checkout already records, not a preference to configure.
|
|
10
|
+
*/
|
|
11
|
+
import { spawnSync } from 'node:child_process';
|
|
12
|
+
/** `owner/name` from any git remote URL form, or null when it names no repository. */
|
|
13
|
+
export function repositorySlugFromUrl(url) {
|
|
14
|
+
const withoutProtocol = url
|
|
15
|
+
.trim()
|
|
16
|
+
.replace(/^[a-z+]+:\/\//i, '')
|
|
17
|
+
.replace(/^[^@/]+@/, '');
|
|
18
|
+
// A remainder starting with `/` or `.` carries no host, so it is a path on
|
|
19
|
+
// this machine - a valid git remote, never a GitHub repository.
|
|
20
|
+
if (withoutProtocol.startsWith('/') || withoutProtocol.startsWith('.'))
|
|
21
|
+
return null;
|
|
22
|
+
const path = withoutProtocol.replace(/^[^/:]+(?::\d+)?[/:]/, '');
|
|
23
|
+
const segments = path
|
|
24
|
+
.replace(/\.git$/i, '')
|
|
25
|
+
.split('/')
|
|
26
|
+
.filter((segment) => segment.length > 0);
|
|
27
|
+
if (segments.length < 2)
|
|
28
|
+
return null;
|
|
29
|
+
return `${segments[segments.length - 2]}/${segments[segments.length - 1]}`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Pure resolution, so the precedence is testable without a checkout.
|
|
33
|
+
*
|
|
34
|
+
* A requested value is either a remote name or an `owner/name` slug; naming a
|
|
35
|
+
* remote is what an operator reaches for first (`-R private-repo`), and
|
|
36
|
+
* refusing it because it lacks a slash would be pedantry.
|
|
37
|
+
*
|
|
38
|
+
* With no request and no upstream - a branch pushed by explicit refspec, which
|
|
39
|
+
* is how the mirror layout is driven - the remote-tracking refs still record
|
|
40
|
+
* where this branch has been pushed. One remote carrying it is an answer; two
|
|
41
|
+
* is a genuine ambiguity worth refusing.
|
|
42
|
+
*/
|
|
43
|
+
export function chooseRepository(inputs) {
|
|
44
|
+
const { remotes, upstreamRemote, requested } = inputs;
|
|
45
|
+
const remotesCarryingBranch = inputs.remotesCarryingBranch ?? [];
|
|
46
|
+
if (requested !== undefined && requested.length > 0) {
|
|
47
|
+
const url = remotes.get(requested);
|
|
48
|
+
if (url !== undefined) {
|
|
49
|
+
const slug = repositorySlugFromUrl(url);
|
|
50
|
+
if (slug === null)
|
|
51
|
+
return { ok: false, reason: `remote ${requested} (${url}) names no owner/name` };
|
|
52
|
+
return { ok: true, choice: { repo: slug, source: `remote ${requested}` } };
|
|
53
|
+
}
|
|
54
|
+
if (requested.includes('/'))
|
|
55
|
+
return { ok: true, choice: { repo: requested, source: 'requested' } };
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
reason: `${requested} is neither a remote of this checkout nor an owner/name. ` +
|
|
59
|
+
`Remotes: ${[...remotes.keys()].join(', ') || 'none'}`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (upstreamRemote !== null) {
|
|
63
|
+
const url = remotes.get(upstreamRemote);
|
|
64
|
+
const slug = url === undefined ? null : repositorySlugFromUrl(url);
|
|
65
|
+
if (slug !== null) {
|
|
66
|
+
return { ok: true, choice: { repo: slug, source: `upstream of the current branch (${upstreamRemote})` } };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (remotesCarryingBranch.length === 1) {
|
|
70
|
+
const name = remotesCarryingBranch[0] ?? '';
|
|
71
|
+
const url = remotes.get(name);
|
|
72
|
+
const slug = url === undefined ? null : repositorySlugFromUrl(url);
|
|
73
|
+
if (slug !== null) {
|
|
74
|
+
return { ok: true, choice: { repo: slug, source: `the only remote carrying this branch (${name})` } };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (remotes.size === 1) {
|
|
78
|
+
const [name, url] = [...remotes][0];
|
|
79
|
+
const slug = repositorySlugFromUrl(url);
|
|
80
|
+
if (slug !== null)
|
|
81
|
+
return { ok: true, choice: { repo: slug, source: `the only remote (${name})` } };
|
|
82
|
+
}
|
|
83
|
+
const origin = remotes.get('origin');
|
|
84
|
+
const originSlug = origin === undefined ? null : repositorySlugFromUrl(origin);
|
|
85
|
+
if (originSlug !== null)
|
|
86
|
+
return { ok: true, choice: { repo: originSlug, source: 'remote origin' } };
|
|
87
|
+
const candidates = [...remotes].map(([name, url]) => `${name} -> ${repositorySlugFromUrl(url) ?? url}`).join(', ');
|
|
88
|
+
const branchClause = inputs.branch === undefined || inputs.branch === null
|
|
89
|
+
? 'this checkout has no current branch'
|
|
90
|
+
: `branch ${inputs.branch} has no upstream and no remote carries it`;
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
reason: `${branchClause}, and there is no origin, so the repository is ambiguous. ` +
|
|
94
|
+
`Pass -R with one of: ${candidates || 'no remotes at all'}`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function git(root, args) {
|
|
98
|
+
const result = spawnSync('git', [...args], { cwd: root, encoding: 'utf8' });
|
|
99
|
+
return result.status === 0 ? result.stdout.trim() : null;
|
|
100
|
+
}
|
|
101
|
+
/** Remote name -> URL, in config order. */
|
|
102
|
+
export function readRemotes(root) {
|
|
103
|
+
const remotes = new Map();
|
|
104
|
+
const config = git(root, ['config', '--get-regexp', '^remote\\..*\\.url']);
|
|
105
|
+
if (config === null)
|
|
106
|
+
return remotes;
|
|
107
|
+
for (const line of config.split('\n')) {
|
|
108
|
+
const match = /^remote\.(.+)\.url\s+(.+)$/.exec(line.trim());
|
|
109
|
+
if (match !== null && match[1] !== undefined && match[2] !== undefined)
|
|
110
|
+
remotes.set(match[1], match[2]);
|
|
111
|
+
}
|
|
112
|
+
return remotes;
|
|
113
|
+
}
|
|
114
|
+
/** Remotes with a remote-tracking ref for the current branch: where it has been pushed. */
|
|
115
|
+
export function readRemotesCarryingBranch(root, remotes) {
|
|
116
|
+
const branch = git(root, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
117
|
+
if (branch === null || branch === 'HEAD')
|
|
118
|
+
return [];
|
|
119
|
+
const carrying = [];
|
|
120
|
+
for (const name of remotes.keys()) {
|
|
121
|
+
if (git(root, ['rev-parse', '--verify', '--quiet', `refs/remotes/${name}/${branch}`]) !== null) {
|
|
122
|
+
carrying.push(name);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return carrying;
|
|
126
|
+
}
|
|
127
|
+
/** The remote the current branch tracks, or null when it tracks nothing. */
|
|
128
|
+
export function readUpstreamRemote(root) {
|
|
129
|
+
const branch = git(root, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
130
|
+
if (branch === null || branch === 'HEAD')
|
|
131
|
+
return null;
|
|
132
|
+
return git(root, ['config', '--get', `branch.${branch}.remote`]);
|
|
133
|
+
}
|
|
134
|
+
/** The repository to operate on, resolved from the checkout. */
|
|
135
|
+
export function resolveRepository(root, requested) {
|
|
136
|
+
const remotes = readRemotes(root);
|
|
137
|
+
const branch = git(root, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
138
|
+
return chooseRepository({
|
|
139
|
+
remotes,
|
|
140
|
+
upstreamRemote: readUpstreamRemote(root),
|
|
141
|
+
remotesCarryingBranch: readRemotesCarryingBranch(root, remotes),
|
|
142
|
+
branch: branch === 'HEAD' ? null : branch,
|
|
143
|
+
requested,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
@@ -5,6 +5,10 @@ export interface R2Bucket {
|
|
|
5
5
|
export interface WorkerScript {
|
|
6
6
|
id: string;
|
|
7
7
|
}
|
|
8
|
+
/** One secret bound to a Worker. Cloudflare answers names and types only; a value is never readable. */
|
|
9
|
+
export interface WorkerSecret {
|
|
10
|
+
name: string;
|
|
11
|
+
}
|
|
8
12
|
export interface WorkerRoute {
|
|
9
13
|
id: string;
|
|
10
14
|
pattern: string;
|
|
@@ -40,6 +44,8 @@ export interface CloudflareClient {
|
|
|
40
44
|
deleteR2Object(bucket: string, key: string): Promise<void>;
|
|
41
45
|
deleteR2Bucket(name: string): Promise<void>;
|
|
42
46
|
listWorkerScripts(): Promise<WorkerScript[]>;
|
|
47
|
+
/** Names only; the Worker's stored values are write-only from outside. */
|
|
48
|
+
listWorkerSecrets(workerName: string): Promise<string[]>;
|
|
43
49
|
deleteWorkerScript(name: string): Promise<void>;
|
|
44
50
|
listWorkerDomains(): Promise<WorkerDomain[]>;
|
|
45
51
|
createWorkerDomain(hostname: string, workerName: string, zoneId: string): Promise<void>;
|
|
@@ -75,6 +81,7 @@ export declare class CloudflareRestClient implements CloudflareClient {
|
|
|
75
81
|
deleteR2Object(bucket: string, key: string): Promise<void>;
|
|
76
82
|
deleteR2Bucket(name: string): Promise<void>;
|
|
77
83
|
listWorkerScripts(): Promise<WorkerScript[]>;
|
|
84
|
+
listWorkerSecrets(workerName: string): Promise<string[]>;
|
|
78
85
|
deleteWorkerScript(name: string): Promise<void>;
|
|
79
86
|
listWorkerDomains(): Promise<WorkerDomain[]>;
|
|
80
87
|
createWorkerDomain(hostname: string, workerName: string, zoneId: string): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cloudflare.d.ts","sourceRoot":"","sources":["../../src/wrangler/cloudflare.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,gBAAgB,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;IAC/C,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IAC3D,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,aAAa,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;IACrC,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACjD,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,iBAAiB,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;IAC7C,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChD,iBAAiB,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;IAC7C,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxF,kBAAkB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9C,SAAS,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC;IACvC,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IACzD,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtF,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IACrD,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9E,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjE,eAAe,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAAC;IAC/C,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC1D,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/C;
|
|
1
|
+
{"version":3,"file":"cloudflare.d.ts","sourceRoot":"","sources":["../../src/wrangler/cloudflare.ts"],"names":[],"mappings":"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"}
|
|
@@ -29,6 +29,10 @@ const isWorkerScripts = (() => {
|
|
|
29
29
|
const _io0 = input => "string" === typeof input.id;
|
|
30
30
|
return input => Array.isArray(input) && input.every(elem => "object" === typeof elem && null !== elem && _io0(elem));
|
|
31
31
|
})();
|
|
32
|
+
const isWorkerSecrets = (() => {
|
|
33
|
+
const _io0 = input => "string" === typeof input.name;
|
|
34
|
+
return input => Array.isArray(input) && input.every(elem => "object" === typeof elem && null !== elem && _io0(elem));
|
|
35
|
+
})();
|
|
32
36
|
const isWorkerDomains = (() => {
|
|
33
37
|
const _io0 = input => "string" === typeof input.id && "string" === typeof input.hostname && (undefined === input.service || "string" === typeof input.service);
|
|
34
38
|
return input => Array.isArray(input) && input.every(elem => "object" === typeof elem && null !== elem && _io0(elem));
|
|
@@ -158,6 +162,12 @@ export class CloudflareRestClient {
|
|
|
158
162
|
// `workers/scripts` takes no pagination parameters: the account's scripts arrive in one answer.
|
|
159
163
|
return this.listOnce(`${this.accountPath}/workers/scripts`, isWorkerScripts);
|
|
160
164
|
}
|
|
165
|
+
async listWorkerSecrets(workerName) {
|
|
166
|
+
// Unpaginated, like the script listing itself. A Worker that does not exist answers 404 rather
|
|
167
|
+
// than an empty list, so the caller must establish that the Worker is there before asking.
|
|
168
|
+
const secrets = await this.listOnce(`${this.accountPath}/workers/scripts/${encodeURIComponent(workerName)}/secrets`, isWorkerSecrets);
|
|
169
|
+
return secrets.map((secret) => secret.name);
|
|
170
|
+
}
|
|
161
171
|
deleteWorkerScript(name) {
|
|
162
172
|
return this.mutate(`${this.accountPath}/workers/scripts/${encodeURIComponent(name)}`, { method: 'DELETE' });
|
|
163
173
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deploy-stage.d.ts","sourceRoot":"","sources":["../../src/wrangler/deploy-stage.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"deploy-stage.d.ts","sourceRoot":"","sources":["../../src/wrangler/deploy-stage.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
3
2
|
import { readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
3
|
import { dirname, join } from 'node:path';
|
|
5
4
|
import { parseJsonFileText } from '../lib/json.js';
|
|
@@ -7,8 +6,8 @@ import { mergeEnv, printCommandOutput } from '../lib/run.js';
|
|
|
7
6
|
import { CloudflareRestClient } from './cloudflare.js';
|
|
8
7
|
import { parseFlatWranglerConfig, planFlatStageResources } from './flat-config.js';
|
|
9
8
|
import { awaitLiveVersion, awaitVersionEndpoint, currentDeploymentVersionId, findVersionIdByTag, liveVersionCacheDirectory, writeCachedLiveVersion, } from './live-version.js';
|
|
10
|
-
import { parseDevVarsExample } from './prepare-env.js';
|
|
11
9
|
import { derivePullRequestStageConfig, derivePullRequestWranglerConfig, hasExactStageSegment, isPullRequestStage, parseDeploymentStage, planConfiguredStageResources, planPullRequestBindings, planPullRequestResources, pullRequestStage, } from './stage.js';
|
|
10
|
+
import { planStageSecrets, readDeclaredSecretNames, readSecretStageMap, stageSecretRefusal, } from './stage-secrets.js';
|
|
12
11
|
/** The child environment `options` describe; every runner, test doubles included, spawns with exactly this. */
|
|
13
12
|
export function childEnvironment(options) {
|
|
14
13
|
return mergeEnv(options.env, options.unsetEnv);
|
|
@@ -40,21 +39,26 @@ export async function deployStage(cwd, options, dependencies = {}) {
|
|
|
40
39
|
const cloudflare = dependencies.cloudflare ??
|
|
41
40
|
new CloudflareRestClient(accountId, requiredEnvironmentValue(apiToken, 'CLOUDFLARE_API_TOKEN'));
|
|
42
41
|
const runner = dependencies.runner ?? new BunProcessRunner();
|
|
43
|
-
const
|
|
42
|
+
const secretPlan = planStageSecrets(readDeclaredSecretNames(cwd), stage, readSecretStageMap(cwd));
|
|
43
|
+
// Only the stage's own secrets, and only the ones a value exists for. A secret scoped to other
|
|
44
|
+
// stages is dropped here even when this shell exports it: that is the whole permission rule.
|
|
44
45
|
const secretValues = {};
|
|
45
|
-
for (const name of
|
|
46
|
+
for (const name of secretPlan.required) {
|
|
46
47
|
const value = processEnv[name];
|
|
47
48
|
if (value)
|
|
48
49
|
secretValues[name] = value;
|
|
49
50
|
}
|
|
50
|
-
const
|
|
51
|
+
const gate = stageSecretGate(secretPlan, new Set(Object.keys(secretValues)), cloudflare);
|
|
51
52
|
let temporaryConfigPath;
|
|
52
53
|
let temporarySecretsPath;
|
|
53
54
|
try {
|
|
54
55
|
const prepared = options.config
|
|
55
|
-
? await prepareFlatConfig(options.config, stage, accountId,
|
|
56
|
-
: await prepareTomlConfig(cwd, stage, accountId,
|
|
56
|
+
? await prepareFlatConfig(options.config, stage, accountId, gate, cloudflare)
|
|
57
|
+
: await prepareTomlConfig(cwd, stage, accountId, gate, cloudflare);
|
|
57
58
|
temporaryConfigPath = prepared.temporaryConfigPath;
|
|
59
|
+
// The pull-request path already ran this before provisioning; the gate answers once per Worker.
|
|
60
|
+
// Everything below here writes to Cloudflare.
|
|
61
|
+
await gate(prepared.plan.workerName);
|
|
58
62
|
const workerExists = await reconcileStageResources(prepared.plan, cloudflare);
|
|
59
63
|
const versionTag = nxTaskVersionTag(processEnv);
|
|
60
64
|
const envArgs = prepared.envFlag ? ['--env', prepared.envFlag] : [];
|
|
@@ -62,7 +66,13 @@ export async function deployStage(cwd, options, dependencies = {}) {
|
|
|
62
66
|
// renames the worker `<name>-<CLOUDFLARE_ENV>`. The build that produced the flat config is the
|
|
63
67
|
// caller that sets the variable, so the deploy would silently succeed under a name neither
|
|
64
68
|
// `reconcileStageResources` nor `versions list` looks at.
|
|
65
|
-
|
|
69
|
+
//
|
|
70
|
+
// A secret this stage is scoped out of goes with it. Leaving it out of the secrets payload is
|
|
71
|
+
// what stops it being installed; withholding it from the child as well means the deploy cannot
|
|
72
|
+
// read it at all, so "this stage never sees that capability" holds at the process boundary and
|
|
73
|
+
// not merely in the file we happen to write.
|
|
74
|
+
const unsetEnv = [...(prepared.envFlag ? [] : ['CLOUDFLARE_ENV']), ...secretPlan.withheld];
|
|
75
|
+
const run = unsetEnv.length > 0 ? { cwd, unsetEnv } : { cwd };
|
|
66
76
|
const workerName = prepared.plan.workerName;
|
|
67
77
|
const probe = {
|
|
68
78
|
deployments: () => wranglerJson(runner, ['deployments', 'status', '--name', workerName, '--json'], run),
|
|
@@ -126,7 +136,7 @@ export async function deployStage(cwd, options, dependencies = {}) {
|
|
|
126
136
|
}
|
|
127
137
|
}
|
|
128
138
|
}
|
|
129
|
-
async function prepareTomlConfig(cwd, stage, accountId,
|
|
139
|
+
async function prepareTomlConfig(cwd, stage, accountId, gate, cloudflare) {
|
|
130
140
|
const committedConfigPath = join(cwd, 'wrangler.toml');
|
|
131
141
|
const committedToml = await readFile(committedConfigPath, 'utf8');
|
|
132
142
|
if (!isPullRequestStage(stage)) {
|
|
@@ -139,7 +149,7 @@ async function prepareTomlConfig(cwd, stage, accountId, missingSecrets, cloudfla
|
|
|
139
149
|
}
|
|
140
150
|
const liveNamespaces = await cloudflare.listKvNamespaces();
|
|
141
151
|
const plan = planPullRequestResources(committedToml, stage, liveNamespaces);
|
|
142
|
-
const { kvNamespaceIds, d1DatabaseIds } = await provisionPullRequestResources(plan,
|
|
152
|
+
const { kvNamespaceIds, d1DatabaseIds } = await provisionPullRequestResources(plan, gate, cloudflare, liveNamespaces);
|
|
143
153
|
const derivedToml = derivePullRequestWranglerConfig(committedToml, {
|
|
144
154
|
stage,
|
|
145
155
|
accountId,
|
|
@@ -156,7 +166,7 @@ async function prepareTomlConfig(cwd, stage, accountId, missingSecrets, cloudfla
|
|
|
156
166
|
envFlag: stage,
|
|
157
167
|
};
|
|
158
168
|
}
|
|
159
|
-
async function prepareFlatConfig(configPath, stage, accountId,
|
|
169
|
+
async function prepareFlatConfig(configPath, stage, accountId, gate, cloudflare) {
|
|
160
170
|
const flat = parseJsonFileText(configPath, await readFile(configPath, 'utf8'), parseFlatWranglerConfig);
|
|
161
171
|
if (!isPullRequestStage(stage)) {
|
|
162
172
|
return {
|
|
@@ -167,7 +177,7 @@ async function prepareFlatConfig(configPath, stage, accountId, missingSecrets, c
|
|
|
167
177
|
}
|
|
168
178
|
const liveNamespaces = await cloudflare.listKvNamespaces();
|
|
169
179
|
const plan = planPullRequestBindings(flat, stage, liveNamespaces);
|
|
170
|
-
const { kvNamespaceIds, d1DatabaseIds } = await provisionPullRequestResources(plan,
|
|
180
|
+
const { kvNamespaceIds, d1DatabaseIds } = await provisionPullRequestResources(plan, gate, cloudflare, liveNamespaces);
|
|
171
181
|
const derived = derivePullRequestStageConfig(flat, { stage, accountId, kvNamespaceIds, d1DatabaseIds });
|
|
172
182
|
// Beside the original: its main/assets/migrations paths are relative to the file.
|
|
173
183
|
const temporaryConfigPath = join(dirname(configPath), `.wrangler.smoo-${process.pid}-${randomUUID()}.json`);
|
|
@@ -199,15 +209,35 @@ function migrationBindings(config) {
|
|
|
199
209
|
.filter((database) => typeof database.migrations_dir === 'string')
|
|
200
210
|
.map((database) => database.binding);
|
|
201
211
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
212
|
+
/**
|
|
213
|
+
* One answer per Worker, reused by both call sites so the read costs one round trip.
|
|
214
|
+
*
|
|
215
|
+
* What the Worker holds comes from the account's script listing first. A Worker absent from it
|
|
216
|
+
* holds nothing — exactly the first-deployment case — and Cloudflare answers 404 rather than an
|
|
217
|
+
* empty list when asked for a missing Worker's secrets. Reading that failure as "then nothing is
|
|
218
|
+
* needed" would disarm the check at the one moment it matters most, so it is never asked.
|
|
219
|
+
*/
|
|
220
|
+
function stageSecretGate(plan, exported, cloudflare) {
|
|
221
|
+
const answered = new Map();
|
|
222
|
+
return (workerName) => {
|
|
223
|
+
let pending = answered.get(workerName);
|
|
224
|
+
if (!pending) {
|
|
225
|
+
pending = assertStageSecrets(plan, exported, cloudflare, workerName);
|
|
226
|
+
answered.set(workerName, pending);
|
|
227
|
+
}
|
|
228
|
+
return pending;
|
|
229
|
+
};
|
|
207
230
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
await
|
|
231
|
+
async function assertStageSecrets(plan, exported, cloudflare, workerName) {
|
|
232
|
+
const live = (await cloudflare.listWorkerScripts()).some((script) => script.id === workerName);
|
|
233
|
+
const held = new Set(live ? await cloudflare.listWorkerSecrets(workerName) : []);
|
|
234
|
+
const refusal = stageSecretRefusal(plan, exported, held, workerName);
|
|
235
|
+
if (refusal)
|
|
236
|
+
throw new Error(refusal);
|
|
237
|
+
}
|
|
238
|
+
/** Isolation refusals already ran in the plan; the gate below is the last one before the first write. */
|
|
239
|
+
async function provisionPullRequestResources(plan, gate, cloudflare, liveNamespaces) {
|
|
240
|
+
await gate(plan.workerName);
|
|
211
241
|
const kvNamespaceIds = await ensureKvNamespaces(plan.kvNamespaces, liveNamespaces, cloudflare);
|
|
212
242
|
const d1DatabaseIds = plan.d1Databases.length === 0
|
|
213
243
|
? new Map()
|
|
@@ -495,7 +525,3 @@ function requiredEnvironmentValue(value, name) {
|
|
|
495
525
|
throw new Error(`${name} is required.`);
|
|
496
526
|
return value;
|
|
497
527
|
}
|
|
498
|
-
function readSecretNames(cwd) {
|
|
499
|
-
const path = join(cwd, '.dev.vars.example');
|
|
500
|
-
return existsSync(path) ? parseDevVarsExample(readFileSync(path, 'utf8')) : [];
|
|
501
|
-
}
|