@devdogsuga/devtools 0.1.25 → 0.1.27

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.
Files changed (271) hide show
  1. package/README.md +72 -11
  2. package/bin/devtools.mjs +4 -4
  3. package/dist/cli-CZKkNHK5.js +6619 -0
  4. package/dist/failure-log-BBPGejQv.js +256 -0
  5. package/dist/launch.js +267 -281
  6. package/dist/local-env-D8HC2JK_.js +351 -0
  7. package/dist/local-env-DrBCXqD8.js +2 -0
  8. package/dist/menu-CQBsQRgH.js +1684 -0
  9. package/dist/peer-redirect-hooks.js +15 -0
  10. package/dist/peers-RBplwovA.js +229 -0
  11. package/dist/root-C2OmZDwD.js +94 -0
  12. package/dist/stack-Bx-55oi_.js +2 -0
  13. package/dist/stack-BynCgRVS.js +181 -0
  14. package/dist/supabase-project-0knM0mqd.js +426 -0
  15. package/dist/telemetry-C4foD8MD.js +249 -0
  16. package/dist/telemetry.js +2 -148
  17. package/package.json +10 -29
  18. package/bin/devtools-ci-bare.mjs +0 -48
  19. package/bin/devtools-ci.mjs +0 -31
  20. package/dist/args.d.ts +0 -26
  21. package/dist/args.js +0 -78
  22. package/dist/bws/bw.d.ts +0 -18
  23. package/dist/bws/bw.js +0 -70
  24. package/dist/bws/client.d.ts +0 -43
  25. package/dist/bws/client.js +0 -363
  26. package/dist/bws/environments.d.ts +0 -96
  27. package/dist/bws/environments.js +0 -111
  28. package/dist/bws/pace.d.ts +0 -25
  29. package/dist/bws/pace.js +0 -43
  30. package/dist/bws/retry.d.ts +0 -12
  31. package/dist/bws/retry.js +0 -39
  32. package/dist/bws/store.d.ts +0 -1
  33. package/dist/bws/store.js +0 -32
  34. package/dist/bws/token.d.ts +0 -31
  35. package/dist/bws/token.js +0 -88
  36. package/dist/bws/vault.d.ts +0 -29
  37. package/dist/bws/vault.js +0 -200
  38. package/dist/catalog.d.ts +0 -57
  39. package/dist/catalog.js +0 -57
  40. package/dist/cf/build.d.ts +0 -13
  41. package/dist/cf/build.js +0 -65
  42. package/dist/cf/commands.d.ts +0 -1
  43. package/dist/cf/commands.js +0 -172
  44. package/dist/cf/local-env.d.ts +0 -58
  45. package/dist/cf/local-env.js +0 -177
  46. package/dist/ci.d.ts +0 -1
  47. package/dist/ci.js +0 -328
  48. package/dist/cli.d.ts +0 -11
  49. package/dist/cli.js +0 -1094
  50. package/dist/commands.d.ts +0 -261
  51. package/dist/commands.js +0 -1318
  52. package/dist/completions.d.ts +0 -4
  53. package/dist/completions.js +0 -110
  54. package/dist/cron/commands.d.ts +0 -47
  55. package/dist/cron/commands.js +0 -372
  56. package/dist/cron/discovery.d.ts +0 -55
  57. package/dist/cron/discovery.js +0 -85
  58. package/dist/cron/schema.d.ts +0 -45
  59. package/dist/cron/schema.js +0 -35
  60. package/dist/db/config-push.d.ts +0 -8
  61. package/dist/db/config-push.js +0 -11
  62. package/dist/db/connection.d.ts +0 -79
  63. package/dist/db/connection.js +0 -114
  64. package/dist/db/exec.d.ts +0 -1
  65. package/dist/db/exec.js +0 -11
  66. package/dist/db/generate-types.d.ts +0 -1
  67. package/dist/db/generate-types.js +0 -4
  68. package/dist/db/generated-env.d.ts +0 -69
  69. package/dist/db/generated-env.js +0 -113
  70. package/dist/db/introspect.d.ts +0 -1
  71. package/dist/db/introspect.js +0 -122
  72. package/dist/db/local-env.d.ts +0 -65
  73. package/dist/db/local-env.js +0 -23
  74. package/dist/db/new-migration.d.ts +0 -8
  75. package/dist/db/new-migration.js +0 -69
  76. package/dist/db/pooler.d.ts +0 -23
  77. package/dist/db/pooler.js +0 -55
  78. package/dist/db/run.d.ts +0 -68
  79. package/dist/db/run.js +0 -156
  80. package/dist/db/seed-buckets.d.ts +0 -2
  81. package/dist/db/seed-buckets.js +0 -4
  82. package/dist/db/seed-production.d.ts +0 -8
  83. package/dist/db/seed-production.js +0 -79
  84. package/dist/db/seed-roles.d.ts +0 -7
  85. package/dist/db/seed-roles.js +0 -33
  86. package/dist/db/session-refresh.d.ts +0 -43
  87. package/dist/db/session-refresh.js +0 -40
  88. package/dist/deploy/migrations.d.ts +0 -20
  89. package/dist/deploy/migrations.js +0 -68
  90. package/dist/deploy/orphans.d.ts +0 -20
  91. package/dist/deploy/orphans.js +0 -177
  92. package/dist/deploy/preflight.d.ts +0 -53
  93. package/dist/deploy/preflight.js +0 -189
  94. package/dist/deploy/report.d.ts +0 -26
  95. package/dist/deploy/report.js +0 -70
  96. package/dist/deploy/require-planner.d.ts +0 -2
  97. package/dist/deploy/require-planner.js +0 -60
  98. package/dist/deploy/require-token.d.ts +0 -15
  99. package/dist/deploy/require-token.js +0 -78
  100. package/dist/deploy/secrets-file.d.ts +0 -17
  101. package/dist/deploy/secrets-file.js +0 -166
  102. package/dist/deploy/write-env.d.ts +0 -40
  103. package/dist/deploy/write-env.js +0 -395
  104. package/dist/docs/index-pages.d.ts +0 -38
  105. package/dist/docs/index-pages.js +0 -186
  106. package/dist/emails/commands.d.ts +0 -15
  107. package/dist/emails/commands.js +0 -130
  108. package/dist/emails/email-types.d.ts +0 -13
  109. package/dist/emails/email-types.js +0 -1
  110. package/dist/emails/fixtures.d.ts +0 -20
  111. package/dist/emails/fixtures.js +0 -20
  112. package/dist/env/audit.d.ts +0 -206
  113. package/dist/env/audit.js +0 -436
  114. package/dist/env/cloudflare.d.ts +0 -21
  115. package/dist/env/cloudflare.js +0 -61
  116. package/dist/env/commands.d.ts +0 -99
  117. package/dist/env/commands.js +0 -613
  118. package/dist/env/discovery.d.ts +0 -15
  119. package/dist/env/discovery.js +0 -239
  120. package/dist/env/document.d.ts +0 -119
  121. package/dist/env/document.js +0 -323
  122. package/dist/env/example.d.ts +0 -68
  123. package/dist/env/example.js +0 -605
  124. package/dist/env/selection.d.ts +0 -105
  125. package/dist/env/selection.js +0 -228
  126. package/dist/env-entry.d.ts +0 -49
  127. package/dist/env-entry.js +0 -204
  128. package/dist/environment-doctor.d.ts +0 -45
  129. package/dist/environment-doctor.js +0 -429
  130. package/dist/environment.d.ts +0 -92
  131. package/dist/environment.js +0 -185
  132. package/dist/fingerprint.d.ts +0 -10
  133. package/dist/fingerprint.js +0 -18
  134. package/dist/gen/campus-map.d.ts +0 -1
  135. package/dist/gen/campus-map.js +0 -473
  136. package/dist/gen/commands.d.ts +0 -1
  137. package/dist/gen/commands.js +0 -25
  138. package/dist/gen/hypno.d.ts +0 -1
  139. package/dist/gen/hypno.js +0 -134
  140. package/dist/gh/client.d.ts +0 -80
  141. package/dist/gh/client.js +0 -259
  142. package/dist/gh/environments.d.ts +0 -83
  143. package/dist/gh/environments.js +0 -178
  144. package/dist/gh/rulesets/actors.d.ts +0 -14
  145. package/dist/gh/rulesets/actors.js +0 -69
  146. package/dist/gh/rulesets/api.d.ts +0 -15
  147. package/dist/gh/rulesets/api.js +0 -104
  148. package/dist/gh/rulesets/commands.d.ts +0 -3
  149. package/dist/gh/rulesets/commands.js +0 -203
  150. package/dist/gh/rulesets/desired.d.ts +0 -112
  151. package/dist/gh/rulesets/desired.js +0 -271
  152. package/dist/gh/rulesets/diff.d.ts +0 -65
  153. package/dist/gh/rulesets/diff.js +0 -170
  154. package/dist/gh/rulesets/types.d.ts +0 -76
  155. package/dist/gh/rulesets/types.js +0 -13
  156. package/dist/gh/settings/api.d.ts +0 -40
  157. package/dist/gh/settings/api.js +0 -180
  158. package/dist/gh/settings/commands.d.ts +0 -1
  159. package/dist/gh/settings/commands.js +0 -254
  160. package/dist/gh/settings/desired.d.ts +0 -53
  161. package/dist/gh/settings/desired.js +0 -43
  162. package/dist/gh/settings/diff.d.ts +0 -57
  163. package/dist/gh/settings/diff.js +0 -161
  164. package/dist/gh/settings/types.d.ts +0 -92
  165. package/dist/gh/settings/types.js +0 -11
  166. package/dist/gh/settings/workflows.d.ts +0 -41
  167. package/dist/gh/settings/workflows.js +0 -128
  168. package/dist/grantRoot.d.ts +0 -42
  169. package/dist/grantRoot.js +0 -89
  170. package/dist/help.d.ts +0 -18
  171. package/dist/help.js +0 -169
  172. package/dist/images/commands.d.ts +0 -32
  173. package/dist/images/commands.js +0 -231
  174. package/dist/images/events.d.ts +0 -47
  175. package/dist/images/events.js +0 -116
  176. package/dist/images/graphics.d.ts +0 -70
  177. package/dist/images/graphics.js +0 -234
  178. package/dist/images/open-graph-types.d.ts +0 -75
  179. package/dist/images/open-graph-types.js +0 -1
  180. package/dist/images/prompts.d.ts +0 -34
  181. package/dist/images/prompts.js +0 -100
  182. package/dist/images/render.d.ts +0 -21
  183. package/dist/images/render.js +0 -61
  184. package/dist/images/select.d.ts +0 -64
  185. package/dist/images/select.js +0 -96
  186. package/dist/instance.d.ts +0 -69
  187. package/dist/instance.js +0 -102
  188. package/dist/invocation.d.ts +0 -49
  189. package/dist/invocation.js +0 -78
  190. package/dist/launch-ci.d.ts +0 -1
  191. package/dist/launch-ci.js +0 -90
  192. package/dist/launch.d.ts +0 -18
  193. package/dist/menu.d.ts +0 -43
  194. package/dist/menu.js +0 -300
  195. package/dist/moderation.d.ts +0 -70
  196. package/dist/moderation.js +0 -128
  197. package/dist/oauth/browser.d.ts +0 -1
  198. package/dist/oauth/browser.js +0 -13
  199. package/dist/oauth/config.d.ts +0 -24
  200. package/dist/oauth/config.js +0 -24
  201. package/dist/oauth/connect-url.d.ts +0 -17
  202. package/dist/oauth/connect-url.js +0 -23
  203. package/dist/oauth/db.d.ts +0 -37
  204. package/dist/oauth/db.js +0 -108
  205. package/dist/oauth/device.d.ts +0 -93
  206. package/dist/oauth/device.js +0 -198
  207. package/dist/oauth/discovery.d.ts +0 -28
  208. package/dist/oauth/discovery.js +0 -54
  209. package/dist/oauth/env-file.d.ts +0 -6
  210. package/dist/oauth/env-file.js +0 -35
  211. package/dist/oauth/exchange.d.ts +0 -43
  212. package/dist/oauth/exchange.js +0 -93
  213. package/dist/oauth/label.d.ts +0 -1
  214. package/dist/oauth/label.js +0 -25
  215. package/dist/oauth/loopback.d.ts +0 -52
  216. package/dist/oauth/loopback.js +0 -168
  217. package/dist/oauth/pkce.d.ts +0 -6
  218. package/dist/oauth/pkce.js +0 -34
  219. package/dist/oauth/target.d.ts +0 -54
  220. package/dist/oauth/target.js +0 -102
  221. package/dist/oauth/transport.d.ts +0 -41
  222. package/dist/oauth/transport.js +0 -32
  223. package/dist/oauth/wizard.d.ts +0 -40
  224. package/dist/oauth/wizard.js +0 -578
  225. package/dist/persona.d.ts +0 -18
  226. package/dist/persona.js +0 -221
  227. package/dist/pick.d.ts +0 -6
  228. package/dist/pick.js +0 -64
  229. package/dist/pipes.d.ts +0 -1
  230. package/dist/pipes.js +0 -25
  231. package/dist/planner/checks.d.ts +0 -26
  232. package/dist/planner/checks.js +0 -104
  233. package/dist/planner/commands.d.ts +0 -10
  234. package/dist/planner/commands.js +0 -367
  235. package/dist/planner/db.d.ts +0 -15
  236. package/dist/planner/db.js +0 -33
  237. package/dist/planner/role.d.ts +0 -33
  238. package/dist/planner/role.js +0 -68
  239. package/dist/repo/peer-redirect-hooks.d.ts +0 -16
  240. package/dist/repo/peer-redirect-hooks.js +0 -10
  241. package/dist/repo/peer-redirect.d.ts +0 -4
  242. package/dist/repo/peer-redirect.js +0 -26
  243. package/dist/repo/peers.d.ts +0 -39
  244. package/dist/repo/peers.js +0 -139
  245. package/dist/repo/resolve.d.ts +0 -41
  246. package/dist/repo/resolve.js +0 -152
  247. package/dist/repo/root.d.ts +0 -23
  248. package/dist/repo/root.js +0 -91
  249. package/dist/repo/source.d.ts +0 -14
  250. package/dist/repo/source.js +0 -54
  251. package/dist/repo/supabase-project.d.ts +0 -60
  252. package/dist/repo/supabase-project.js +0 -124
  253. package/dist/repo/tsx-loader.d.ts +0 -6
  254. package/dist/repo/tsx-loader.js +0 -41
  255. package/dist/run/pick.d.ts +0 -90
  256. package/dist/run/pick.js +0 -664
  257. package/dist/setup.d.ts +0 -1
  258. package/dist/setup.js +0 -280
  259. package/dist/stack.d.ts +0 -48
  260. package/dist/stack.js +0 -403
  261. package/dist/telemetry.d.ts +0 -59
  262. package/dist/tier.d.ts +0 -40
  263. package/dist/tier.js +0 -96
  264. package/dist/ui.d.ts +0 -40
  265. package/dist/ui.js +0 -71
  266. package/dist/version.d.ts +0 -7
  267. package/dist/version.js +0 -17
  268. package/dist/workers.d.ts +0 -5
  269. package/dist/workers.js +0 -33
  270. package/dist/workflows/commands.d.ts +0 -54
  271. package/dist/workflows/commands.js +0 -763
@@ -1,40 +0,0 @@
1
- import { loadEnvLoad, loadEnvSession } from "../repo/peers.js";
2
- /**
3
- * Re-enters development — refreshing `process.env` from `.env.generated`'s
4
- * latest state — when (and only when) this process is already running under
5
- * development. Returns the one-line note callers report on success, or an
6
- * empty array when the guard held and nothing needed refreshing.
7
- */
8
- export async function refreshSessionEnv(opts = {}) {
9
- const deployEnv = opts.deployEnv ?? process.env.DEPLOY_ENV;
10
- if (deployEnv !== undefined &&
11
- deployEnv !== "" &&
12
- deployEnv !== "development") {
13
- return [];
14
- }
15
- const envLoad = await loadEnvLoad();
16
- const enter = opts.enterEnvironment ?? (await loadEnvSession()).enterEnvironment;
17
- try {
18
- await enter("development", { override: true });
19
- }
20
- catch (err) {
21
- if (!(err instanceof envLoad.LocalStackOfflineError))
22
- throw err;
23
- // A `development:local` session whose stack just went down — `db stop`
24
- // did exactly what it was asked, so this refresh must not turn that into
25
- // a failure. Re-enter unqualified (probe decides: `.env` alone, with its
26
- // stale-overlay warning) but LEAVE `DEV_DB` in place: the session still
27
- // MEANS the local database, and `db/connection.ts`'s guard is what turns
28
- // a later data command into "start the stack", not `.env`'s own DB_URL.
29
- const devDb = process.env[envLoad.DEV_DB_ENV];
30
- delete process.env[envLoad.DEV_DB_ENV];
31
- try {
32
- await enter("development", { override: true });
33
- }
34
- finally {
35
- if (devDb !== undefined)
36
- process.env[envLoad.DEV_DB_ENV] = devDb;
37
- }
38
- }
39
- return [`refreshed ${envLoad.GENERATED_FILE}`];
40
- }
@@ -1,20 +0,0 @@
1
- /**
2
- * Dry-run the migrations and write the plan to the job summary.
3
- *
4
- * The plan text is captured, not streamed, so `dbPushDryRun`'s throw on a
5
- * non-zero exit is what fails the step when the connection is dead — and the
6
- * job summary then carries no plan, which is the honest outcome. `label` lets
7
- * the caller distinguish the two stages (`main`'s early plan vs the one taken
8
- * seconds before the apply); it is the only thing that varies between them.
9
- */
10
- export declare function runDeployPlan(label?: string, env?: NodeJS.ProcessEnv, plan?: (dbUrl: string) => Promise<string>): Promise<void>;
11
- /**
12
- * Apply the migrations to DB_URL.
13
- *
14
- * `dbPush(url, { yes: true })` — the bare push, WITHOUT the type regeneration
15
- * the contributor path layers on, so a production apply never writes back into
16
- * the checkout. A non-zero exit becomes a DeployError so the job fails with a
17
- * line a person can read; the supabase CLI's own output (inherited stdio) says
18
- * why above it.
19
- */
20
- export declare function runDeployMigrate(env?: NodeJS.ProcessEnv, push?: (dbUrl: string) => Promise<number>): Promise<void>;
@@ -1,68 +0,0 @@
1
- /**
2
- * `pnpm devtools-ci deploy plan` and `deploy migrate`.
3
- *
4
- * The two halves of a production schema change, behind the same tested seam as
5
- * the rest of the deploy group. They replace the raw `supabase db push` shell
6
- * that used to live in four places in `deploy.yaml` — `--dry-run` piped to
7
- * `$GITHUB_STEP_SUMMARY` for the plan, `--yes` for the apply. The load-bearing
8
- * `set -o pipefail` there (so a failed connection did not read as a clean plan)
9
- * was a shell idiom no test could reach; here it is `dbPushDryRun`'s
10
- * throw-on-non-zero, which a test drives.
11
- *
12
- * Both read `DB_URL` from the step's own `env:` (the plan under the
13
- * migration_planner role that `require-planner` guards, the apply under the
14
- * deploy-tier credential), compose no env file, and run through the bare `ci`
15
- * package script — the one-credential convention shared with `require-planner`
16
- * and `require-token`. Dependencies are injected (env, the push runner) rather
17
- * than reached for, so the tests exercise the argv, the refusals, the summary
18
- * framing, and the failed-connection invariant without a database.
19
- */
20
- import { dbPush, dbPushDryRun } from "../db/run.js";
21
- import { DeployError, say, summary } from "./report.js";
22
- const MISSING_DB_URL = [
23
- "This step runs with the environment's DB_URL. An empty value usually",
24
- "means the environment secret was never pushed, or the workflow step",
25
- "lost its env: block.",
26
- ];
27
- /**
28
- * Dry-run the migrations and write the plan to the job summary.
29
- *
30
- * The plan text is captured, not streamed, so `dbPushDryRun`'s throw on a
31
- * non-zero exit is what fails the step when the connection is dead — and the
32
- * job summary then carries no plan, which is the honest outcome. `label` lets
33
- * the caller distinguish the two stages (`main`'s early plan vs the one taken
34
- * seconds before the apply); it is the only thing that varies between them.
35
- */
36
- export async function runDeployPlan(label = "Migration plan", env = process.env, plan = dbPushDryRun) {
37
- const url = env.DB_URL;
38
- if (!url) {
39
- throw new DeployError("DB_URL is not set — refusing to plan.", MISSING_DB_URL);
40
- }
41
- const text = await plan(url);
42
- // Echoed to stderr as well as the summary, so the plan is visible in the live
43
- // job log too — stdout belongs to the machine for this group (see report.ts).
44
- say([text]);
45
- summary([`### ${label}`, "", "```", text, "```"], env);
46
- }
47
- /**
48
- * Apply the migrations to DB_URL.
49
- *
50
- * `dbPush(url, { yes: true })` — the bare push, WITHOUT the type regeneration
51
- * the contributor path layers on, so a production apply never writes back into
52
- * the checkout. A non-zero exit becomes a DeployError so the job fails with a
53
- * line a person can read; the supabase CLI's own output (inherited stdio) says
54
- * why above it.
55
- */
56
- export async function runDeployMigrate(env = process.env, push = (url) => dbPush(url, { yes: true })) {
57
- const url = env.DB_URL;
58
- if (!url) {
59
- throw new DeployError("DB_URL is not set — refusing to migrate.", MISSING_DB_URL);
60
- }
61
- const code = await push(url);
62
- if (code !== 0) {
63
- throw new DeployError(`\`supabase db push\` failed (exit ${code}).`, [
64
- "The migration was not applied. The supabase CLI output above says why.",
65
- ]);
66
- }
67
- say(["deploy migrate: migrations applied."]);
68
- }
@@ -1,20 +0,0 @@
1
- import { listWorkerSecrets } from "../env/cloudflare.js";
2
- export interface OrphansOptions {
3
- /** `--prune`: delete what it found. Nothing else in here can delete. */
4
- prune: boolean;
5
- /** Defaults to the ambient environment; a parameter so tests need not mutate it. */
6
- env?: NodeJS.ProcessEnv;
7
- /** Project root; the CLI passes the real one. A parameter for the tests. */
8
- root?: string;
9
- /** Injectable so the tests need neither wrangler nor a Cloudflare token. */
10
- listSecrets?: typeof listWorkerSecrets;
11
- /** Injectable for the same reason. The tests assert it is NOT called without `--prune`. */
12
- deleteSecret?: (app: string, key: string, environment: string) => void;
13
- }
14
- export interface OrphansResult {
15
- /** How many names nothing declares, across every readable Worker. */
16
- orphans: number;
17
- /** `worker/key` for each deletion that happened. Empty unless `--prune`. */
18
- pruned: string[];
19
- }
20
- export declare function runDeployOrphans(options: OrphansOptions): Promise<OrphansResult>;
@@ -1,177 +0,0 @@
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
- }
@@ -1,53 +0,0 @@
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>;
@@ -1,189 +0,0 @@
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
- }
@@ -1,26 +0,0 @@
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;
@@ -1,70 +0,0 @@
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
- }
@@ -1,2 +0,0 @@
1
- import { type Connect } from "../planner/db.js";
2
- export declare function runRequirePlanner(env?: Record<string, string | undefined>, connect?: Connect): Promise<void>;