@devdogsuga/devtools 0.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/README.md +44 -0
- package/bin/devtools-ci-bare.mjs +48 -0
- package/bin/devtools-ci.mjs +17 -0
- package/bin/devtools.mjs +27 -0
- package/dist/args.d.ts +26 -0
- package/dist/args.js +78 -0
- package/dist/bws/bw.d.ts +9 -0
- package/dist/bws/bw.js +49 -0
- package/dist/bws/client.d.ts +43 -0
- package/dist/bws/client.js +363 -0
- package/dist/bws/environments.d.ts +96 -0
- package/dist/bws/environments.js +111 -0
- package/dist/bws/pace.d.ts +27 -0
- package/dist/bws/pace.js +43 -0
- package/dist/bws/retry.d.ts +12 -0
- package/dist/bws/retry.js +39 -0
- package/dist/bws/store.d.ts +1 -0
- package/dist/bws/store.js +32 -0
- package/dist/bws/token.d.ts +31 -0
- package/dist/bws/token.js +88 -0
- package/dist/bws/vault.d.ts +29 -0
- package/dist/bws/vault.js +200 -0
- package/dist/catalog.d.ts +55 -0
- package/dist/catalog.js +81 -0
- package/dist/cf/commands.d.ts +1 -0
- package/dist/cf/commands.js +182 -0
- package/dist/cf/local-env.d.ts +44 -0
- package/dist/cf/local-env.js +148 -0
- package/dist/ci.d.ts +1 -0
- package/dist/ci.js +319 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +1077 -0
- package/dist/commands.d.ts +255 -0
- package/dist/commands.js +1279 -0
- package/dist/completions.d.ts +4 -0
- package/dist/completions.js +110 -0
- package/dist/cron/commands.d.ts +47 -0
- package/dist/cron/commands.js +372 -0
- package/dist/cron/discovery.d.ts +55 -0
- package/dist/cron/discovery.js +85 -0
- package/dist/cron/schema.d.ts +45 -0
- package/dist/cron/schema.js +35 -0
- package/dist/db/config-push.d.ts +8 -0
- package/dist/db/config-push.js +11 -0
- package/dist/db/connection.d.ts +79 -0
- package/dist/db/connection.js +114 -0
- package/dist/db/exec.d.ts +1 -0
- package/dist/db/exec.js +11 -0
- package/dist/db/generate-migration.d.ts +1 -0
- package/dist/db/generate-migration.js +67 -0
- package/dist/db/generate-types.d.ts +1 -0
- package/dist/db/generate-types.js +4 -0
- package/dist/db/introspect.d.ts +1 -0
- package/dist/db/introspect.js +113 -0
- package/dist/db/local-env.d.ts +65 -0
- package/dist/db/local-env.js +23 -0
- package/dist/db/new-migration.d.ts +1 -0
- package/dist/db/new-migration.js +8 -0
- package/dist/db/run.d.ts +70 -0
- package/dist/db/run.js +158 -0
- package/dist/db/seed-buckets.d.ts +2 -0
- package/dist/db/seed-buckets.js +4 -0
- package/dist/db/seed-production.d.ts +8 -0
- package/dist/db/seed-production.js +73 -0
- package/dist/db/seed-roles.d.ts +7 -0
- package/dist/db/seed-roles.js +31 -0
- package/dist/db/session-refresh.d.ts +43 -0
- package/dist/db/session-refresh.js +40 -0
- package/dist/deploy/migrations.d.ts +20 -0
- package/dist/deploy/migrations.js +68 -0
- package/dist/deploy/orphans.d.ts +20 -0
- package/dist/deploy/orphans.js +177 -0
- package/dist/deploy/preflight.d.ts +53 -0
- package/dist/deploy/preflight.js +189 -0
- package/dist/deploy/report.d.ts +26 -0
- package/dist/deploy/report.js +70 -0
- package/dist/deploy/require-planner.d.ts +2 -0
- package/dist/deploy/require-planner.js +60 -0
- package/dist/deploy/require-token.d.ts +15 -0
- package/dist/deploy/require-token.js +78 -0
- package/dist/deploy/secrets-file.d.ts +17 -0
- package/dist/deploy/secrets-file.js +167 -0
- package/dist/deploy/write-env.d.ts +40 -0
- package/dist/deploy/write-env.js +393 -0
- package/dist/docs/index-pages.d.ts +36 -0
- package/dist/docs/index-pages.js +184 -0
- package/dist/doctor.d.ts +62 -0
- package/dist/doctor.js +225 -0
- package/dist/emails/commands.d.ts +15 -0
- package/dist/emails/commands.js +130 -0
- package/dist/emails/email-types.d.ts +13 -0
- package/dist/emails/email-types.js +1 -0
- package/dist/emails/fixtures.d.ts +20 -0
- package/dist/emails/fixtures.js +20 -0
- package/dist/env/audit.d.ts +206 -0
- package/dist/env/audit.js +436 -0
- package/dist/env/cloudflare.d.ts +21 -0
- package/dist/env/cloudflare.js +61 -0
- package/dist/env/commands.d.ts +99 -0
- package/dist/env/commands.js +612 -0
- package/dist/env/discovery.d.ts +15 -0
- package/dist/env/discovery.js +226 -0
- package/dist/env/document.d.ts +119 -0
- package/dist/env/document.js +323 -0
- package/dist/env/example.d.ts +68 -0
- package/dist/env/example.js +603 -0
- package/dist/env/selection.d.ts +105 -0
- package/dist/env/selection.js +226 -0
- package/dist/environment.d.ts +92 -0
- package/dist/environment.js +206 -0
- package/dist/fingerprint.d.ts +10 -0
- package/dist/fingerprint.js +18 -0
- package/dist/gen/campus-map.d.ts +1 -0
- package/dist/gen/campus-map.js +473 -0
- package/dist/gen/commands.d.ts +1 -0
- package/dist/gen/commands.js +25 -0
- package/dist/gen/hypno.d.ts +1 -0
- package/dist/gen/hypno.js +134 -0
- package/dist/gh/client.d.ts +80 -0
- package/dist/gh/client.js +258 -0
- package/dist/gh/environments.d.ts +83 -0
- package/dist/gh/environments.js +178 -0
- package/dist/gh/rulesets/actors.d.ts +14 -0
- package/dist/gh/rulesets/actors.js +68 -0
- package/dist/gh/rulesets/api.d.ts +15 -0
- package/dist/gh/rulesets/api.js +99 -0
- package/dist/gh/rulesets/commands.d.ts +3 -0
- package/dist/gh/rulesets/commands.js +203 -0
- package/dist/gh/rulesets/desired.d.ts +112 -0
- package/dist/gh/rulesets/desired.js +247 -0
- package/dist/gh/rulesets/diff.d.ts +65 -0
- package/dist/gh/rulesets/diff.js +134 -0
- package/dist/gh/rulesets/types.d.ts +76 -0
- package/dist/gh/rulesets/types.js +13 -0
- package/dist/gh/settings/api.d.ts +40 -0
- package/dist/gh/settings/api.js +172 -0
- package/dist/gh/settings/commands.d.ts +1 -0
- package/dist/gh/settings/commands.js +245 -0
- package/dist/gh/settings/desired.d.ts +53 -0
- package/dist/gh/settings/desired.js +39 -0
- package/dist/gh/settings/diff.d.ts +57 -0
- package/dist/gh/settings/diff.js +156 -0
- package/dist/gh/settings/types.d.ts +92 -0
- package/dist/gh/settings/types.js +11 -0
- package/dist/gh/settings/workflows.d.ts +41 -0
- package/dist/gh/settings/workflows.js +124 -0
- package/dist/grantRoot.d.ts +42 -0
- package/dist/grantRoot.js +89 -0
- package/dist/help.d.ts +18 -0
- package/dist/help.js +169 -0
- package/dist/images/commands.d.ts +35 -0
- package/dist/images/commands.js +233 -0
- package/dist/images/events.d.ts +36 -0
- package/dist/images/events.js +152 -0
- package/dist/images/graphics.d.ts +70 -0
- package/dist/images/graphics.js +233 -0
- package/dist/images/open-graph-types.d.ts +75 -0
- package/dist/images/open-graph-types.js +1 -0
- package/dist/images/prompts.d.ts +34 -0
- package/dist/images/prompts.js +100 -0
- package/dist/images/render.d.ts +21 -0
- package/dist/images/render.js +57 -0
- package/dist/images/select.d.ts +64 -0
- package/dist/images/select.js +96 -0
- package/dist/instance.d.ts +67 -0
- package/dist/instance.js +129 -0
- package/dist/invocation.d.ts +49 -0
- package/dist/invocation.js +78 -0
- package/dist/launch-ci.d.ts +1 -0
- package/dist/launch-ci.js +86 -0
- package/dist/launch.d.ts +29 -0
- package/dist/launch.js +371 -0
- package/dist/menu.d.ts +31 -0
- package/dist/menu.js +277 -0
- package/dist/newsletter/commands.d.ts +16 -0
- package/dist/newsletter/commands.js +311 -0
- package/dist/newsletter/imap.d.ts +17 -0
- package/dist/newsletter/imap.js +127 -0
- package/dist/newsletter/loopback.d.ts +24 -0
- package/dist/newsletter/loopback.js +100 -0
- package/dist/newsletter/oauth.d.ts +26 -0
- package/dist/newsletter/oauth.js +130 -0
- package/dist/newsletter/smtp.d.ts +23 -0
- package/dist/newsletter/smtp.js +133 -0
- package/dist/oauth/browser.d.ts +1 -0
- package/dist/oauth/browser.js +13 -0
- package/dist/oauth/config.d.ts +24 -0
- package/dist/oauth/config.js +24 -0
- package/dist/oauth/connect-url.d.ts +17 -0
- package/dist/oauth/connect-url.js +23 -0
- package/dist/oauth/db.d.ts +37 -0
- package/dist/oauth/db.js +108 -0
- package/dist/oauth/device.d.ts +93 -0
- package/dist/oauth/device.js +190 -0
- package/dist/oauth/discovery.d.ts +28 -0
- package/dist/oauth/discovery.js +54 -0
- package/dist/oauth/env-file.d.ts +6 -0
- package/dist/oauth/env-file.js +35 -0
- package/dist/oauth/exchange.d.ts +43 -0
- package/dist/oauth/exchange.js +87 -0
- package/dist/oauth/label.d.ts +1 -0
- package/dist/oauth/label.js +25 -0
- package/dist/oauth/loopback.d.ts +52 -0
- package/dist/oauth/loopback.js +162 -0
- package/dist/oauth/pkce.d.ts +6 -0
- package/dist/oauth/pkce.js +34 -0
- package/dist/oauth/target.d.ts +54 -0
- package/dist/oauth/target.js +102 -0
- package/dist/oauth/transport.d.ts +41 -0
- package/dist/oauth/transport.js +32 -0
- package/dist/oauth/wizard.d.ts +33 -0
- package/dist/oauth/wizard.js +571 -0
- package/dist/pick.d.ts +6 -0
- package/dist/pick.js +64 -0
- package/dist/planner/checks.d.ts +26 -0
- package/dist/planner/checks.js +104 -0
- package/dist/planner/commands.d.ts +10 -0
- package/dist/planner/commands.js +367 -0
- package/dist/planner/db.d.ts +15 -0
- package/dist/planner/db.js +33 -0
- package/dist/planner/role.d.ts +33 -0
- package/dist/planner/role.js +68 -0
- package/dist/repo/peers.d.ts +24 -0
- package/dist/repo/peers.js +114 -0
- package/dist/repo/preflight.d.ts +109 -0
- package/dist/repo/preflight.js +266 -0
- package/dist/repo/resolve.d.ts +41 -0
- package/dist/repo/resolve.js +152 -0
- package/dist/repo/root.d.ts +23 -0
- package/dist/repo/root.js +91 -0
- package/dist/repo/source.d.ts +14 -0
- package/dist/repo/source.js +54 -0
- package/dist/repo/tsx-loader.d.ts +6 -0
- package/dist/repo/tsx-loader.js +41 -0
- package/dist/run/pick.d.ts +60 -0
- package/dist/run/pick.js +559 -0
- package/dist/setup.d.ts +1 -0
- package/dist/setup.js +152 -0
- package/dist/stack.d.ts +48 -0
- package/dist/stack.js +290 -0
- package/dist/telemetry.d.ts +34 -0
- package/dist/telemetry.js +93 -0
- package/dist/tier.d.ts +40 -0
- package/dist/tier.js +93 -0
- package/dist/tsconfig.typecheck.tsbuildinfo +1 -0
- package/dist/ui.d.ts +25 -0
- package/dist/ui.js +51 -0
- package/dist/workers.d.ts +5 -0
- package/dist/workers.js +35 -0
- package/dist/workflows/commands.d.ts +44 -0
- package/dist/workflows/commands.js +668 -0
- package/env.ts +158 -0
- package/package.json +80 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `devtools deploy orphans [--prune]`
|
|
3
|
+
*
|
|
4
|
+
* Reports, and only when asked deletes, Worker secrets nothing declares.
|
|
5
|
+
*
|
|
6
|
+
* ## Why there is anything to audit
|
|
7
|
+
*
|
|
8
|
+
* `wrangler deploy --secrets-file` applies additively: it preserves every
|
|
9
|
+
* secret it does not mention. So renaming a variable, or dropping one, leaves
|
|
10
|
+
* the old secret on the Worker indefinitely, and nothing else in the system can
|
|
11
|
+
* see it. Bitwarden does not know about it, GitHub does not, and the app's
|
|
12
|
+
* schema stopped mentioning it. Cloudflare will hold a credential nobody has
|
|
13
|
+
* thought about for a year.
|
|
14
|
+
*
|
|
15
|
+
* The expected set is derived from the app's own manifest: every key it
|
|
16
|
+
* declares with `secrecy: "secret"`. That deliberately includes the MINTED
|
|
17
|
+
* ones. `SANDBOX_PROXY_TOKEN` has no copy in Bitwarden or GitHub by design, and
|
|
18
|
+
* an audit that reasoned from stored copies alone would report the live proxy
|
|
19
|
+
* credential as safe to delete. (`env audit` had exactly that bug; see
|
|
20
|
+
* `EnvMeta.minted`.)
|
|
21
|
+
*
|
|
22
|
+
* ## Report always, prune almost never (security plan §3.6)
|
|
23
|
+
*
|
|
24
|
+
* report every production deploy environment: production
|
|
25
|
+
* prune `workflow_dispatch` only environment: production-apply
|
|
26
|
+
*
|
|
27
|
+
* **Orphans never fail the deploy.** A stale secret name is not a defect in the
|
|
28
|
+
* change being deployed, and failing on it would make an unrelated commit
|
|
29
|
+
* responsible for a cleanup somebody else deferred. So the reporting path
|
|
30
|
+
* returns normally even when it found something; the only non-zero exit here is
|
|
31
|
+
* a deletion that was asked for and did not happen.
|
|
32
|
+
*
|
|
33
|
+
* **Pruning must not chain to a deploy.** Deleting a secret publishes a new
|
|
34
|
+
* version of the currently deployed code with the secret gone. There is no way
|
|
35
|
+
* to change a secret without publishing. Auto-pruning would turn every deploy
|
|
36
|
+
* carrying an orphan into TWO version publishes, and the second one bypasses
|
|
37
|
+
* both the promotion PR and the `production-apply` approval that the first went
|
|
38
|
+
* through. That is why this is a separate, human-triggered run, and why
|
|
39
|
+
* `--prune` is the only thing that can reach `deleteSecret`: a default that
|
|
40
|
+
* deleted would be a default that deployed.
|
|
41
|
+
*
|
|
42
|
+
* > **Names only.** Cloudflare never returns a secret's value, so a secret
|
|
43
|
+
* > whose VALUE is wrong is undetectable here. A clean audit means "no name is
|
|
44
|
+
* > unaccounted for", not "the configuration is correct".
|
|
45
|
+
*
|
|
46
|
+
* Recovery from a wrong prune is real but not instant: Bitwarden is the source
|
|
47
|
+
* of truth and every deploy sends the complete set, so a wrongly deleted secret
|
|
48
|
+
* comes back on the next deploy, with an outage in between.
|
|
49
|
+
*
|
|
50
|
+
* ## Interface
|
|
51
|
+
*
|
|
52
|
+
* Reads secret NAMES from Cloudflare and needs no env file, so it runs through
|
|
53
|
+
* the `cli:no-env` seam like `write-env` does. There is no `.env.production` in
|
|
54
|
+
* the job that audits, and demanding one would be demanding the whole
|
|
55
|
+
* credential set to list some names:
|
|
56
|
+
*
|
|
57
|
+
* DEPLOY_ENV=production \
|
|
58
|
+
* pnpm --filter @devdogsuga/devtools run cli:no-env deploy orphans [--prune]
|
|
59
|
+
*/
|
|
60
|
+
import { execFileSync } from "node:child_process";
|
|
61
|
+
import { join } from "node:path";
|
|
62
|
+
import { assertRegistryLoaded } from "../env/discovery.js";
|
|
63
|
+
import { listWorkerSecrets, workerApps } from "../env/cloudflare.js";
|
|
64
|
+
import { getEnvSync } from "../repo/peers.js";
|
|
65
|
+
import { findRepoRoot } from "../repo/root.js";
|
|
66
|
+
import { DeployError, say, summary } from "./report.js";
|
|
67
|
+
/**
|
|
68
|
+
* Deletes one secret through wrangler.
|
|
69
|
+
*
|
|
70
|
+
* Wrangler prompts before deleting and takes the default when stdin is not a
|
|
71
|
+
* TTY, which on a runner it is not. `stdio: "inherit"` keeps its own output on
|
|
72
|
+
* the job log where a reviewer approving this can read it.
|
|
73
|
+
*/
|
|
74
|
+
function deleteViaWrangler(root, app, key, environment) {
|
|
75
|
+
execFileSync("pnpm",
|
|
76
|
+
// WRANGLER's `--env` is a wrangler.jsonc environment block. Unrelated to
|
|
77
|
+
// devtools' `--target`, and not renamed with it.
|
|
78
|
+
["exec", "wrangler", "secret", "delete", key, "--env", environment], { cwd: join(root, "apps", app), stdio: "inherit", shell: false });
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Names the app's manifest declares for its Worker: secrets (stored or minted)
|
|
82
|
+
* AND public server keys. `secrets-file` ships both since 2026-08-20, so a
|
|
83
|
+
* public key on the Worker is expected, not an orphan.
|
|
84
|
+
*/
|
|
85
|
+
function expectedFor(app) {
|
|
86
|
+
const keys = new Set();
|
|
87
|
+
for (const [key, entries] of getEnvSync().variables()) {
|
|
88
|
+
const own = entries.filter((e) => e.source === app && !e.client);
|
|
89
|
+
if (own.length === 0)
|
|
90
|
+
continue;
|
|
91
|
+
if (own.some((e) => e.meta.secrecy === "secret" || e.meta.secrecy === "public")) {
|
|
92
|
+
keys.add(key);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return keys;
|
|
96
|
+
}
|
|
97
|
+
export async function runDeployOrphans(options) {
|
|
98
|
+
assertRegistryLoaded();
|
|
99
|
+
const env = options.env ?? process.env;
|
|
100
|
+
const root = options.root ?? findRepoRoot();
|
|
101
|
+
const listSecrets = options.listSecrets ?? listWorkerSecrets;
|
|
102
|
+
const deleteSecret = options.deleteSecret ??
|
|
103
|
+
((app, key, environment) => deleteViaWrangler(root, app, key, environment));
|
|
104
|
+
const environment = getEnvSync().resolveEnvironment(env.DEPLOY_ENV);
|
|
105
|
+
if (environment === "development") {
|
|
106
|
+
throw new DeployError("DEPLOY_ENV must name a deployed environment (staging | production).");
|
|
107
|
+
}
|
|
108
|
+
// Both the job summary and the log, because a run of this IS its report and
|
|
109
|
+
// the two audiences read different places.
|
|
110
|
+
const emit = (lines) => {
|
|
111
|
+
summary(lines, env);
|
|
112
|
+
say(lines);
|
|
113
|
+
};
|
|
114
|
+
const { secrets, unreadable } = await listSecrets(environment);
|
|
115
|
+
const lines = [
|
|
116
|
+
`### Worker secret audit — \`${environment}\``,
|
|
117
|
+
"",
|
|
118
|
+
"Names only: Cloudflare never returns a value, so this cannot see a secret",
|
|
119
|
+
"that is present and wrong.",
|
|
120
|
+
"",
|
|
121
|
+
];
|
|
122
|
+
let total = 0;
|
|
123
|
+
const toPrune = [];
|
|
124
|
+
for (const app of workerApps()) {
|
|
125
|
+
const worker = `${environment}-${app}`;
|
|
126
|
+
const found = secrets.get(worker);
|
|
127
|
+
if (!found)
|
|
128
|
+
continue;
|
|
129
|
+
const expected = expectedFor(app);
|
|
130
|
+
const orphans = [...found].filter((name) => !expected.has(name)).sort();
|
|
131
|
+
total += orphans.length;
|
|
132
|
+
lines.push(orphans.length === 0
|
|
133
|
+
? `* \`${worker}\` — ${found.size} secret(s), nothing unaccounted for.`
|
|
134
|
+
: `* \`${worker}\` — **${orphans.length} orphaned**: ${orphans.map((o) => `\`${o}\``).join(", ")}`);
|
|
135
|
+
for (const key of orphans)
|
|
136
|
+
toPrune.push({ app, worker, key });
|
|
137
|
+
}
|
|
138
|
+
if (unreadable.length > 0) {
|
|
139
|
+
lines.push("", `Could not read: ${unreadable.map((w) => `\`${w}\``).join(", ")}. A Worker`, "that has never been deployed is the ordinary reason, and the audit still", "reports everything it could see — an audit that refuses to run is an", "audit nobody runs.");
|
|
140
|
+
}
|
|
141
|
+
if (total === 0) {
|
|
142
|
+
lines.push("", "No orphaned secrets.");
|
|
143
|
+
emit([...lines, ""]);
|
|
144
|
+
return { orphans: 0, pruned: [] };
|
|
145
|
+
}
|
|
146
|
+
if (!options.prune) {
|
|
147
|
+
lines.push("", "Nothing was deleted, and this does not fail the deploy — a stale name is", "not a defect in the change that was just shipped. To remove them, run", "this workflow by hand with **Prune orphaned Worker secrets** ticked; it", "goes through the `production-apply` reviewers, because each deletion", "publishes a new version of the deployed code.", "");
|
|
148
|
+
emit(lines);
|
|
149
|
+
return { orphans: total, pruned: [] };
|
|
150
|
+
}
|
|
151
|
+
lines.push("", "Pruning:");
|
|
152
|
+
const pruned = [];
|
|
153
|
+
for (const { app, worker, key } of toPrune) {
|
|
154
|
+
try {
|
|
155
|
+
deleteSecret(app, key, environment);
|
|
156
|
+
pruned.push(`${worker}/${key}`);
|
|
157
|
+
lines.push(`* deleted \`${key}\` from \`${worker}\``);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// Fatal HERE and only here: this run exists to delete these, so a failure
|
|
161
|
+
// is the whole result rather than a footnote to a deploy. The report goes
|
|
162
|
+
// out before the throw, so the partial result survives the failure.
|
|
163
|
+
lines.push(`* **failed** to delete \`${key}\` from \`${worker}\``);
|
|
164
|
+
emit([...lines, ""]);
|
|
165
|
+
throw new DeployError(`Could not delete ${key} from ${worker}. Wrangler's own message is above.`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
emit([
|
|
169
|
+
...lines,
|
|
170
|
+
"",
|
|
171
|
+
"Each deletion published a new version of the code already deployed. If one",
|
|
172
|
+
"of these was live rather than stale, the next deploy restores it from",
|
|
173
|
+
"Bitwarden — with an outage in between.",
|
|
174
|
+
"",
|
|
175
|
+
]);
|
|
176
|
+
return { orphans: total, pruned };
|
|
177
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** Enough to answer, short enough that a hung gateway is not a stuck job. */
|
|
2
|
+
export declare const TIMEOUT_MS = 15000;
|
|
3
|
+
/**
|
|
4
|
+
* Transport failures get a retry; HTTP statuses do not.
|
|
5
|
+
*
|
|
6
|
+
* A status, any status, is the gateway ANSWERING, and classifying that answer
|
|
7
|
+
* is this command's whole job; retrying would just ask the same question
|
|
8
|
+
* again. A rejected fetch is the absence of an answer, and a DNS blip on a
|
|
9
|
+
* shared runner turning into a red staging deploy is the noise §3.2 exists to
|
|
10
|
+
* avoid.
|
|
11
|
+
*/
|
|
12
|
+
export declare const TRANSPORT_ATTEMPTS = 3;
|
|
13
|
+
/** Supabase's own gateway code for a paused project. Not in any HTTP registry. */
|
|
14
|
+
export declare const PAUSED_STATUS = 540;
|
|
15
|
+
/**
|
|
16
|
+
* Everything that touches the outside world, injected.
|
|
17
|
+
*
|
|
18
|
+
* The 540-vs-everything-else decision must not rot, and it is unreachable in a
|
|
19
|
+
* test that has to open a socket or find a real paused project. So the fetch,
|
|
20
|
+
* the environment and the reporter are all parameters, and `preflight.test.ts`
|
|
21
|
+
* drives every branch without a socket.
|
|
22
|
+
*
|
|
23
|
+
* The two FILE writes are deliberately not injected: `$GITHUB_OUTPUT` and
|
|
24
|
+
* `$GITHUB_STEP_SUMMARY` are paths the environment already supplies, so a test
|
|
25
|
+
* points them at a temp file and asserts the real bytes. A stub there would
|
|
26
|
+
* assert the call and not the contract.
|
|
27
|
+
*/
|
|
28
|
+
export interface PreflightDeps {
|
|
29
|
+
env?: Record<string, string | undefined>;
|
|
30
|
+
fetch?: typeof globalThis.fetch;
|
|
31
|
+
/** Where the human-readable lines go. Defaults to `say()`, so stderr. */
|
|
32
|
+
report?: (lines: readonly string[]) => void;
|
|
33
|
+
}
|
|
34
|
+
export interface PreflightVerdict {
|
|
35
|
+
/**
|
|
36
|
+
* `true` for a paused project, `false` for a healthy one. Both exit 0; the
|
|
37
|
+
* workflow reads `paused` from `$GITHUB_OUTPUT` to decide whether to deploy.
|
|
38
|
+
*/
|
|
39
|
+
paused: boolean;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Classifies the project.
|
|
43
|
+
*
|
|
44
|
+
* Returning at all means exit 0, healthy or paused. Anything else throws a
|
|
45
|
+
* `DeployError`, which `cli.ts` renders to stderr and turns into exit 1. That
|
|
46
|
+
* mapping IS the contract the deploy job gates on, so the two must not drift:
|
|
47
|
+
* a refusal that returned, or a paused project that threw, would each invert a
|
|
48
|
+
* deploy decision.
|
|
49
|
+
*
|
|
50
|
+
* @throws {DeployError} for a broken project, an unreachable one, or a
|
|
51
|
+
* missing `PROJECT_REF` / `PUBLISHABLE_KEY`.
|
|
52
|
+
*/
|
|
53
|
+
export declare function runPreflight(deps?: PreflightDeps): Promise<PreflightVerdict>;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pnpm devtools deploy preflight`
|
|
3
|
+
*
|
|
4
|
+
* Classifies a Supabase project as healthy, paused, or broken before a deploy
|
|
5
|
+
* spends any credential on it.
|
|
6
|
+
*
|
|
7
|
+
* ## Why this exists (security plan §3.2)
|
|
8
|
+
*
|
|
9
|
+
* Staging's Supabase project is on the free tier, so it pauses after a week
|
|
10
|
+
* without activity, and staging deploys land roughly weekly. A paused project
|
|
11
|
+
* is the expected state, not an exception. Unhandled, it does not announce
|
|
12
|
+
* itself: the deploy proceeds until the migrations step opens a Postgres
|
|
13
|
+
* connection and dies with a connection error that reads exactly like a broken
|
|
14
|
+
* migration.
|
|
15
|
+
*
|
|
16
|
+
* Supabase publishes a status code for this case: HTTP 540, "the project the
|
|
17
|
+
* request was being made against has been paused". It comes from the API
|
|
18
|
+
* gateway rather than from Postgres, so one request classifies the project
|
|
19
|
+
* before anything authenticates to it.
|
|
20
|
+
*
|
|
21
|
+
* 540 paused skip the deploy, annotate, exit 0
|
|
22
|
+
* 200 healthy proceed
|
|
23
|
+
* anything broken fail loudly
|
|
24
|
+
*
|
|
25
|
+
* ## Skip rather than fail, and ONLY on 540
|
|
26
|
+
*
|
|
27
|
+
* A staging deploy runs after the merge to `main` has already landed, so
|
|
28
|
+
* failing it blocks nothing and only paints CI red. If that happens most weeks
|
|
29
|
+
* then red stops meaning anything, which costs more than a stale staging
|
|
30
|
+
* environment. Every other outcome stays a hard failure, because those are the
|
|
31
|
+
* ones a person has to look at.
|
|
32
|
+
*
|
|
33
|
+
* Resuming is a dashboard action on purpose: there is no documented Management
|
|
34
|
+
* API restore, and a Management API token carries full account privileges
|
|
35
|
+
* across BOTH Supabase organizations (§1.3), which is what the staging tier
|
|
36
|
+
* must not hold. So the skip writes the dashboard link and stops there.
|
|
37
|
+
*
|
|
38
|
+
* > Rejected: a keep-alive cron on the staging Worker. Supabase documents that
|
|
39
|
+
* > a few requests a day prevent pausing, but that only holds while staging is
|
|
40
|
+
* > deployed and healthy, so the first broken deploy silently starts a one-week
|
|
41
|
+
* > timer to a paused project.
|
|
42
|
+
*
|
|
43
|
+
* ⚠️ Delete this command when staging moves to a paid project. Paid projects
|
|
44
|
+
* cannot be paused, and this becomes dead code.
|
|
45
|
+
*
|
|
46
|
+
* ## ⚠️ The exit code is the interface, and it gates a deploy job
|
|
47
|
+
*
|
|
48
|
+
* Exit 0 for healthy OR paused; 1 for anything else. The workflow reads
|
|
49
|
+
* `paused` from `$GITHUB_OUTPUT` to decide whether to run the deploy, and
|
|
50
|
+
* reads the exit code to decide whether the preflight itself failed, so
|
|
51
|
+
* "paused" and "broken" must never collapse into one another. Only 540 skips.
|
|
52
|
+
*
|
|
53
|
+
* ## Everything human goes to stderr
|
|
54
|
+
*
|
|
55
|
+
* Like every command under `deploy/`, this one reports through `say()` rather
|
|
56
|
+
* than `@clack/prompts`, which writes exclusively to stdout (see
|
|
57
|
+
* `deploy/report.ts`). Second reason: it runs in GitHub Actions, which merges
|
|
58
|
+
* stdout and stderr into one log with no guaranteed ordering between them, so
|
|
59
|
+
* a verdict on one stream and its hints on the other can surface out of order.
|
|
60
|
+
*
|
|
61
|
+
* ## Interface
|
|
62
|
+
*
|
|
63
|
+
* PROJECT_REF=… PUBLISHABLE_KEY=… pnpm devtools deploy preflight
|
|
64
|
+
*
|
|
65
|
+
* Writes `paused=true|false` to `$GITHUB_OUTPUT` and, when paused, a note to
|
|
66
|
+
* `$GITHUB_STEP_SUMMARY`. It needs no env FILE and no credential. Both
|
|
67
|
+
* variables are public (§A.4), so it runs through `cli:no-env`.
|
|
68
|
+
*/
|
|
69
|
+
import { appendFileSync } from "node:fs";
|
|
70
|
+
import { DeployError, say, summary } from "./report.js";
|
|
71
|
+
/** Enough to answer, short enough that a hung gateway is not a stuck job. */
|
|
72
|
+
export const TIMEOUT_MS = 15_000;
|
|
73
|
+
/**
|
|
74
|
+
* Transport failures get a retry; HTTP statuses do not.
|
|
75
|
+
*
|
|
76
|
+
* A status, any status, is the gateway ANSWERING, and classifying that answer
|
|
77
|
+
* is this command's whole job; retrying would just ask the same question
|
|
78
|
+
* again. A rejected fetch is the absence of an answer, and a DNS blip on a
|
|
79
|
+
* shared runner turning into a red staging deploy is the noise §3.2 exists to
|
|
80
|
+
* avoid.
|
|
81
|
+
*/
|
|
82
|
+
export const TRANSPORT_ATTEMPTS = 3;
|
|
83
|
+
/** Supabase's own gateway code for a paused project. Not in any HTTP registry. */
|
|
84
|
+
export const PAUSED_STATUS = 540;
|
|
85
|
+
/**
|
|
86
|
+
* Classifies the project.
|
|
87
|
+
*
|
|
88
|
+
* Returning at all means exit 0, healthy or paused. Anything else throws a
|
|
89
|
+
* `DeployError`, which `cli.ts` renders to stderr and turns into exit 1. That
|
|
90
|
+
* mapping IS the contract the deploy job gates on, so the two must not drift:
|
|
91
|
+
* a refusal that returned, or a paused project that threw, would each invert a
|
|
92
|
+
* deploy decision.
|
|
93
|
+
*
|
|
94
|
+
* @throws {DeployError} for a broken project, an unreachable one, or a
|
|
95
|
+
* missing `PROJECT_REF` / `PUBLISHABLE_KEY`.
|
|
96
|
+
*/
|
|
97
|
+
export async function runPreflight(deps = {}) {
|
|
98
|
+
const { env = process.env, fetch = globalThis.fetch, report = say } = deps;
|
|
99
|
+
/** A machine-readable verdict, echoed to the log so a human sees it too. */
|
|
100
|
+
const output = (text) => {
|
|
101
|
+
const path = env.GITHUB_OUTPUT;
|
|
102
|
+
if (path)
|
|
103
|
+
appendFileSync(path, `${text}\n`);
|
|
104
|
+
report([`preflight: ${text}`]);
|
|
105
|
+
};
|
|
106
|
+
const ref = env.PROJECT_REF;
|
|
107
|
+
const key = env.PUBLISHABLE_KEY;
|
|
108
|
+
if (!ref) {
|
|
109
|
+
throw new DeployError("PROJECT_REF is not set — nothing to classify.", [
|
|
110
|
+
"It is a per-environment GitHub VARIABLE (not a secret); see §A.4.",
|
|
111
|
+
]);
|
|
112
|
+
}
|
|
113
|
+
// Unauthenticated, PostgREST answers 401 and a 401 is indistinguishable from
|
|
114
|
+
// the "broken" bucket. The publishable key is what makes 200 mean 200; it
|
|
115
|
+
// discloses nothing, which is why this check can run before any real
|
|
116
|
+
// credential is in scope.
|
|
117
|
+
if (!key) {
|
|
118
|
+
throw new DeployError("PUBLISHABLE_KEY is not set — a healthy project would look broken.", [
|
|
119
|
+
"Without it PostgREST answers 401, which this command cannot tell apart",
|
|
120
|
+
"from a genuinely broken project. It is a GitHub VARIABLE, not a secret.",
|
|
121
|
+
]);
|
|
122
|
+
}
|
|
123
|
+
// Auth's health endpoint, NOT `/rest/v1/`. The REST root is PostgREST's
|
|
124
|
+
// OpenAPI schema document, and Supabase now answers it with 401 "Secret API
|
|
125
|
+
// key required" for every non-secret key, publishable and legacy anon alike
|
|
126
|
+
// (observed 2026-08-20; it failed the first real staging preflight). Health
|
|
127
|
+
// keeps the same verdict table: 200 only with the right publishable key,
|
|
128
|
+
// 401 for a wrong one, 540 from the gateway when paused.
|
|
129
|
+
const url = `https://${ref}.supabase.co/auth/v1/health`;
|
|
130
|
+
let response;
|
|
131
|
+
let lastTransportError;
|
|
132
|
+
for (let attempt = 1; attempt <= TRANSPORT_ATTEMPTS; attempt += 1) {
|
|
133
|
+
try {
|
|
134
|
+
response = await fetch(url, {
|
|
135
|
+
headers: { apikey: key },
|
|
136
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
137
|
+
});
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
lastTransportError = error;
|
|
142
|
+
report([
|
|
143
|
+
`preflight: attempt ${attempt}/${TRANSPORT_ATTEMPTS} did not reach ${ref}: ${String(error)}`,
|
|
144
|
+
]);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (!response) {
|
|
148
|
+
throw new DeployError(`could not reach ${url} — ${String(lastTransportError)}`, [
|
|
149
|
+
"This is not a verdict about the project: nothing answered at all.",
|
|
150
|
+
"Re-run the job; if it keeps happening, the DNS name or the ref is wrong.",
|
|
151
|
+
]);
|
|
152
|
+
}
|
|
153
|
+
if (response.status === PAUSED_STATUS) {
|
|
154
|
+
output("paused=true");
|
|
155
|
+
const note = [
|
|
156
|
+
"### Staging is paused — deploy skipped",
|
|
157
|
+
"",
|
|
158
|
+
`Supabase project \`${ref}\` answered **HTTP 540**: the project has been`,
|
|
159
|
+
"paused. Free-tier projects pause after a week without activity, and",
|
|
160
|
+
"staging deploys roughly weekly, so this is the expected state rather than",
|
|
161
|
+
"a fault. Nothing was deployed and nothing is broken.",
|
|
162
|
+
"",
|
|
163
|
+
`**Resume it:** <https://supabase.com/dashboard/project/${ref}>`,
|
|
164
|
+
"",
|
|
165
|
+
"Resuming is a dashboard action on purpose: there is no documented",
|
|
166
|
+
"Management API restore, and a Management API token would carry full",
|
|
167
|
+
"account privileges across both Supabase organizations — which is exactly",
|
|
168
|
+
"what the staging tier must not hold. Re-run this workflow once the",
|
|
169
|
+
"project is up.",
|
|
170
|
+
"",
|
|
171
|
+
];
|
|
172
|
+
summary(note, env);
|
|
173
|
+
// Echoed to the log as well as the summary tab: a paused staging deploy is
|
|
174
|
+
// a green job that did nothing, and the log is where somebody looks first.
|
|
175
|
+
report(note);
|
|
176
|
+
return { paused: true };
|
|
177
|
+
}
|
|
178
|
+
if (response.status === 200) {
|
|
179
|
+
output("paused=false");
|
|
180
|
+
report([`preflight: ${ref} is up.`]);
|
|
181
|
+
return { paused: false };
|
|
182
|
+
}
|
|
183
|
+
throw new DeployError(`${url} answered ${response.status} ${response.statusText}, which is neither ` +
|
|
184
|
+
"healthy (200) nor paused (540).", [
|
|
185
|
+
"Only 540 is skippable. Everything else is a real fault and stays red:",
|
|
186
|
+
"401 means the publishable key is wrong for this project, 404 means the",
|
|
187
|
+
"ref is, and a 5xx means Supabase is having a bad day.",
|
|
188
|
+
]);
|
|
189
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A deploy command's own refusal, as opposed to a crash.
|
|
3
|
+
*
|
|
4
|
+
* The distinction lets `cli.ts` print a message a person can act on instead of
|
|
5
|
+
* a stack trace, and it is worth having even though both outcomes exit 1:
|
|
6
|
+
* these commands run unattended, and the log line IS the report.
|
|
7
|
+
*/
|
|
8
|
+
export declare class DeployError extends Error {
|
|
9
|
+
readonly detail: readonly string[];
|
|
10
|
+
constructor(message: string, detail?: readonly string[]);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Prints to STDERR. The only way a deploy command should say anything.
|
|
14
|
+
*
|
|
15
|
+
* See the header for why this is not `log.message`: stdout is machine-read
|
|
16
|
+
* here, and clack writes there.
|
|
17
|
+
*/
|
|
18
|
+
export declare function say(lines: readonly string[]): void;
|
|
19
|
+
/**
|
|
20
|
+
* Appends to the job summary under Actions; a no-op anywhere else.
|
|
21
|
+
*
|
|
22
|
+
* `env` is a parameter rather than a read of `process.env` so a test can point
|
|
23
|
+
* it at a temporary file without mutating the ambient environment of whatever
|
|
24
|
+
* else the runner is executing in the same process.
|
|
25
|
+
*/
|
|
26
|
+
export declare function summary(lines: readonly string[], env?: NodeJS.ProcessEnv): void;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a `devtools deploy` command may print, and where it may print it.
|
|
3
|
+
*
|
|
4
|
+
* ## ⚠️ stdout belongs to the machine
|
|
5
|
+
*
|
|
6
|
+
* Every other command in this CLI talks through `@clack/prompts`, and every
|
|
7
|
+
* one of its writers goes to **stdout**: `intro`, `outro`, `log.*`, `note`,
|
|
8
|
+
* the spinner. Verified against @clack/prompts as vendored here on 2026-08-16
|
|
9
|
+
* by running each of them with the two streams captured separately: stdout got
|
|
10
|
+
* the whole box-drawn transcript and stderr got nothing.
|
|
11
|
+
*
|
|
12
|
+
* That is fine for a contributor at a terminal and wrong for this group: a
|
|
13
|
+
* deploy command's stdout is a channel something downstream may PARSE or take
|
|
14
|
+
* whole, `::add-mask::` directives and minted credentials among the things
|
|
15
|
+
* that have lived there. So `cli.ts` prints no banner for the `deploy` group,
|
|
16
|
+
* and everything in this group reports through `say()`, which is stderr,
|
|
17
|
+
* instead of `log`/`note`. A `console.log` anywhere under this directory is a
|
|
18
|
+
* bug: the next command with a real stdout protocol would inherit a
|
|
19
|
+
* decoration it never asked for, silently, with no test catching it until
|
|
20
|
+
* that command exists.
|
|
21
|
+
*
|
|
22
|
+
* ## Failures
|
|
23
|
+
*
|
|
24
|
+
* `DeployError` carries the same two-part shape the scripts these commands
|
|
25
|
+
* grew out of printed by hand: a one-line message, then indented detail naming
|
|
26
|
+
* the thing to go and fix. `cli.ts` renders it to stderr and sets a non-zero
|
|
27
|
+
* exit code; a command that wants the failure in the job summary as well writes
|
|
28
|
+
* that itself, because which failures deserve a summary entry is a per-command
|
|
29
|
+
* judgement (`write-env`'s do; a bad flag does not).
|
|
30
|
+
*/
|
|
31
|
+
import { appendFileSync } from "node:fs";
|
|
32
|
+
/**
|
|
33
|
+
* A deploy command's own refusal, as opposed to a crash.
|
|
34
|
+
*
|
|
35
|
+
* The distinction lets `cli.ts` print a message a person can act on instead of
|
|
36
|
+
* a stack trace, and it is worth having even though both outcomes exit 1:
|
|
37
|
+
* these commands run unattended, and the log line IS the report.
|
|
38
|
+
*/
|
|
39
|
+
export class DeployError extends Error {
|
|
40
|
+
detail;
|
|
41
|
+
constructor(message, detail = []) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.name = "DeployError";
|
|
44
|
+
this.detail = detail;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Prints to STDERR. The only way a deploy command should say anything.
|
|
49
|
+
*
|
|
50
|
+
* See the header for why this is not `log.message`: stdout is machine-read
|
|
51
|
+
* here, and clack writes there.
|
|
52
|
+
*/
|
|
53
|
+
export function say(lines) {
|
|
54
|
+
if (lines.length === 0)
|
|
55
|
+
return;
|
|
56
|
+
process.stderr.write(`${lines.join("\n")}\n`);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Appends to the job summary under Actions; a no-op anywhere else.
|
|
60
|
+
*
|
|
61
|
+
* `env` is a parameter rather than a read of `process.env` so a test can point
|
|
62
|
+
* it at a temporary file without mutating the ambient environment of whatever
|
|
63
|
+
* else the runner is executing in the same process.
|
|
64
|
+
*/
|
|
65
|
+
export function summary(lines, env = process.env) {
|
|
66
|
+
const path = env.GITHUB_STEP_SUMMARY;
|
|
67
|
+
if (!path)
|
|
68
|
+
return;
|
|
69
|
+
appendFileSync(path, `${lines.join("\n")}\n`);
|
|
70
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pnpm devtools deploy require-planner`
|
|
3
|
+
*
|
|
4
|
+
* Refuses the §3.5 stage-1 dry run when its `DB_URL` is anything more than
|
|
5
|
+
* the `migration_planner` role.
|
|
6
|
+
*
|
|
7
|
+
* The preflight credential is the one narrowed value NOTHING can verify at
|
|
8
|
+
* rest: a full-access connection string pasted into `.env.preflight` works
|
|
9
|
+
* identically. The dry run succeeds, `env push` uploads it cleanly, and
|
|
10
|
+
* `env audit` reports no drift forever, because the stored value matches the
|
|
11
|
+
* file it came from. Only a live connection shows the difference, so this
|
|
12
|
+
* guard opens one: it must authenticate as the planner, it must FAIL to read
|
|
13
|
+
* `platform.profile`, and it must succeed in reading the migrations table the
|
|
14
|
+
* dry run is about to plan against. See `planner/checks.ts` for the three
|
|
15
|
+
* checks; `planner create` runs the same ones, so a credential that passed at
|
|
16
|
+
* minting time passes here.
|
|
17
|
+
*
|
|
18
|
+
* Runs in the `main-plan` job only. `production-plan` runs the same dry run
|
|
19
|
+
* with the PRODUCTION environment's `DB_URL`, which is the full connection
|
|
20
|
+
* string by design, since apps boot from it, so this guard would (correctly)
|
|
21
|
+
* refuse there. The narrowing belongs to the preflight tier, not to the dry
|
|
22
|
+
* run.
|
|
23
|
+
*
|
|
24
|
+
* Like the rest of the group: `cli:no-env` (the job holds one credential in
|
|
25
|
+
* the step's `env:` block and composes no file), reports through `say()`
|
|
26
|
+
* (stderr), refuses by `DeployError`. One line per check on success. This
|
|
27
|
+
* runs as its own step, and three green lines are the evidence the §3.5
|
|
28
|
+
* argument leans on.
|
|
29
|
+
*/
|
|
30
|
+
import { checkPlanner } from "../planner/checks.js";
|
|
31
|
+
import { connectDb } from "../planner/db.js";
|
|
32
|
+
import { DeployError, say } from "./report.js";
|
|
33
|
+
export async function runRequirePlanner(env = process.env, connect = connectDb) {
|
|
34
|
+
const url = env.DB_URL;
|
|
35
|
+
if (!url) {
|
|
36
|
+
throw new DeployError("DB_URL is not set — refusing to plan.", [
|
|
37
|
+
"This step runs with the preflight environment's DB_URL: the",
|
|
38
|
+
"migration_planner role, pushed by `env push --target preflight`.",
|
|
39
|
+
"An empty value usually means the environment secret was never",
|
|
40
|
+
"pushed, or the workflow step lost its env: block.",
|
|
41
|
+
]);
|
|
42
|
+
}
|
|
43
|
+
const db = connect(url);
|
|
44
|
+
try {
|
|
45
|
+
const verdict = await checkPlanner(db);
|
|
46
|
+
for (const line of verdict.lines)
|
|
47
|
+
say([`require-planner: ${line}`]);
|
|
48
|
+
if (!verdict.ok) {
|
|
49
|
+
throw new DeployError(`refusing to plan: ${verdict.problem}`, [
|
|
50
|
+
"The preflight DB_URL must be the migration_planner role and no",
|
|
51
|
+
"more — this environment is reachable from `main`. Mint or repair",
|
|
52
|
+
"it with `pnpm devtools planner create` (or `planner status` to",
|
|
53
|
+
"see what is wrong), then `env push --target preflight`.",
|
|
54
|
+
]);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
await db.end();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refuses the deploy when `CLOUDFLARE_API_TOKEN` is absent, and says nothing
|
|
3
|
+
* when it is set.
|
|
4
|
+
*
|
|
5
|
+
* The silence is deliberate: it stands in front of a deploy inside an `&&`
|
|
6
|
+
* chain, and a guard that announces itself on every run is a guard people stop
|
|
7
|
+
* reading. The refusal is the only thing it has to say.
|
|
8
|
+
*
|
|
9
|
+
* It refuses by throwing rather than by returning a code so that `cli.ts`
|
|
10
|
+
* renders it like every other deploy failure, through `say()`, which is
|
|
11
|
+
* stderr, never `explain()`, which is clack and therefore stdout.
|
|
12
|
+
*
|
|
13
|
+
* @throws {DeployError} when `CLOUDFLARE_API_TOKEN` is absent or empty.
|
|
14
|
+
*/
|
|
15
|
+
export declare function runRequireToken(env?: Record<string, string | undefined>): void;
|