@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.
Files changed (253) hide show
  1. package/README.md +44 -0
  2. package/bin/devtools-ci-bare.mjs +48 -0
  3. package/bin/devtools-ci.mjs +17 -0
  4. package/bin/devtools.mjs +27 -0
  5. package/dist/args.d.ts +26 -0
  6. package/dist/args.js +78 -0
  7. package/dist/bws/bw.d.ts +9 -0
  8. package/dist/bws/bw.js +49 -0
  9. package/dist/bws/client.d.ts +43 -0
  10. package/dist/bws/client.js +363 -0
  11. package/dist/bws/environments.d.ts +96 -0
  12. package/dist/bws/environments.js +111 -0
  13. package/dist/bws/pace.d.ts +27 -0
  14. package/dist/bws/pace.js +43 -0
  15. package/dist/bws/retry.d.ts +12 -0
  16. package/dist/bws/retry.js +39 -0
  17. package/dist/bws/store.d.ts +1 -0
  18. package/dist/bws/store.js +32 -0
  19. package/dist/bws/token.d.ts +31 -0
  20. package/dist/bws/token.js +88 -0
  21. package/dist/bws/vault.d.ts +29 -0
  22. package/dist/bws/vault.js +200 -0
  23. package/dist/catalog.d.ts +55 -0
  24. package/dist/catalog.js +81 -0
  25. package/dist/cf/commands.d.ts +1 -0
  26. package/dist/cf/commands.js +182 -0
  27. package/dist/cf/local-env.d.ts +44 -0
  28. package/dist/cf/local-env.js +148 -0
  29. package/dist/ci.d.ts +1 -0
  30. package/dist/ci.js +319 -0
  31. package/dist/cli.d.ts +11 -0
  32. package/dist/cli.js +1077 -0
  33. package/dist/commands.d.ts +255 -0
  34. package/dist/commands.js +1279 -0
  35. package/dist/completions.d.ts +4 -0
  36. package/dist/completions.js +110 -0
  37. package/dist/cron/commands.d.ts +47 -0
  38. package/dist/cron/commands.js +372 -0
  39. package/dist/cron/discovery.d.ts +55 -0
  40. package/dist/cron/discovery.js +85 -0
  41. package/dist/cron/schema.d.ts +45 -0
  42. package/dist/cron/schema.js +35 -0
  43. package/dist/db/config-push.d.ts +8 -0
  44. package/dist/db/config-push.js +11 -0
  45. package/dist/db/connection.d.ts +79 -0
  46. package/dist/db/connection.js +114 -0
  47. package/dist/db/exec.d.ts +1 -0
  48. package/dist/db/exec.js +11 -0
  49. package/dist/db/generate-migration.d.ts +1 -0
  50. package/dist/db/generate-migration.js +67 -0
  51. package/dist/db/generate-types.d.ts +1 -0
  52. package/dist/db/generate-types.js +4 -0
  53. package/dist/db/introspect.d.ts +1 -0
  54. package/dist/db/introspect.js +113 -0
  55. package/dist/db/local-env.d.ts +65 -0
  56. package/dist/db/local-env.js +23 -0
  57. package/dist/db/new-migration.d.ts +1 -0
  58. package/dist/db/new-migration.js +8 -0
  59. package/dist/db/run.d.ts +70 -0
  60. package/dist/db/run.js +158 -0
  61. package/dist/db/seed-buckets.d.ts +2 -0
  62. package/dist/db/seed-buckets.js +4 -0
  63. package/dist/db/seed-production.d.ts +8 -0
  64. package/dist/db/seed-production.js +73 -0
  65. package/dist/db/seed-roles.d.ts +7 -0
  66. package/dist/db/seed-roles.js +31 -0
  67. package/dist/db/session-refresh.d.ts +43 -0
  68. package/dist/db/session-refresh.js +40 -0
  69. package/dist/deploy/migrations.d.ts +20 -0
  70. package/dist/deploy/migrations.js +68 -0
  71. package/dist/deploy/orphans.d.ts +20 -0
  72. package/dist/deploy/orphans.js +177 -0
  73. package/dist/deploy/preflight.d.ts +53 -0
  74. package/dist/deploy/preflight.js +189 -0
  75. package/dist/deploy/report.d.ts +26 -0
  76. package/dist/deploy/report.js +70 -0
  77. package/dist/deploy/require-planner.d.ts +2 -0
  78. package/dist/deploy/require-planner.js +60 -0
  79. package/dist/deploy/require-token.d.ts +15 -0
  80. package/dist/deploy/require-token.js +78 -0
  81. package/dist/deploy/secrets-file.d.ts +17 -0
  82. package/dist/deploy/secrets-file.js +167 -0
  83. package/dist/deploy/write-env.d.ts +40 -0
  84. package/dist/deploy/write-env.js +393 -0
  85. package/dist/docs/index-pages.d.ts +36 -0
  86. package/dist/docs/index-pages.js +184 -0
  87. package/dist/doctor.d.ts +62 -0
  88. package/dist/doctor.js +225 -0
  89. package/dist/emails/commands.d.ts +15 -0
  90. package/dist/emails/commands.js +130 -0
  91. package/dist/emails/email-types.d.ts +13 -0
  92. package/dist/emails/email-types.js +1 -0
  93. package/dist/emails/fixtures.d.ts +20 -0
  94. package/dist/emails/fixtures.js +20 -0
  95. package/dist/env/audit.d.ts +206 -0
  96. package/dist/env/audit.js +436 -0
  97. package/dist/env/cloudflare.d.ts +21 -0
  98. package/dist/env/cloudflare.js +61 -0
  99. package/dist/env/commands.d.ts +99 -0
  100. package/dist/env/commands.js +612 -0
  101. package/dist/env/discovery.d.ts +15 -0
  102. package/dist/env/discovery.js +226 -0
  103. package/dist/env/document.d.ts +119 -0
  104. package/dist/env/document.js +323 -0
  105. package/dist/env/example.d.ts +68 -0
  106. package/dist/env/example.js +603 -0
  107. package/dist/env/selection.d.ts +105 -0
  108. package/dist/env/selection.js +226 -0
  109. package/dist/environment.d.ts +92 -0
  110. package/dist/environment.js +206 -0
  111. package/dist/fingerprint.d.ts +10 -0
  112. package/dist/fingerprint.js +18 -0
  113. package/dist/gen/campus-map.d.ts +1 -0
  114. package/dist/gen/campus-map.js +473 -0
  115. package/dist/gen/commands.d.ts +1 -0
  116. package/dist/gen/commands.js +25 -0
  117. package/dist/gen/hypno.d.ts +1 -0
  118. package/dist/gen/hypno.js +134 -0
  119. package/dist/gh/client.d.ts +80 -0
  120. package/dist/gh/client.js +258 -0
  121. package/dist/gh/environments.d.ts +83 -0
  122. package/dist/gh/environments.js +178 -0
  123. package/dist/gh/rulesets/actors.d.ts +14 -0
  124. package/dist/gh/rulesets/actors.js +68 -0
  125. package/dist/gh/rulesets/api.d.ts +15 -0
  126. package/dist/gh/rulesets/api.js +99 -0
  127. package/dist/gh/rulesets/commands.d.ts +3 -0
  128. package/dist/gh/rulesets/commands.js +203 -0
  129. package/dist/gh/rulesets/desired.d.ts +112 -0
  130. package/dist/gh/rulesets/desired.js +247 -0
  131. package/dist/gh/rulesets/diff.d.ts +65 -0
  132. package/dist/gh/rulesets/diff.js +134 -0
  133. package/dist/gh/rulesets/types.d.ts +76 -0
  134. package/dist/gh/rulesets/types.js +13 -0
  135. package/dist/gh/settings/api.d.ts +40 -0
  136. package/dist/gh/settings/api.js +172 -0
  137. package/dist/gh/settings/commands.d.ts +1 -0
  138. package/dist/gh/settings/commands.js +245 -0
  139. package/dist/gh/settings/desired.d.ts +53 -0
  140. package/dist/gh/settings/desired.js +39 -0
  141. package/dist/gh/settings/diff.d.ts +57 -0
  142. package/dist/gh/settings/diff.js +156 -0
  143. package/dist/gh/settings/types.d.ts +92 -0
  144. package/dist/gh/settings/types.js +11 -0
  145. package/dist/gh/settings/workflows.d.ts +41 -0
  146. package/dist/gh/settings/workflows.js +124 -0
  147. package/dist/grantRoot.d.ts +42 -0
  148. package/dist/grantRoot.js +89 -0
  149. package/dist/help.d.ts +18 -0
  150. package/dist/help.js +169 -0
  151. package/dist/images/commands.d.ts +35 -0
  152. package/dist/images/commands.js +233 -0
  153. package/dist/images/events.d.ts +36 -0
  154. package/dist/images/events.js +152 -0
  155. package/dist/images/graphics.d.ts +70 -0
  156. package/dist/images/graphics.js +233 -0
  157. package/dist/images/open-graph-types.d.ts +75 -0
  158. package/dist/images/open-graph-types.js +1 -0
  159. package/dist/images/prompts.d.ts +34 -0
  160. package/dist/images/prompts.js +100 -0
  161. package/dist/images/render.d.ts +21 -0
  162. package/dist/images/render.js +57 -0
  163. package/dist/images/select.d.ts +64 -0
  164. package/dist/images/select.js +96 -0
  165. package/dist/instance.d.ts +67 -0
  166. package/dist/instance.js +129 -0
  167. package/dist/invocation.d.ts +49 -0
  168. package/dist/invocation.js +78 -0
  169. package/dist/launch-ci.d.ts +1 -0
  170. package/dist/launch-ci.js +86 -0
  171. package/dist/launch.d.ts +29 -0
  172. package/dist/launch.js +371 -0
  173. package/dist/menu.d.ts +31 -0
  174. package/dist/menu.js +277 -0
  175. package/dist/newsletter/commands.d.ts +16 -0
  176. package/dist/newsletter/commands.js +311 -0
  177. package/dist/newsletter/imap.d.ts +17 -0
  178. package/dist/newsletter/imap.js +127 -0
  179. package/dist/newsletter/loopback.d.ts +24 -0
  180. package/dist/newsletter/loopback.js +100 -0
  181. package/dist/newsletter/oauth.d.ts +26 -0
  182. package/dist/newsletter/oauth.js +130 -0
  183. package/dist/newsletter/smtp.d.ts +23 -0
  184. package/dist/newsletter/smtp.js +133 -0
  185. package/dist/oauth/browser.d.ts +1 -0
  186. package/dist/oauth/browser.js +13 -0
  187. package/dist/oauth/config.d.ts +24 -0
  188. package/dist/oauth/config.js +24 -0
  189. package/dist/oauth/connect-url.d.ts +17 -0
  190. package/dist/oauth/connect-url.js +23 -0
  191. package/dist/oauth/db.d.ts +37 -0
  192. package/dist/oauth/db.js +108 -0
  193. package/dist/oauth/device.d.ts +93 -0
  194. package/dist/oauth/device.js +190 -0
  195. package/dist/oauth/discovery.d.ts +28 -0
  196. package/dist/oauth/discovery.js +54 -0
  197. package/dist/oauth/env-file.d.ts +6 -0
  198. package/dist/oauth/env-file.js +35 -0
  199. package/dist/oauth/exchange.d.ts +43 -0
  200. package/dist/oauth/exchange.js +87 -0
  201. package/dist/oauth/label.d.ts +1 -0
  202. package/dist/oauth/label.js +25 -0
  203. package/dist/oauth/loopback.d.ts +52 -0
  204. package/dist/oauth/loopback.js +162 -0
  205. package/dist/oauth/pkce.d.ts +6 -0
  206. package/dist/oauth/pkce.js +34 -0
  207. package/dist/oauth/target.d.ts +54 -0
  208. package/dist/oauth/target.js +102 -0
  209. package/dist/oauth/transport.d.ts +41 -0
  210. package/dist/oauth/transport.js +32 -0
  211. package/dist/oauth/wizard.d.ts +33 -0
  212. package/dist/oauth/wizard.js +571 -0
  213. package/dist/pick.d.ts +6 -0
  214. package/dist/pick.js +64 -0
  215. package/dist/planner/checks.d.ts +26 -0
  216. package/dist/planner/checks.js +104 -0
  217. package/dist/planner/commands.d.ts +10 -0
  218. package/dist/planner/commands.js +367 -0
  219. package/dist/planner/db.d.ts +15 -0
  220. package/dist/planner/db.js +33 -0
  221. package/dist/planner/role.d.ts +33 -0
  222. package/dist/planner/role.js +68 -0
  223. package/dist/repo/peers.d.ts +24 -0
  224. package/dist/repo/peers.js +114 -0
  225. package/dist/repo/preflight.d.ts +109 -0
  226. package/dist/repo/preflight.js +266 -0
  227. package/dist/repo/resolve.d.ts +41 -0
  228. package/dist/repo/resolve.js +152 -0
  229. package/dist/repo/root.d.ts +23 -0
  230. package/dist/repo/root.js +91 -0
  231. package/dist/repo/source.d.ts +14 -0
  232. package/dist/repo/source.js +54 -0
  233. package/dist/repo/tsx-loader.d.ts +6 -0
  234. package/dist/repo/tsx-loader.js +41 -0
  235. package/dist/run/pick.d.ts +60 -0
  236. package/dist/run/pick.js +559 -0
  237. package/dist/setup.d.ts +1 -0
  238. package/dist/setup.js +152 -0
  239. package/dist/stack.d.ts +48 -0
  240. package/dist/stack.js +290 -0
  241. package/dist/telemetry.d.ts +34 -0
  242. package/dist/telemetry.js +93 -0
  243. package/dist/tier.d.ts +40 -0
  244. package/dist/tier.js +93 -0
  245. package/dist/tsconfig.typecheck.tsbuildinfo +1 -0
  246. package/dist/ui.d.ts +25 -0
  247. package/dist/ui.js +51 -0
  248. package/dist/workers.d.ts +5 -0
  249. package/dist/workers.js +35 -0
  250. package/dist/workflows/commands.d.ts +44 -0
  251. package/dist/workflows/commands.js +668 -0
  252. package/env.ts +158 -0
  253. package/package.json +80 -0
@@ -0,0 +1,78 @@
1
+ /**
2
+ * `pnpm devtools deploy require-token`
3
+ *
4
+ * Refuses a Cloudflare deploy when `CLOUDFLARE_API_TOKEN` is absent. Without
5
+ * it, wrangler falls back to an interactive browser OAuth prompt, which on a
6
+ * contributor's machine reads as an invitation to authenticate. The policy
7
+ * (design doc §9) is that only devops hold the token and nobody else deploys.
8
+ *
9
+ * ## ⚠️ Invoke it through `cli:no-env`, never through `cli`
10
+ *
11
+ * This is a GUARD, not a command anybody types. It runs in two shapes, and
12
+ * `cli` (`with-env tsx src/cli.ts`) is wrong for both:
13
+ *
14
+ * * **Inside the deploy's own `with-env -c '...'` string**, in
15
+ * `apps/{platform,schedule-builder,sandbox}/package.json`:
16
+ *
17
+ * DEPLOY_ENV=production with-env -c '<guard> && wrangler deploy -e production'
18
+ *
19
+ * The guard must run after with-env loads the env files, because the token
20
+ * lives in a devops `.env.production` and never in the ambient
21
+ * environment. `guard && with-env deploy` would check the wrong
22
+ * environment; `with-env guard && deploy` would drop the loaded env for
23
+ * the deploy itself. `-c` gives both commands the same loaded env, and it
24
+ * evaluates through @yarnpkg/shell, so the `&&` chain works identically on
25
+ * Windows and POSIX. The env is therefore ALREADY LOADED by the time the
26
+ * guard runs; a second `with-env` inside it re-loads the same files to no
27
+ * effect and prints a second `with-env: loaded ...` line, which makes the
28
+ * deploy log claim the environment was selected twice.
29
+ *
30
+ * * **Bare, in CI**, in the orphan-audit and prune jobs, which run with no
31
+ * env file at all, on purpose: they read secret NAMES from Cloudflare and
32
+ * need exactly one credential to do it. Here `cli` is an outright bug.
33
+ * With no `.env*` present, `with-env` raises `MissingEnvFileError` and
34
+ * exits 1 BEFORE this code runs, so the guard would refuse the deploy
35
+ * while reporting a missing file instead of a missing token. The wrong
36
+ * diagnosis, on the failure path, which is the only path anybody reads.
37
+ *
38
+ * `cli:no-env` (`tsx --conditions=devdogs-source src/cli.ts`) is the seam for
39
+ * both: it loads no env of its own and reads whatever the caller already put
40
+ * in `process.env`. Verified by running `pnpm --filter @devdogsuga/devtools
41
+ * run cli:no-env` inside a real `with-env -c` string, where it resolves
42
+ * through @yarnpkg/shell and adds no second env-selection line.
43
+ *
44
+ * Nesting `with-env` is otherwise HARMLESS: dotenvx leaves values already in
45
+ * the environment alone, so the inner load re-reads the same files and changes
46
+ * nothing. The objection is the missing-file case above and the duplicated
47
+ * `with-env: loaded ...` line, not a wrong value.
48
+ *
49
+ * `cf:build:*` scripts deliberately do NOT run this: building needs no token.
50
+ */
51
+ import { DeployError } from "./report.js";
52
+ /**
53
+ * Refuses the deploy when `CLOUDFLARE_API_TOKEN` is absent, and says nothing
54
+ * when it is set.
55
+ *
56
+ * The silence is deliberate: it stands in front of a deploy inside an `&&`
57
+ * chain, and a guard that announces itself on every run is a guard people stop
58
+ * reading. The refusal is the only thing it has to say.
59
+ *
60
+ * It refuses by throwing rather than by returning a code so that `cli.ts`
61
+ * renders it like every other deploy failure, through `say()`, which is
62
+ * stderr, never `explain()`, which is clack and therefore stdout.
63
+ *
64
+ * @throws {DeployError} when `CLOUDFLARE_API_TOKEN` is absent or empty.
65
+ */
66
+ export function runRequireToken(env = process.env) {
67
+ if (env.CLOUDFLARE_API_TOKEN)
68
+ return;
69
+ throw new DeployError("CLOUDFLARE_API_TOKEN is not set — refusing to deploy.", [
70
+ "Cloudflare access is devops-only (design doc §9). Without this guard",
71
+ "wrangler falls back to an interactive browser login, which looks like",
72
+ "an invitation to authenticate rather than the refusal it should be.",
73
+ "",
74
+ "Ask the devops team to run this deploy. If you are joining them, the",
75
+ "token lives in the devops .env.production:",
76
+ " pnpm devtools env pull --target production",
77
+ ]);
78
+ }
@@ -0,0 +1,17 @@
1
+ export interface SecretsFileOptions {
2
+ /** `--app`: the workspace app whose manifest names the Worker's secrets. */
3
+ app: string;
4
+ /** Defaults to the ambient environment; a parameter so tests need not mutate it. */
5
+ env?: NodeJS.ProcessEnv;
6
+ }
7
+ export interface SecretsFileResult {
8
+ /** The 0700 directory holding the file; the caller removes it. */
9
+ dir: string;
10
+ /** The 0600 file itself. */
11
+ file: string;
12
+ /** Names sent, in registry order. Never values. */
13
+ keys: string[];
14
+ /** Declared, optional, and absent from the environment. Reported, not fatal. */
15
+ omitted: string[];
16
+ }
17
+ export declare function runDeploySecretsFile(options: SecretsFileOptions): Promise<SecretsFileResult>;
@@ -0,0 +1,167 @@
1
+ /**
2
+ * `devtools deploy secrets-file --app <app>`
3
+ *
4
+ * Composes the `--secrets-file` a `wrangler deploy` uploads with one Worker.
5
+ *
6
+ * Runs INSIDE `with-env`, invoked as the ordinary
7
+ * `pnpm devtools deploy secrets-file …`, so `process.env` is the environment
8
+ * `devtools deploy write-env` composed a few steps earlier. `write-env` is the
9
+ * one command in this group that has to bypass the wrapper. This one reads no
10
+ * GitHub context of its own: one place in the pipeline touches `secrets` and
11
+ * `vars`, and this is not it.
12
+ *
13
+ * ## Which keys, and why they are derived rather than listed
14
+ *
15
+ * `storableKeys()` intersected with the keys the app's own manifest declares.
16
+ * A hand-written list per Worker is the exact failure the registry was built
17
+ * to remove: a variable added to `src/env.ts` and forgotten in the list is
18
+ * simply absent from the deployed environment, and the first thing that reads
19
+ * it fails at run time in production.
20
+ *
21
+ * Two exclusions follow from that rule rather than being special cases:
22
+ *
23
+ * * `:tooling` sources — declared for the deploy pipeline itself, not for
24
+ * the Worker. `study-group-finder:tooling` declares `SECRET_KEY` for
25
+ * supadart, which runs on a laptop. A key the deploy needs is not
26
+ * automatically a key the WORKER needs, and sending one anyway hands an
27
+ * internet-facing Worker a credential it never asks for.
28
+ * * `client: true` declarations, inlined into the browser bundle at build
29
+ * time and not Worker secrets at all (§A.6.3).
30
+ *
31
+ * ## ⚠️ Always the complete set
32
+ *
33
+ * `wrangler deploy --secrets-file` applies ADDITIVELY: it preserves every
34
+ * secret it does not mention, which may be one another environment set. A
35
+ * partial file inherits stale values silently rather than failing. So this
36
+ * sends everything the app declares, every time, and `devtools deploy orphans`
37
+ * reports whatever the Worker is still holding that nothing declares any more.
38
+ *
39
+ * ## Minted keys are refused, not filled
40
+ *
41
+ * A `secrecy: "secret"` key `storableKeys()` excludes is "minted": signed at
42
+ * deploy time rather than stored anywhere, which is what `SANDBOX_PROXY_TOKEN`
43
+ * is declared as. This command has no minter of its own — there is nothing in
44
+ * this repository that produces one — so a declared minted key is always a
45
+ * hard failure here rather than a silent omission: a Worker that expects a
46
+ * value substituted fails loudly at the file it never got, not at whatever
47
+ * reads it.
48
+ *
49
+ * ## Interface
50
+ *
51
+ * DEPLOY_ENV=production pnpm devtools deploy secrets-file --app platform
52
+ *
53
+ * Writes `dir=` and `file=` to `$GITHUB_OUTPUT`. The caller removes `dir` in an
54
+ * `if: always()` step. The runner is ephemeral, so this matters on a
55
+ * self-hosted one and costs nothing on a hosted one.
56
+ */
57
+ import { mkdtempSync, writeFileSync } from "node:fs";
58
+ import { tmpdir } from "node:os";
59
+ import { join } from "node:path";
60
+ import { assertRegistryLoaded } from "../env/discovery.js";
61
+ import { getEnvSync } from "../repo/peers.js";
62
+ import { DeployError, say, summary } from "./report.js";
63
+ /**
64
+ * Declared by this app, for this app's Worker.
65
+ *
66
+ * `source` is the manifest that declared it, and a `:` suffix is the repo's
67
+ * existing convention for "declared here, but not part of the running thing".
68
+ * See `sectionOf()` in devtools' `env/example.ts`, which folds the two together
69
+ * for display and keeps them distinct everywhere else.
70
+ */
71
+ function declaredBy(entries, app) {
72
+ return entries.some((e) => e.source === app && !e.client);
73
+ }
74
+ export async function runDeploySecretsFile(options) {
75
+ assertRegistryLoaded();
76
+ const { app } = options;
77
+ const env = options.env ?? process.env;
78
+ const storable = new Set(getEnvSync().storableKeys());
79
+ const send = new Map();
80
+ const absent = [];
81
+ const minted = [];
82
+ for (const [key, entries] of getEnvSync().variables()) {
83
+ if (!declaredBy(entries, app))
84
+ continue;
85
+ const meta = entries[0].meta;
86
+ // Public server keys ship alongside the secrets, because nothing else
87
+ // reaches the Worker's runtime: OpenNext copies the Worker env into
88
+ // process.env per request, and a value that was only in the composed
89
+ // .env file at build time is gone by then. The first staging deploy
90
+ // proved it: the schedule-builder Worker booted without API_URL,
91
+ // REST_URL, PUBLISHABLE_KEY, STORAGE_S3_URL or S3_PROTOCOL_REGION and
92
+ // answered 500 on every route. Client (NEXT_PUBLIC) keys are excluded
93
+ // by `declaredBy` (inlined at build); never-store keys stay excluded
94
+ // because a Worker secret is a remote copy, the thing they forbid.
95
+ if (meta.secrecy === "secret") {
96
+ if (!storable.has(key)) {
97
+ minted.push(key);
98
+ continue;
99
+ }
100
+ }
101
+ else if (meta.secrecy !== "public") {
102
+ continue;
103
+ }
104
+ const value = env[key];
105
+ if (value === undefined || value === "") {
106
+ absent.push(key);
107
+ continue;
108
+ }
109
+ send.set(key, value);
110
+ }
111
+ // Absent is reported, not fatal. `deploy write-env` already failed the job
112
+ // for anything an app's schema requires, so what reaches here is optional by
113
+ // declaration, `OAUTH_CLIENT_SECRET` on a deployment that uses the built-in
114
+ // provider, say. Sending an empty string instead would be worse than omitting
115
+ // it: every consumer that checks for presence would read it as configured.
116
+ if (absent.length > 0) {
117
+ say([
118
+ `devtools deploy secrets-file: ${absent.length} optional key(s) have ` +
119
+ `no value and are omitted: ${absent.join(", ")}`,
120
+ ]);
121
+ }
122
+ if (minted.length > 0) {
123
+ throw new DeployError(`${app} declares minted secret(s) with nothing to mint them: ${minted.join(", ")}.`, [
124
+ "This command has no minter — nothing in this repository mints a",
125
+ "secret. Drop the `minted: true` declaration from the app's",
126
+ "env.ts, or the Worker deploys without it and keeps whatever the",
127
+ "previous deploy left — --secrets-file preserves omissions.",
128
+ ]);
129
+ }
130
+ if (send.size === 0) {
131
+ throw new DeployError(`${app} has no Worker secrets to send.`, [
132
+ "That is almost certainly wrong: a --secrets-file with nothing in it",
133
+ "would leave every previously set secret in place, unexamined.",
134
+ ]);
135
+ }
136
+ // mkdtemp gives 0700 on every platform Node supports; the mode is repeated
137
+ // anyway so the intent survives a reader who does not know that. RUNNER_TEMP
138
+ // is cleared between jobs on a hosted runner and is the documented place for
139
+ // this on a self-hosted one.
140
+ const dir = mkdtempSync(join(env.RUNNER_TEMP ?? tmpdir(), "deploy-secrets-"));
141
+ const file = join(dir, `${app}.json`);
142
+ writeFileSync(file, `${JSON.stringify(Object.fromEntries(send), null, 2)}\n`, { mode: 0o600 });
143
+ const out = env.GITHUB_OUTPUT;
144
+ if (out)
145
+ writeFileSync(out, `dir=${dir}\nfile=${file}\n`, { flag: "a" });
146
+ const keys = [...send.keys()];
147
+ say([
148
+ `devtools deploy secrets-file: ${send.size} secret(s) for ${app} -> ${file}`,
149
+ ...keys.map((k) => ` ${k}`),
150
+ ]);
151
+ // Names go to the job summary as well as the log, because this list IS every
152
+ // credential the Worker holds. Reading it should not require expanding a
153
+ // step: "why does this Worker hold that?" is exactly the question a
154
+ // per-deploy record makes answerable, and the answer lives in the app's
155
+ // manifest rather than anywhere in this pipeline.
156
+ summary([
157
+ `### Worker secrets sent to \`${app}\``,
158
+ "",
159
+ ...keys.map((key) => `* \`${key}\``),
160
+ "",
161
+ "Names only. The complete set is sent every deploy, because",
162
+ "`--secrets-file` preserves what it omits.",
163
+ "",
164
+ "",
165
+ ], env);
166
+ return { dir, file, keys, omitted: absent };
167
+ }
@@ -0,0 +1,40 @@
1
+ /** Where a value came from, for the provenance table. */
2
+ export type Provenance = "secret" | "variable" | "derived" | "committed";
3
+ export interface WriteEnvOptions {
4
+ /** `--source`: compose only what this manifest declares. */
5
+ source?: string | null;
6
+ /**
7
+ * Directory the env file is written into.
8
+ *
9
+ * The CLI always passes the project root, which is what the workflow means.
10
+ * It is a parameter so the tests can compose into a temporary directory:
11
+ * the real destinations are `.env.staging` and `.env.production`, which on a
12
+ * maintainer's laptop are the files being filled in with live credentials.
13
+ */
14
+ root?: string;
15
+ /** Defaults to the ambient environment; a parameter so tests need not mutate it. */
16
+ env?: NodeJS.ProcessEnv;
17
+ }
18
+ export interface WriteEnvResult {
19
+ /** The file that was written, relative to `root`. */
20
+ file: string;
21
+ /** Key → where its value came from, in the order they were written. */
22
+ provenance: Map<string, Provenance>;
23
+ }
24
+ /**
25
+ * Composes the file and writes it.
26
+ *
27
+ * Every refusal is a `DeployError`, and every one of them ALSO lands in the job
28
+ * summary, via the wrapper below. That is deliberate here and not for the other
29
+ * commands in the group: the failure mode this exists to catch is a missing
30
+ * credential, and the person who has to add it is reading the run page rather
31
+ * than a step's log.
32
+ */
33
+ export declare function runDeployWriteEnv(options?: WriteEnvOptions): Promise<WriteEnvResult>;
34
+ /**
35
+ * The line the job log gets: names and provenance only.
36
+ *
37
+ * Never a value, and never a fingerprint either. A fingerprint helps when
38
+ * comparing two stores by hand, and there is nothing here to compare against.
39
+ */
40
+ export declare function renderWriteEnvReport(result: WriteEnvResult): string[];