@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,80 @@
|
|
|
1
|
+
export declare class GhError extends Error {
|
|
2
|
+
}
|
|
3
|
+
export interface GhSecret {
|
|
4
|
+
name: string;
|
|
5
|
+
updatedAt: string;
|
|
6
|
+
}
|
|
7
|
+
/** Unlike `GhSecret`, this carries the value: variables are readable. */
|
|
8
|
+
export interface GhVariable {
|
|
9
|
+
name: string;
|
|
10
|
+
value: string;
|
|
11
|
+
updatedAt: string;
|
|
12
|
+
}
|
|
13
|
+
/** Environment secret names and when each last changed. Never values. */
|
|
14
|
+
export declare function listSecrets(environment: string): Promise<GhSecret[]>;
|
|
15
|
+
/**
|
|
16
|
+
* Sets one secret, passing the value on **stdin**.
|
|
17
|
+
*
|
|
18
|
+
* Not `--body`, which would put a live credential in argv where `ps` can read
|
|
19
|
+
* it. Not `--env-file` either, despite it being one call for the whole set:
|
|
20
|
+
* that hands the file to `gh`'s own dotenv parser, and this tool has already
|
|
21
|
+
* parsed it with different rules. Two parsers over one file agree right up
|
|
22
|
+
* until a multi-line value, a PEM private key being the case, and then disagree
|
|
23
|
+
* silently, storing something that looks like a key and is not.
|
|
24
|
+
*
|
|
25
|
+
* One process per secret is the cost. It is paid once per rotation.
|
|
26
|
+
*/
|
|
27
|
+
export declare function setSecret(environment: string, name: string, value: string): Promise<void>;
|
|
28
|
+
export declare function deleteSecret(environment: string, name: string): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Environment variable names, VALUES, and when each last changed.
|
|
31
|
+
*
|
|
32
|
+
* The value is the point. It is what turns the GitHub half of `env audit` from
|
|
33
|
+
* a presence check into a real comparison for these keys: a variable whose
|
|
34
|
+
* value drifted from Bitwarden is detectable, where the same drift in a secret
|
|
35
|
+
* is not detectable by anything.
|
|
36
|
+
*/
|
|
37
|
+
export declare function listVariables(environment: string): Promise<GhVariable[]>;
|
|
38
|
+
/**
|
|
39
|
+
* Sets one variable, passing the value on **stdin**.
|
|
40
|
+
*
|
|
41
|
+
* Stdin rather than `--body` even though a variable is public by definition:
|
|
42
|
+
* argv is shared machinery, and a rule with an exception ("stdin for secrets,
|
|
43
|
+
* argv for variables") is a rule somebody applies to the wrong call. The
|
|
44
|
+
* `--env-file` form is refused for the reason `setSecret` refuses it: it would
|
|
45
|
+
* hand the file to `gh`'s own dotenv parser, which agrees with this tool's
|
|
46
|
+
* parser right up until a multi-line value and then disagrees silently.
|
|
47
|
+
*/
|
|
48
|
+
export declare function setVariable(environment: string, name: string, value: string): Promise<void>;
|
|
49
|
+
export declare function deleteVariable(environment: string, name: string): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* A repository-level variable. NAME ONLY, and that is deliberate.
|
|
52
|
+
*
|
|
53
|
+
* `gh variable list` will return the value here as readily as it does for an
|
|
54
|
+
* environment variable, and fetching it would invite the obvious next step,
|
|
55
|
+
* comparing it against Bitwarden. That comparison has no correct answer. This
|
|
56
|
+
* copy is unmanaged whatever it holds, so a value that matches today proves
|
|
57
|
+
* nothing except that somebody set them the same way once; reporting only the
|
|
58
|
+
* mismatch would then read as "the matching one is fine", which is the belief
|
|
59
|
+
* this whole check exists to break.
|
|
60
|
+
*/
|
|
61
|
+
export interface GhRepositoryVariable {
|
|
62
|
+
name: string;
|
|
63
|
+
updatedAt: string;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The repository's OWN variables, the ones with no environment.
|
|
67
|
+
*
|
|
68
|
+
* ⚠️ The reason this exists at all: an environment variable of the same name
|
|
69
|
+
* **shadows** a repository one, and nothing in GitHub's UI says so. Every job
|
|
70
|
+
* that runs in the environment reads the environment copy, so a repository
|
|
71
|
+
* variable a person set by hand before `env push` routed that key keeps its
|
|
72
|
+
* stale value indefinitely and invisibly, right up until somebody deletes the
|
|
73
|
+
* environment copy, at which point the stale value silently becomes live.
|
|
74
|
+
*
|
|
75
|
+
* No `--env`, and that missing flag IS the call. Everything else in this file
|
|
76
|
+
* addresses one environment; this addresses the repository itself, which is
|
|
77
|
+
* the scope nothing here writes to and therefore the scope nothing here would
|
|
78
|
+
* otherwise notice.
|
|
79
|
+
*/
|
|
80
|
+
export declare function listRepositoryVariables(): Promise<GhRepositoryVariable[]>;
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A thin wrapper over the `gh` CLI, for environment secrets and variables.
|
|
3
|
+
*
|
|
4
|
+
* `gh` rather than the REST API for the same reason `bws` is used over its API:
|
|
5
|
+
* a secret's value has to be encrypted client-side with a libsodium sealed box
|
|
6
|
+
* against the environment's public key before it can be sent. `gh` does that
|
|
7
|
+
* locally. Doing it by hand means fetching the key, pulling in a sodium
|
|
8
|
+
* binding, and owning that code forever to avoid one dependency that is already
|
|
9
|
+
* installed.
|
|
10
|
+
*
|
|
11
|
+
* ## Secrets and variables are two different stores
|
|
12
|
+
*
|
|
13
|
+
* They share an environment and nothing else, and the differences are the whole
|
|
14
|
+
* reason the public per-environment values go to the second:
|
|
15
|
+
*
|
|
16
|
+
* | | Sent as | Readable back | In logs |
|
|
17
|
+
* |------------|------------|---------------|----------------|
|
|
18
|
+
* | secret | sealed box | never | masked, by substring |
|
|
19
|
+
* | variable | plaintext | value and all | verbatim |
|
|
20
|
+
*
|
|
21
|
+
* The masking is not a nicety either way. `PROJECT_REF` as a *secret* rewrites
|
|
22
|
+
* `https://supabase.com/dashboard/project/<ref>` to `.../***`, which is the
|
|
23
|
+
* paused-project gate's whole output. And because the ref is a substring of
|
|
24
|
+
* every Supabase hostname, it corrupts unrelated log lines across the repo. The
|
|
25
|
+
* readability is what lets `env audit` compare a variable by VALUE, which it
|
|
26
|
+
* cannot do for anything on the top row.
|
|
27
|
+
*
|
|
28
|
+
* ⚠️ The corollary, and the reason each setter is its own function rather than
|
|
29
|
+
* one with a flag: putting a secret in the variable store publishes its value
|
|
30
|
+
* to anyone with read access to the repository's Actions config, silently and
|
|
31
|
+
* irreversibly. There is no operation here that can be pointed at the wrong
|
|
32
|
+
* store by a boolean somebody passed the wrong way round.
|
|
33
|
+
*/
|
|
34
|
+
import { spawn } from "node:child_process";
|
|
35
|
+
import { promisify } from "node:util";
|
|
36
|
+
import { execFile } from "node:child_process";
|
|
37
|
+
const run = promisify(execFile);
|
|
38
|
+
const MAX_BUFFER = 16 * 1024 * 1024;
|
|
39
|
+
export class GhError extends Error {
|
|
40
|
+
}
|
|
41
|
+
let warnedAboutTokenOverride = false;
|
|
42
|
+
/**
|
|
43
|
+
* Says, once, when `gh` is about to ignore its own login.
|
|
44
|
+
*
|
|
45
|
+
* `gh` prefers `GH_TOKEN` / `GITHUB_TOKEN` from the environment over the
|
|
46
|
+
* account somebody logged in with, and `with-env` loads `.env` for every
|
|
47
|
+
* devtools command, so a token line in that file takes over EVERY `gh` call
|
|
48
|
+
* silently. That is how a leftover classic PAT kept authenticating after the
|
|
49
|
+
* operator's real login was a perfectly valid OAuth token, right up until
|
|
50
|
+
* the org's classic-PAT block turned it into a 403 with no visible cause.
|
|
51
|
+
* stderr and not clack, so it shows the same way from every command shape.
|
|
52
|
+
*/
|
|
53
|
+
function warnOnEnvTokenOverride() {
|
|
54
|
+
if (warnedAboutTokenOverride)
|
|
55
|
+
return;
|
|
56
|
+
const source = process.env.GH_TOKEN
|
|
57
|
+
? "GH_TOKEN"
|
|
58
|
+
: process.env.GITHUB_TOKEN
|
|
59
|
+
? "GITHUB_TOKEN"
|
|
60
|
+
: undefined;
|
|
61
|
+
if (!source)
|
|
62
|
+
return;
|
|
63
|
+
warnedAboutTokenOverride = true;
|
|
64
|
+
console.error(`gh: authenticating with ${source} from the environment, NOT your ` +
|
|
65
|
+
"`gh auth login` account. with-env loads .env, so a token line there " +
|
|
66
|
+
"takes over silently — comment it out if that is not what you meant.");
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Turns a spawn failure into something with a next step in it.
|
|
70
|
+
*
|
|
71
|
+
* The three that happen are a missing binary, an unauthenticated CLI, and an
|
|
72
|
+
* environment that does not exist yet. The last reports as a bare 404, because
|
|
73
|
+
* an environment you cannot see and one that was never created look identical
|
|
74
|
+
* from here.
|
|
75
|
+
*/
|
|
76
|
+
function describe(err) {
|
|
77
|
+
const e = err;
|
|
78
|
+
if (e.code === "ENOENT") {
|
|
79
|
+
return ("The `gh` CLI is not installed, or is not on PATH.\n" +
|
|
80
|
+
"Install it from https://cli.github.com, then run `gh auth login`.");
|
|
81
|
+
}
|
|
82
|
+
const stderr = (e.stderr ?? "").trim();
|
|
83
|
+
if (/personal access token \(classic\)/i.test(stderr)) {
|
|
84
|
+
return (`${stderr}\n\n` +
|
|
85
|
+
"The organization forbids classic PATs (by design — §0.3), and gh " +
|
|
86
|
+
"authenticated with one anyway. gh prefers GH_TOKEN / GITHUB_TOKEN " +
|
|
87
|
+
"from the environment over your `gh auth login`, and with-env loads " +
|
|
88
|
+
".env — so the usual culprit is a leftover ghp_ token line in that " +
|
|
89
|
+
"file. Comment it out (and revoke the token at " +
|
|
90
|
+
"github.com/settings/tokens); your stored gh login then takes over.");
|
|
91
|
+
}
|
|
92
|
+
if (/not logged into|authentication required|gh auth login/i.test(stderr)) {
|
|
93
|
+
return `${stderr}\n\nRun \`gh auth login\`. Managing environment secrets needs the \`repo\` scope.`;
|
|
94
|
+
}
|
|
95
|
+
if (/404|not found/i.test(stderr)) {
|
|
96
|
+
return (`${stderr}\n\n` +
|
|
97
|
+
"A 404 usually means the environment does not exist yet — GitHub creates " +
|
|
98
|
+
"environments explicitly, and a typo silently addresses a different one. " +
|
|
99
|
+
"Check Settings → Environments.");
|
|
100
|
+
}
|
|
101
|
+
if (/403|forbidden|must have admin/i.test(stderr)) {
|
|
102
|
+
return `${stderr}\n\nSetting environment secrets needs admin on the repository.`;
|
|
103
|
+
}
|
|
104
|
+
if (/\b429\b|rate.?limit|too many requests/i.test(stderr)) {
|
|
105
|
+
return (`${stderr}\n\n` +
|
|
106
|
+
"GitHub rate-limited this. A push is one API call per secret, so a " +
|
|
107
|
+
"burst of pushes can trip the secondary limit — wait a minute and " +
|
|
108
|
+
"re-run; already-written values are simply overwritten with themselves.");
|
|
109
|
+
}
|
|
110
|
+
return stderr || e.message || "gh failed with no output.";
|
|
111
|
+
}
|
|
112
|
+
/** Environment secret names and when each last changed. Never values. */
|
|
113
|
+
export async function listSecrets(environment) {
|
|
114
|
+
warnOnEnvTokenOverride();
|
|
115
|
+
try {
|
|
116
|
+
const { stdout } = await run("gh", ["secret", "list", "--env", environment, "--json", "name,updatedAt"], { maxBuffer: MAX_BUFFER, shell: false });
|
|
117
|
+
return stdout.trim() === "" ? [] : JSON.parse(stdout);
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
throw new GhError(describe(err));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Sets one secret, passing the value on **stdin**.
|
|
125
|
+
*
|
|
126
|
+
* Not `--body`, which would put a live credential in argv where `ps` can read
|
|
127
|
+
* it. Not `--env-file` either, despite it being one call for the whole set:
|
|
128
|
+
* that hands the file to `gh`'s own dotenv parser, and this tool has already
|
|
129
|
+
* parsed it with different rules. Two parsers over one file agree right up
|
|
130
|
+
* until a multi-line value, a PEM private key being the case, and then disagree
|
|
131
|
+
* silently, storing something that looks like a key and is not.
|
|
132
|
+
*
|
|
133
|
+
* One process per secret is the cost. It is paid once per rotation.
|
|
134
|
+
*/
|
|
135
|
+
export async function setSecret(environment, name, value) {
|
|
136
|
+
warnOnEnvTokenOverride();
|
|
137
|
+
await new Promise((resolve, reject) => {
|
|
138
|
+
const child = spawn("gh", ["secret", "set", name, "--env", environment], {
|
|
139
|
+
stdio: ["pipe", "ignore", "pipe"],
|
|
140
|
+
shell: false,
|
|
141
|
+
});
|
|
142
|
+
let stderr = "";
|
|
143
|
+
child.stderr.on("data", (c) => (stderr += c.toString()));
|
|
144
|
+
child.on("error", (err) => reject(new GhError(describe(err))));
|
|
145
|
+
child.on("close", (code) => code === 0
|
|
146
|
+
? resolve()
|
|
147
|
+
: reject(new GhError(describe({ stderr, message: `gh exited ${code}` }))));
|
|
148
|
+
// Exactly the bytes, with no trailing newline: `gh` stores stdin verbatim,
|
|
149
|
+
// so a stray "\n" becomes part of the secret and every comparison against
|
|
150
|
+
// it fails in a way that reads like a wrong value rather than a stray byte.
|
|
151
|
+
child.stdin.end(value);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
export async function deleteSecret(environment, name) {
|
|
155
|
+
warnOnEnvTokenOverride();
|
|
156
|
+
try {
|
|
157
|
+
await run("gh", ["secret", "delete", name, "--env", environment], {
|
|
158
|
+
shell: false,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
throw new GhError(describe(err));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// ── variables ────────────────────────────────────────────────────────────────
|
|
166
|
+
/**
|
|
167
|
+
* Environment variable names, VALUES, and when each last changed.
|
|
168
|
+
*
|
|
169
|
+
* The value is the point. It is what turns the GitHub half of `env audit` from
|
|
170
|
+
* a presence check into a real comparison for these keys: a variable whose
|
|
171
|
+
* value drifted from Bitwarden is detectable, where the same drift in a secret
|
|
172
|
+
* is not detectable by anything.
|
|
173
|
+
*/
|
|
174
|
+
export async function listVariables(environment) {
|
|
175
|
+
warnOnEnvTokenOverride();
|
|
176
|
+
try {
|
|
177
|
+
const { stdout } = await run("gh", [
|
|
178
|
+
"variable",
|
|
179
|
+
"list",
|
|
180
|
+
"--env",
|
|
181
|
+
environment,
|
|
182
|
+
"--json",
|
|
183
|
+
"name,value,updatedAt",
|
|
184
|
+
], { maxBuffer: MAX_BUFFER, shell: false });
|
|
185
|
+
return stdout.trim() === "" ? [] : JSON.parse(stdout);
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
throw new GhError(describe(err));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Sets one variable, passing the value on **stdin**.
|
|
193
|
+
*
|
|
194
|
+
* Stdin rather than `--body` even though a variable is public by definition:
|
|
195
|
+
* argv is shared machinery, and a rule with an exception ("stdin for secrets,
|
|
196
|
+
* argv for variables") is a rule somebody applies to the wrong call. The
|
|
197
|
+
* `--env-file` form is refused for the reason `setSecret` refuses it: it would
|
|
198
|
+
* hand the file to `gh`'s own dotenv parser, which agrees with this tool's
|
|
199
|
+
* parser right up until a multi-line value and then disagrees silently.
|
|
200
|
+
*/
|
|
201
|
+
export async function setVariable(environment, name, value) {
|
|
202
|
+
warnOnEnvTokenOverride();
|
|
203
|
+
await new Promise((resolve, reject) => {
|
|
204
|
+
const child = spawn("gh", ["variable", "set", name, "--env", environment], {
|
|
205
|
+
stdio: ["pipe", "ignore", "pipe"],
|
|
206
|
+
shell: false,
|
|
207
|
+
});
|
|
208
|
+
let stderr = "";
|
|
209
|
+
child.stderr.on("data", (c) => (stderr += c.toString()));
|
|
210
|
+
child.on("error", (err) => reject(new GhError(describe(err))));
|
|
211
|
+
child.on("close", (code) => code === 0
|
|
212
|
+
? resolve()
|
|
213
|
+
: reject(new GhError(describe({ stderr, message: `gh exited ${code}` }))));
|
|
214
|
+
// No trailing newline, for a reason that bites harder here than it does for
|
|
215
|
+
// a secret: the stored value IS readable, so a stray "\n" turns every
|
|
216
|
+
// `audit` run into a value-drift error against a Bitwarden copy that is
|
|
217
|
+
// actually identical, and no amount of re-pushing fixes it.
|
|
218
|
+
child.stdin.end(value);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
export async function deleteVariable(environment, name) {
|
|
222
|
+
warnOnEnvTokenOverride();
|
|
223
|
+
try {
|
|
224
|
+
await run("gh", ["variable", "delete", name, "--env", environment], {
|
|
225
|
+
shell: false,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
catch (err) {
|
|
229
|
+
throw new GhError(describe(err));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* The repository's OWN variables, the ones with no environment.
|
|
234
|
+
*
|
|
235
|
+
* ⚠️ The reason this exists at all: an environment variable of the same name
|
|
236
|
+
* **shadows** a repository one, and nothing in GitHub's UI says so. Every job
|
|
237
|
+
* that runs in the environment reads the environment copy, so a repository
|
|
238
|
+
* variable a person set by hand before `env push` routed that key keeps its
|
|
239
|
+
* stale value indefinitely and invisibly, right up until somebody deletes the
|
|
240
|
+
* environment copy, at which point the stale value silently becomes live.
|
|
241
|
+
*
|
|
242
|
+
* No `--env`, and that missing flag IS the call. Everything else in this file
|
|
243
|
+
* addresses one environment; this addresses the repository itself, which is
|
|
244
|
+
* the scope nothing here writes to and therefore the scope nothing here would
|
|
245
|
+
* otherwise notice.
|
|
246
|
+
*/
|
|
247
|
+
export async function listRepositoryVariables() {
|
|
248
|
+
warnOnEnvTokenOverride();
|
|
249
|
+
try {
|
|
250
|
+
const { stdout } = await run("gh", ["variable", "list", "--json", "name,updatedAt"], { maxBuffer: MAX_BUFFER, shell: false });
|
|
251
|
+
return stdout.trim() === ""
|
|
252
|
+
? []
|
|
253
|
+
: JSON.parse(stdout);
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
256
|
+
throw new GhError(describe(err));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export declare const GITHUB_ENVIRONMENTS: readonly ["preflight", "staging", "production", "production-apply"];
|
|
2
|
+
export type GithubEnvironment = (typeof GITHUB_ENVIRONMENTS)[number];
|
|
3
|
+
export declare function isGithubEnvironment(v: string): v is GithubEnvironment;
|
|
4
|
+
export interface GithubEnvironmentSpec {
|
|
5
|
+
/** BWS project to compare against, or null when nothing backs it. */
|
|
6
|
+
bwsProject: string | null;
|
|
7
|
+
/** Deployment branch policy, for the summary line. */
|
|
8
|
+
branch: string;
|
|
9
|
+
/**
|
|
10
|
+
* Keys allowed here, or null for "whatever the file defines".
|
|
11
|
+
*
|
|
12
|
+
* ⚠️ NULL ON EVERY ROW TODAY, and that is the deliberate state rather than a
|
|
13
|
+
* field nobody got round to filling in. `production-apply` used to carry the
|
|
14
|
+
* apply-tier pair here, on the reasoning that "holding a third credential
|
|
15
|
+
* makes the reviewer gate stop meaning anything", which conflated two
|
|
16
|
+
* constraints and kept only the one that was not the gate. An allowlist here
|
|
17
|
+
* restricts what the REVIEWED environment may hold; the gate is
|
|
18
|
+
* `production.excludeKeys`, which restricts what the UNREVIEWED one may hold.
|
|
19
|
+
* Only the second is a security property, and it is enforced below.
|
|
20
|
+
*
|
|
21
|
+
* Kept as a field because the mechanism is worth having and costs one branch
|
|
22
|
+
* in `accepts()`: a future environment that genuinely holds a fixed short
|
|
23
|
+
* list, a third-party integration's two keys and nothing else, states it here
|
|
24
|
+
* rather than as an `excludeKeys` of everything else.
|
|
25
|
+
*/
|
|
26
|
+
onlyKeys: readonly string[] | null;
|
|
27
|
+
/**
|
|
28
|
+
* Keys that must never reach this environment.
|
|
29
|
+
*
|
|
30
|
+
* The whole reviewer gate now lives in this field: `production` (and
|
|
31
|
+
* `staging`) exclude the apply-tier set, and nothing else stops a
|
|
32
|
+
* write-capable credential from landing in an environment that deploys with
|
|
33
|
+
* nobody in front of it.
|
|
34
|
+
*/
|
|
35
|
+
excludeKeys: readonly string[];
|
|
36
|
+
/** Extra confirmation before writing. */
|
|
37
|
+
guarded: boolean;
|
|
38
|
+
}
|
|
39
|
+
export declare const GITHUB_ENVIRONMENT_SPECS: Record<GithubEnvironment, GithubEnvironmentSpec>;
|
|
40
|
+
/** The GitHub environments fed by one Bitwarden project, in precedence order. */
|
|
41
|
+
export declare function githubTargets(bwsProject: string): GithubEnvironment[];
|
|
42
|
+
/** Whether one environment takes a given key. */
|
|
43
|
+
export declare function accepts(environment: GithubEnvironment, key: string): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* The PRIMARY environment a key belongs in, or `null` for "nowhere here".
|
|
46
|
+
*
|
|
47
|
+
* `null` is not an error. Pushing `staging` with an apply-only credential in
|
|
48
|
+
* the file is the ordinary case: that key belongs to production and has no home
|
|
49
|
+
* in the staging environment.
|
|
50
|
+
*
|
|
51
|
+
* ⚠️ "Primary", not "only". Since `production-apply` became a superset, an
|
|
52
|
+
* ordinary production key legitimately lives in TWO environments and this
|
|
53
|
+
* returns the first, the unreviewed one, where the deploy reads it. A caller
|
|
54
|
+
* asking "is this copy misplaced?" wants `acceptedBy()` instead; answering that
|
|
55
|
+
* question with this function reports every correctly-pushed copy in
|
|
56
|
+
* `production-apply` as a stray that somebody should delete.
|
|
57
|
+
*/
|
|
58
|
+
export declare function routeTo(bwsProject: string, key: string): GithubEnvironment | null;
|
|
59
|
+
/**
|
|
60
|
+
* EVERY environment of a project that takes a key, in precedence order.
|
|
61
|
+
*
|
|
62
|
+
* The set-shaped question, for callers that compare against what a push
|
|
63
|
+
* actually wrote rather than against one destination. `routeTo()` is this with
|
|
64
|
+
* `[0]` taken, and the difference is only ever visible for `production`, the
|
|
65
|
+
* one project that fans out to two environments.
|
|
66
|
+
*/
|
|
67
|
+
export declare function acceptedBy(bwsProject: string, key: string): GithubEnvironment[];
|
|
68
|
+
/**
|
|
69
|
+
* `accepts()` for an environment name that has not been narrowed yet, in the
|
|
70
|
+
* argument order `AuditInput.accepted` wants.
|
|
71
|
+
*
|
|
72
|
+
* Exists so that the predicate `env audit` hands the audit is a NAME rather
|
|
73
|
+
* than a lambda written at the call site: `runEnvAudit` cannot be unit-tested
|
|
74
|
+
* without mocking Bitwarden, GitHub and Cloudflare at once, so anything spelled
|
|
75
|
+
* out there is untested by construction.
|
|
76
|
+
*
|
|
77
|
+
* ⚠️ An unrecognised environment is REFUSED, not accepted. The string comes
|
|
78
|
+
* from whatever `gh` listed, and the audit uses this to decide whether a found
|
|
79
|
+
* copy is a stray. Failing open here would silence the finding for a copy
|
|
80
|
+
* sitting in an environment this repository has never heard of, which is
|
|
81
|
+
* exactly the case worth hearing about.
|
|
82
|
+
*/
|
|
83
|
+
export declare function acceptsKey(key: string, environment: string): boolean;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The GitHub environments, and which BWS project (if any) backs each.
|
|
3
|
+
*
|
|
4
|
+
* GitHub environments and BWS projects are NOT the same set, and conflating
|
|
5
|
+
* them is the mistake this file exists to prevent. There are FOUR GitHub
|
|
6
|
+
* environments and THREE BWS projects:
|
|
7
|
+
*
|
|
8
|
+
* | GitHub environment | BWS project | Receives |
|
|
9
|
+
* |--------------------|--------------|------------------------------|
|
|
10
|
+
* | `preflight` | `preflight` | everything in the project |
|
|
11
|
+
* | `staging` | `staging` | everything in the project |
|
|
12
|
+
* | `production` | `production` | everything EXCEPT apply-only |
|
|
13
|
+
* | `production-apply` | `production` | everything, apply-only too |
|
|
14
|
+
*
|
|
15
|
+
* The last two split one project between two GitHub environments, and that
|
|
16
|
+
* split IS the reviewer gate. `production` deploys on a push with nothing in
|
|
17
|
+
* front of it; `production-apply` has required reviewers. A write-capable
|
|
18
|
+
* credential reaching the first makes the second decorative, so the routing is
|
|
19
|
+
* enforced here rather than left to whoever last edited a file.
|
|
20
|
+
*
|
|
21
|
+
* ⚠️ THE GATE IS ONE-DIRECTIONAL, and reading it as symmetric is the mistake
|
|
22
|
+
* this file made until 2026-08-17. What the gate asserts, through
|
|
23
|
+
* `production.excludeKeys`, is that apply-tier credentials never reach the
|
|
24
|
+
* UNREVIEWED environment, and nothing more. What the reviewed environment also
|
|
25
|
+
* holds buys no privilege: it is the same Bitwarden project, and
|
|
26
|
+
* `production-apply` is the strictly MORE trusted of the two. Withholding
|
|
27
|
+
* ordinary keys from it broke every job that runs there: the config push wanted
|
|
28
|
+
* deploy-tier OAuth secrets, and the orphan prune wanted a deploy-tier API
|
|
29
|
+
* token. So `production-apply` now receives a SUPERSET of `production`.
|
|
30
|
+
*/
|
|
31
|
+
import { assertRegistryLoaded } from "../env/discovery.js";
|
|
32
|
+
import { getEnvSync } from "../repo/peers.js";
|
|
33
|
+
export const GITHUB_ENVIRONMENTS = [
|
|
34
|
+
"preflight",
|
|
35
|
+
"staging",
|
|
36
|
+
"production",
|
|
37
|
+
"production-apply",
|
|
38
|
+
];
|
|
39
|
+
export function isGithubEnvironment(v) {
|
|
40
|
+
return GITHUB_ENVIRONMENTS.includes(v);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The apply-only set, derived from the env manifests (`tier: "apply"` on the
|
|
44
|
+
* declarations in `packages/devtools/env.ts`) rather than listed here.
|
|
45
|
+
*
|
|
46
|
+
* Read at ACCESS time, not module load: this module is imported by the CLI
|
|
47
|
+
* before any manifest is, so a snapshot taken now would be empty, and an empty
|
|
48
|
+
* apply set routes the write-capable credentials to the unreviewed
|
|
49
|
+
* `production` environment, which is the exact failure the split exists to
|
|
50
|
+
* prevent. The guard turns "forgot to loadRegistry()" into a crash instead.
|
|
51
|
+
*/
|
|
52
|
+
function applyOnly() {
|
|
53
|
+
assertRegistryLoaded();
|
|
54
|
+
return getEnvSync().applyOnlyKeys();
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The plan-tier set, same shape and same load-time caveat as `applyOnly()`.
|
|
58
|
+
* Read by `main-plan` (preflight) and `production-plan` (production); a copy
|
|
59
|
+
* anywhere else is a credential nothing reads, so `staging` excludes it.
|
|
60
|
+
*/
|
|
61
|
+
function planOnly() {
|
|
62
|
+
assertRegistryLoaded();
|
|
63
|
+
return getEnvSync().planOnlyKeys();
|
|
64
|
+
}
|
|
65
|
+
export const GITHUB_ENVIRONMENT_SPECS = {
|
|
66
|
+
preflight: {
|
|
67
|
+
bwsProject: "preflight",
|
|
68
|
+
branch: "main",
|
|
69
|
+
onlyKeys: null,
|
|
70
|
+
excludeKeys: [],
|
|
71
|
+
guarded: false,
|
|
72
|
+
},
|
|
73
|
+
staging: {
|
|
74
|
+
bwsProject: "staging",
|
|
75
|
+
branch: "main",
|
|
76
|
+
onlyKeys: null,
|
|
77
|
+
// Both narrow tiers: no staging job plans, and none may apply.
|
|
78
|
+
get excludeKeys() {
|
|
79
|
+
return [...applyOnly(), ...planOnly()];
|
|
80
|
+
},
|
|
81
|
+
guarded: false,
|
|
82
|
+
},
|
|
83
|
+
// ⚠️ THE REVIEWER GATE, in one property. `excludeKeys` here is the only
|
|
84
|
+
// thing keeping the apply-tier credentials out of an environment that
|
|
85
|
+
// deploys on a push to `production` with nobody in front of it. Widening it
|
|
86
|
+
// to `[]` hands a write-capable token to an unreviewed deploy, and so does
|
|
87
|
+
// letting `applyOnly()` answer empty, which is what `assertRegistryLoaded()`
|
|
88
|
+
// exists to prevent. `environments.test.ts` asserts both apply keys by name
|
|
89
|
+
// against exactly this.
|
|
90
|
+
production: {
|
|
91
|
+
bwsProject: "production",
|
|
92
|
+
branch: "production",
|
|
93
|
+
onlyKeys: null,
|
|
94
|
+
get excludeKeys() {
|
|
95
|
+
return applyOnly();
|
|
96
|
+
},
|
|
97
|
+
guarded: true,
|
|
98
|
+
},
|
|
99
|
+
// A SUPERSET of `production`, deliberately: everything that environment
|
|
100
|
+
// receives, plus the apply-tier credential it may not have.
|
|
101
|
+
//
|
|
102
|
+
// Not a relaxation of the gate. This is the same Bitwarden project behind
|
|
103
|
+
// required reviewers, the more trusted half of the split, so withholding a
|
|
104
|
+
// key from it protects nothing and only starves the jobs that run here
|
|
105
|
+
// (`production-config`, `prune-orphans`, both of which needed a deploy-tier
|
|
106
|
+
// secret and got none). The gate is what `production` may NOT have, one row
|
|
107
|
+
// above.
|
|
108
|
+
"production-apply": {
|
|
109
|
+
bwsProject: "production",
|
|
110
|
+
branch: "production",
|
|
111
|
+
onlyKeys: null,
|
|
112
|
+
excludeKeys: [],
|
|
113
|
+
guarded: true,
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
// ── Routing ──────────────────────────────────────────────────────────────────
|
|
117
|
+
//
|
|
118
|
+
// One Bitwarden project can feed more than one GitHub environment; production
|
|
119
|
+
// feeds two. So a push has to know which key goes where. Derived from the table
|
|
120
|
+
// above rather than written out a second time: the split IS the reviewer gate,
|
|
121
|
+
// and a hardcoded copy of it is a copy that can disagree.
|
|
122
|
+
/** The GitHub environments fed by one Bitwarden project, in precedence order. */
|
|
123
|
+
export function githubTargets(bwsProject) {
|
|
124
|
+
return GITHUB_ENVIRONMENTS.filter((e) => GITHUB_ENVIRONMENT_SPECS[e].bwsProject === bwsProject);
|
|
125
|
+
}
|
|
126
|
+
/** Whether one environment takes a given key. */
|
|
127
|
+
export function accepts(environment, key) {
|
|
128
|
+
const spec = GITHUB_ENVIRONMENT_SPECS[environment];
|
|
129
|
+
return spec.onlyKeys
|
|
130
|
+
? spec.onlyKeys.includes(key)
|
|
131
|
+
: !spec.excludeKeys.includes(key);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* The PRIMARY environment a key belongs in, or `null` for "nowhere here".
|
|
135
|
+
*
|
|
136
|
+
* `null` is not an error. Pushing `staging` with an apply-only credential in
|
|
137
|
+
* the file is the ordinary case: that key belongs to production and has no home
|
|
138
|
+
* in the staging environment.
|
|
139
|
+
*
|
|
140
|
+
* ⚠️ "Primary", not "only". Since `production-apply` became a superset, an
|
|
141
|
+
* ordinary production key legitimately lives in TWO environments and this
|
|
142
|
+
* returns the first, the unreviewed one, where the deploy reads it. A caller
|
|
143
|
+
* asking "is this copy misplaced?" wants `acceptedBy()` instead; answering that
|
|
144
|
+
* question with this function reports every correctly-pushed copy in
|
|
145
|
+
* `production-apply` as a stray that somebody should delete.
|
|
146
|
+
*/
|
|
147
|
+
export function routeTo(bwsProject, key) {
|
|
148
|
+
return acceptedBy(bwsProject, key)[0] ?? null;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* EVERY environment of a project that takes a key, in precedence order.
|
|
152
|
+
*
|
|
153
|
+
* The set-shaped question, for callers that compare against what a push
|
|
154
|
+
* actually wrote rather than against one destination. `routeTo()` is this with
|
|
155
|
+
* `[0]` taken, and the difference is only ever visible for `production`, the
|
|
156
|
+
* one project that fans out to two environments.
|
|
157
|
+
*/
|
|
158
|
+
export function acceptedBy(bwsProject, key) {
|
|
159
|
+
return githubTargets(bwsProject).filter((e) => accepts(e, key));
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* `accepts()` for an environment name that has not been narrowed yet, in the
|
|
163
|
+
* argument order `AuditInput.accepted` wants.
|
|
164
|
+
*
|
|
165
|
+
* Exists so that the predicate `env audit` hands the audit is a NAME rather
|
|
166
|
+
* than a lambda written at the call site: `runEnvAudit` cannot be unit-tested
|
|
167
|
+
* without mocking Bitwarden, GitHub and Cloudflare at once, so anything spelled
|
|
168
|
+
* out there is untested by construction.
|
|
169
|
+
*
|
|
170
|
+
* ⚠️ An unrecognised environment is REFUSED, not accepted. The string comes
|
|
171
|
+
* from whatever `gh` listed, and the audit uses this to decide whether a found
|
|
172
|
+
* copy is a stray. Failing open here would silence the finding for a copy
|
|
173
|
+
* sitting in an environment this repository has never heard of, which is
|
|
174
|
+
* exactly the case worth hearing about.
|
|
175
|
+
*/
|
|
176
|
+
export function acceptsKey(key, environment) {
|
|
177
|
+
return isGithubEnvironment(environment) && accepts(environment, key);
|
|
178
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** `GET /orgs/{org}/teams/{slug}` -> the team's numeric id. */
|
|
2
|
+
export declare function resolveTeamId(org: string, slug: string): Promise<number>;
|
|
3
|
+
/**
|
|
4
|
+
* `GET /orgs/{org}/installations` -> the id of the App installation whose
|
|
5
|
+
* `app_slug` matches, e.g. `devdogs-platform` (production) or
|
|
6
|
+
* `devdogs-platform-staging`.
|
|
7
|
+
*
|
|
8
|
+
* `orgs/{org}/installations` rather than the App's own `/app` endpoint: the
|
|
9
|
+
* latter needs App-JWT auth this CLI has no path to, while the installations
|
|
10
|
+
* list is a plain org-admin GET — the exact command DevDogsUGA's own
|
|
11
|
+
* `docs/platform/guides/identity/github-app.md` already tells an operator to
|
|
12
|
+
* run to verify the App's permission grant.
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveAppId(org: string, appSlug: string): Promise<number>;
|