@devdogsuga/backstage 0.1.2 → 0.1.4

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.
@@ -1,18 +1,22 @@
1
1
  import { C as discoverRepoRoot, T as cliName, b as ownPackageDir, c as reportDevtoolsError, f as isNoEnv, l as reportDevtoolsFailure, n as captureDevtoolsError, o as initDevtoolsTelemetry, p as isNonInteractive, w as findRepoRoot, x as ownVersion } from "./telemetry-Bjoz29Hl.js";
2
- import { _ as repoPeerUrl, a as recordEnteredTier, b as renderHelp, f as nonEmpty, h as loadEnvLoad, i as beginInvocation, l as dbPush, m as loadEnv, n as bareGroupStartPath, o as recordResolved, p as getEnvSync, r as runMenu, s as reproducibleCommand, t as catalog, u as dbPushDryRun, v as helpPath, y as renderCommandList } from "./catalog-BOBE4ee7.js";
2
+ import { a as recordEnteredTier, f as helpPath, i as beginInvocation, l as dbPush, m as renderHelp, n as bareGroupStartPath, o as recordResolved, p as renderCommandList, r as runMenu, s as reproducibleCommand, t as catalog, u as dbPushDryRun } from "./catalog-BuNLUu08.js";
3
+ import { a as repoPeerUrl, n as loadEnv, r as loadEnvLoad, t as getEnvSync } from "./peers-B0KDkI6a.js";
4
+ import { t as nonEmpty } from "./connection-D3XsOE3S.js";
3
5
  import { a as dryRunKind, l as wouldRunLine, o as isDryRun } from "./options-BTjOf5KP.js";
4
6
  import { a as explainError, i as explain, n as bail, o as unwrap, r as errorMessage } from "./ui-CdKo8mLw.js";
5
7
  import { n as positionals, t as flagValue } from "./args-Cjr_Iqts.js";
8
+ import { a as listSecrets$1, c as updateSecret, i as createSecret, o as projectIdFor, r as byKey, s as setExplicitAccessToken } from "./client-B7HhuTaA.js";
9
+ import { c as EnvDocument } from "./vault-C8LYJFOQ.js";
6
10
  import { t as DONE } from "./dispatch-D048O65I.js";
7
- import { createRequire, register } from "node:module";
11
+ import { register } from "node:module";
8
12
  import { pathToFileURL } from "node:url";
9
13
  import { appendFileSync, existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
10
- import { basename, dirname, join, resolve } from "node:path";
11
- import { confirm, intro, log, note, outro, password, select, spinner, text } from "@clack/prompts";
12
- import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
14
+ import { basename, join, resolve } from "node:path";
15
+ import { confirm, intro, log, note, outro, select } from "@clack/prompts";
16
+ import { chmod, readFile, writeFile } from "node:fs/promises";
13
17
  import { execFile, execFileSync, spawn } from "node:child_process";
14
18
  import { promisify } from "node:util";
15
- import { homedir, tmpdir } from "node:os";
19
+ import { tmpdir } from "node:os";
16
20
  import { z } from "zod";
17
21
  import { randomBytes } from "node:crypto";
18
22
  import postgres from "postgres";
@@ -1387,7 +1391,7 @@ function ambientValue(ambient, name) {
1387
1391
  * character fails at the far end of the pipeline, against a service that will
1388
1392
  * only say "unauthorized".
1389
1393
  */
1390
- function quote$1(key, value) {
1394
+ function quote(key, value) {
1391
1395
  if (value.startsWith("'") || value.endsWith("'")) throw new DeployError(`${key} begins or ends with a single quote, which cannot be quoted.`, ["Re-issue the credential, or wrap it (base64) at both ends."]);
1392
1396
  return `'${value}'`;
1393
1397
  }
@@ -1489,7 +1493,7 @@ function compose(options, env) {
1489
1493
  throw error;
1490
1494
  }
1491
1495
  else value = entry.value;
1492
- lines.push(`${key}=${quote$1(key, value)}`);
1496
+ lines.push(`${key}=${quote(key, value)}`);
1493
1497
  provenance.set(key, entry.from);
1494
1498
  }
1495
1499
  try {
@@ -1746,292 +1750,6 @@ async function runDeployCommand(rest) {
1746
1750
  }
1747
1751
  }
1748
1752
  //#endregion
1749
- //#region ../cli-core/src/env/document.ts
1750
- /**
1751
- * An editable `.env` that survives being edited.
1752
- *
1753
- * The root `.env` is not a data file. It is 150 lines of hard-won commentary
1754
- * about which value breaks the Supabase CLI when empty, which one must stay
1755
- * commented out, and why. A writer that parses to a Map and serializes back
1756
- * destroys all of it on the first save.
1757
- *
1758
- * So this keeps the file as **lines** and edits in place. An untouched line is
1759
- * returned byte-for-byte, including its spacing, quoting style and trailing
1760
- * comment. Only the lines actually being changed are rewritten.
1761
- *
1762
- * Nothing is ever deleted. Removing a key comments it out, so the value stays
1763
- * recoverable from the file itself, and re-adding it later uncomments that line
1764
- * rather than appending a duplicate.
1765
- */
1766
- /**
1767
- * `KEY=value`, active. Groups: indent, `export ` prefix, key, value.
1768
- *
1769
- * The prefix is CAPTURED rather than skipped so an update can put it back.
1770
- * Dropping it turns `export FOO=` into `FOO=`, which still parses here and
1771
- * stops being exported to child processes. That shows up as a missing variable
1772
- * three tools downstream.
1773
- */
1774
- const ACTIVE = /^(\s*)((?:export\s+)?)([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$/;
1775
- /** `# KEY=value`, the commented form this tool writes and reads back. */
1776
- const COMMENTED = /^(\s*)#\s?((?:export\s+)?)([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$/;
1777
- /**
1778
- * Reads the value half of an assignment.
1779
- *
1780
- * Deliberately NOT a general dotenv parser: it does not expand `$VAR`, because
1781
- * a stored value that means one thing in the file and another in the process
1782
- * cannot be rotated with confidence.
1783
- */
1784
- function parseValue(rest) {
1785
- const text = rest.trim();
1786
- if (text.startsWith("\"")) {
1787
- const end = findClosing(text, "\"");
1788
- if (end === -1) return unescape(text.slice(1));
1789
- return unescape(text.slice(1, end));
1790
- }
1791
- if (text.startsWith("'")) {
1792
- const end = findClosing(text, "'");
1793
- if (end === -1) return text.slice(1);
1794
- return text.slice(1, end);
1795
- }
1796
- const hash = text.search(/\s#/);
1797
- return (hash === -1 ? text : text.slice(0, hash)).trim();
1798
- }
1799
- function findClosing(text, quote) {
1800
- for (let i = 1; i < text.length; i += 1) {
1801
- if (text[i] === "\\") {
1802
- i += 1;
1803
- continue;
1804
- }
1805
- if (text[i] === quote) return i;
1806
- }
1807
- return -1;
1808
- }
1809
- function unescape(text) {
1810
- return text.replace(/\\([nrt\\"'])/g, (_, c) => c === "n" ? "\n" : c === "r" ? "\r" : c === "t" ? " " : c);
1811
- }
1812
- /**
1813
- * Splits everything after `=` into its value and its trailing comment.
1814
- *
1815
- * Quote-aware, because the naive "cut at the first `#`" truncates a generated
1816
- * password into something that still looks like one. A `#` inside quotes is
1817
- * part of the value; outside them it starts a comment only when whitespace
1818
- * precedes it.
1819
- */
1820
- function splitComment(rest) {
1821
- const text = rest.trimStart();
1822
- let after;
1823
- if (text.startsWith("\"") || text.startsWith("'")) {
1824
- const end = findClosing(text, text[0]);
1825
- after = end === -1 ? text.length : end + 1;
1826
- } else {
1827
- const hash = text.search(/\s#/);
1828
- after = hash === -1 ? text.length : hash;
1829
- }
1830
- const tail = text.slice(after);
1831
- const hash = tail.indexOf("#");
1832
- return { comment: hash === -1 ? "" : tail.slice(hash).trimEnd() };
1833
- }
1834
- /**
1835
- * Recognises a stamp this tool wrote, so it is replaced rather than duplicated.
1836
- *
1837
- * Deliberately narrow: it matches the exact bracketed shape below and nothing
1838
- * else, because the cost of a false positive is deleting somebody's own note.
1839
- */
1840
- const STAMP = /\s*#\s*\[[a-z][a-z0-9-]* (?:pushed|pulled) \d{4}-\d{2}-\d{2}\]\s*$/;
1841
- function stampText(stamp) {
1842
- return `# [${stamp.environment} ${stamp.action} ${stamp.date}]`;
1843
- }
1844
- function isStamp(comment) {
1845
- return STAMP.test(comment);
1846
- }
1847
- /** Always double-quoted, so a multi-line value round-trips through one line. */
1848
- function quote(value) {
1849
- return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t")}"`;
1850
- }
1851
- var EnvDocument = class EnvDocument {
1852
- lines;
1853
- /** Whether this session has already appended, so it separates only once. */
1854
- appended = false;
1855
- constructor(lines) {
1856
- this.lines = lines;
1857
- }
1858
- static parse(text) {
1859
- const lines = text.split("\n").map((raw) => {
1860
- const active = ACTIVE.exec(raw);
1861
- if (active) return {
1862
- raw,
1863
- key: active[3],
1864
- value: parseValue(active[4])
1865
- };
1866
- const commented = COMMENTED.exec(raw);
1867
- if (commented) return {
1868
- raw,
1869
- key: commented[3],
1870
- value: parseValue(commented[4]),
1871
- commented: true
1872
- };
1873
- return { raw };
1874
- });
1875
- return new EnvDocument(lines);
1876
- }
1877
- static empty() {
1878
- return new EnvDocument([]);
1879
- }
1880
- find(key, commented) {
1881
- return this.lines.findIndex((l) => l.key === key && Boolean(l.commented) === commented);
1882
- }
1883
- /** The active value, or undefined when absent or commented out. */
1884
- get(key) {
1885
- const i = this.find(key, false);
1886
- return i === -1 ? void 0 : this.lines[i].value;
1887
- }
1888
- /** Present and active. */
1889
- has(key) {
1890
- return this.find(key, false) !== -1;
1891
- }
1892
- /** Present, but commented out. */
1893
- isCommented(key) {
1894
- return this.find(key, false) === -1 && this.find(key, true) !== -1;
1895
- }
1896
- /** Every active assignment, in file order. */
1897
- entries() {
1898
- return this.lines.filter((l) => l.key !== void 0 && !l.commented).map((l) => [l.key, l.value]);
1899
- }
1900
- keys() {
1901
- return this.entries().map(([k]) => k);
1902
- }
1903
- /**
1904
- * Sets a value, preferring to revive a commented line over appending.
1905
- *
1906
- * Appending when a commented form already exists is how a file ends up with
1907
- * the same key twice: one stale, both plausible, and which one looks
1908
- * authoritative depends on where the reader scrolled to.
1909
- */
1910
- set(key, value, stamp) {
1911
- const active = this.find(key, false);
1912
- if (active !== -1) {
1913
- const line = this.lines[active];
1914
- const match = ACTIVE.exec(line.raw);
1915
- const existing = splitComment(match[4]).comment;
1916
- if (stamp && existing !== "" && !isStamp(existing)) this.lines.splice(this.first(key), 0, { raw: existing });
1917
- const trailing = stamp ? ` ${stampText(stamp)}` : existing === "" ? "" : ` ${existing}`;
1918
- const at = this.find(key, false);
1919
- const target = this.lines[at];
1920
- target.raw = `${match[1]}${match[2]}${key}=${quote(value)}${trailing}`;
1921
- target.value = value;
1922
- return;
1923
- }
1924
- const commented = this.find(key, true);
1925
- if (commented !== -1) {
1926
- const line = this.lines[commented];
1927
- line.raw = `${key}=${quote(value)}${stamp ? ` ${stampText(stamp)}` : ""}`;
1928
- line.value = value;
1929
- line.commented = false;
1930
- return;
1931
- }
1932
- if (!this.appended && this.lines.length > 0 && this.lines.at(-1).raw !== "") this.lines.push({ raw: "" });
1933
- this.appended = true;
1934
- this.lines.push({
1935
- raw: `${key}=${quote(value)}${stamp ? ` ${stampText(stamp)}` : ""}`,
1936
- key,
1937
- value
1938
- });
1939
- }
1940
- /** Index of the first line mentioning a key, active or commented. */
1941
- first(key) {
1942
- return this.lines.findIndex((l) => l.key === key);
1943
- }
1944
- /**
1945
- * Moves every line for a key next to that key's first appearance.
1946
- *
1947
- * Files drift: a key gets commented out near the bottom, re-added at the top
1948
- * six weeks later, and now two lines claim the same name a hundred lines
1949
- * apart. Whichever one a reader scrolls to first looks authoritative.
1950
- *
1951
- * The FIRST occurrence keeps its position, so the standalone comment block
1952
- * documenting a key stays attached to it. Everything else moves up to join
1953
- * it, in the order it already had.
1954
- */
1955
- group() {
1956
- const out = [];
1957
- const taken = /* @__PURE__ */ new Set();
1958
- let moved = false;
1959
- for (let i = 0; i < this.lines.length; i += 1) {
1960
- if (taken.has(i)) continue;
1961
- const line = this.lines[i];
1962
- out.push(line);
1963
- taken.add(i);
1964
- if (line.key === void 0) continue;
1965
- for (let j = i + 1; j < this.lines.length; j += 1) {
1966
- if (taken.has(j) || this.lines[j].key !== line.key) continue;
1967
- if (j !== i + 1 || taken.has(i + 1)) moved = true;
1968
- out.push(this.lines[j]);
1969
- taken.add(j);
1970
- }
1971
- }
1972
- this.lines = out;
1973
- return moved;
1974
- }
1975
- /**
1976
- * Blanks every active value without losing one.
1977
- *
1978
- * Each becomes a commented line holding what it was, plus an empty active
1979
- * line under it. The file still declares every key it needs, which is what
1980
- * makes it a usable checklist, while holding nothing.
1981
- *
1982
- * Already-empty keys are skipped. Commenting out `FOO=""` to write `FOO=""`
1983
- * underneath is churn that makes the next diff harder to read.
1984
- */
1985
- reset() {
1986
- const out = [];
1987
- const cleared = [];
1988
- for (const line of this.lines) {
1989
- if (line.key === void 0 || line.commented || line.value === "") {
1990
- out.push(line);
1991
- continue;
1992
- }
1993
- const exported = ACTIVE.exec(line.raw)?.[2] ?? "";
1994
- line.raw = `# ${line.raw.trimStart()}`;
1995
- line.commented = true;
1996
- out.push(line);
1997
- out.push({
1998
- raw: `${exported}${line.key}=""`,
1999
- key: line.key,
2000
- value: ""
2001
- });
2002
- cleared.push(line.key);
2003
- }
2004
- this.lines = out;
2005
- return cleared;
2006
- }
2007
- /**
2008
- * Comments a key out rather than deleting it.
2009
- *
2010
- * A no-op when it is already commented or absent, so running a sync twice
2011
- * cannot produce `## KEY=`.
2012
- */
2013
- comment(key) {
2014
- const i = this.find(key, false);
2015
- if (i === -1) return false;
2016
- const line = this.lines[i];
2017
- line.raw = `# ${line.raw.trimStart()}`;
2018
- line.commented = true;
2019
- return true;
2020
- }
2021
- /** Restores a commented assignment. Returns its value, or undefined. */
2022
- uncomment(key) {
2023
- const i = this.find(key, true);
2024
- if (i === -1) return void 0;
2025
- const line = this.lines[i];
2026
- line.raw = line.raw.replace(/^(\s*)#\s?/, "$1");
2027
- line.commented = false;
2028
- return line.value;
2029
- }
2030
- toString() {
2031
- return this.lines.map((l) => l.raw).join("\n");
2032
- }
2033
- };
2034
- //#endregion
2035
1753
  //#region ../cli-core/src/env/files.ts
2036
1754
  /**
2037
1755
  * Reading and writing a target's env file, shared by every command that edits
@@ -2217,665 +1935,6 @@ async function resolveVaultTarget(given, message) {
2217
1935
  }));
2218
1936
  }
2219
1937
  //#endregion
2220
- //#region src/bws/token.ts
2221
- /**
2222
- * Finding the Secrets Manager access token, in four places, in order.
2223
- *
2224
- * 1. `--access-token` explicit, and explicit beats ambient
2225
- * 2. `BWS_ACCESS_TOKEN` the environment, which includes your `.env`,
2226
- * since `with-env` loads it for every command
2227
- * 3. the Bitwarden Password Manager vault, via the bundled `bw`
2228
- * (`env` signs in and unlocks it for itself)
2229
- * 4. ask, and offer to save it to `.env` (the default) or the vault, so
2230
- * this is the last time
2231
- *
2232
- * The ordering logic is separated from the four implementations because it is
2233
- * the part that can be wrong invisibly. A chain that silently prefers a stale
2234
- * environment variable over a rotated vault entry authenticates as the wrong
2235
- * account and reports nothing, so the order is asserted rather than assumed.
2236
- */
2237
- var NoAccessTokenError = class extends Error {};
2238
- async function resolveToken(sources) {
2239
- const announce = sources.onSource ?? (() => void 0);
2240
- if (sources.explicit) {
2241
- announce("flag");
2242
- return sources.explicit;
2243
- }
2244
- if (sources.env) {
2245
- announce("environment");
2246
- return sources.env;
2247
- }
2248
- const stored = await sources.fromVault();
2249
- if (stored) {
2250
- announce("vault");
2251
- return stored;
2252
- }
2253
- const typed = await sources.prompt();
2254
- if (!typed) throw new NoAccessTokenError("No access token. Put BWS_ACCESS_TOKEN in your .env (an interactive run offers to do this for you), pass --access-token, or store one in your Bitwarden vault. See docs/toolkit/guides/env/commands.md.");
2255
- announce("prompt");
2256
- if (await sources.offerSave()) try {
2257
- await sources.save(typed);
2258
- } catch (err) {
2259
- log.warn(`Could not save to your vault: ${err instanceof Error ? err.message : String(err)}. Continuing with the token you typed.`);
2260
- }
2261
- return typed;
2262
- }
2263
- /**
2264
- * A weak shape check, to turn a paste error into a sentence rather than a 401.
2265
- *
2266
- * Machine account tokens are `0.<uuid>.<secret>:<secret>`. This checks the
2267
- * leading version segment only, and **warns rather than refuses**: the format
2268
- * is Bitwarden's to change, and a tool that rejects a valid token because its
2269
- * prefix moved is worse than one that passes a bad token to `bws` and lets the
2270
- * real error surface.
2271
- */
2272
- function looksLikeAccessToken(value) {
2273
- return /^\d+\.[0-9a-f-]{36}\./i.test(value.trim());
2274
- }
2275
- /** The interactive half. Masked, and never echoed back. */
2276
- async function promptForToken() {
2277
- if (!process.stdin.isTTY) return void 0;
2278
- const typed = unwrap(await password({
2279
- message: "Paste the Secrets Manager access token for the `admin` account",
2280
- mask: "•",
2281
- validate: (v) => (v ?? "").trim() === "" ? "An access token is required." : void 0
2282
- })).trim();
2283
- if (!looksLikeAccessToken(typed)) log.warn("That does not look like a machine account access token (they start `0.<uuid>.`). Continuing anyway — if it is wrong, Bitwarden will say so.");
2284
- return typed;
2285
- }
2286
- //#endregion
2287
- //#region src/bws/retry.ts
2288
- /**
2289
- * Retrying exactly one failure class: HTTP 429, rate limiting.
2290
- *
2291
- * A 429 is the server saying "correct request, wrong minute", the only error
2292
- * where trying the same thing again IS the fix. Everything else (bad token,
2293
- * missing project, malformed input) re-throws untouched on the first attempt,
2294
- * because retrying those just triples the time to the real message.
2295
- *
2296
- * Written for the Secrets Manager calls, where the minute matters: a push is
2297
- * ~45 sequential writes plus a login, several targets get pushed back-to-back,
2298
- * and Bitwarden's identity endpoint rate-limits repeated access-token logins
2299
- * aggressively. The login half is also fixed at the source, since the client
2300
- * now caches its login in a state file, so the retry is the belt on that
2301
- * suspender.
2302
- */
2303
- function isRateLimited(err) {
2304
- const message = err instanceof Error ? err.message : String(err);
2305
- return /\b429\b|too many requests|rate.?limit/i.test(message);
2306
- }
2307
- const wait$1 = (ms) => new Promise((r) => setTimeout(r, ms));
2308
- async function withRateLimitRetry(op, { retries = 3, baseMs = 5e3, sleep = wait$1, doing }) {
2309
- for (let attempt = 0;; attempt++) try {
2310
- return await op();
2311
- } catch (err) {
2312
- if (!isRateLimited(err) || attempt >= retries) throw err;
2313
- const ms = baseMs * 2 ** attempt;
2314
- log.warn(`Bitwarden rate-limited ${doing} (HTTP 429). Waiting ${ms / 1e3}s and retrying (${attempt + 1}/${retries})…`);
2315
- await sleep(ms);
2316
- }
2317
- }
2318
- //#endregion
2319
- //#region src/bws/pace.ts
2320
- const WRITE_GAP_MS = 1100;
2321
- const wait = (ms) => new Promise((r) => setTimeout(r, ms));
2322
- function makePacer(now = Date.now, sleep = wait) {
2323
- let notBefore = 0;
2324
- let chain = Promise.resolve();
2325
- return (kind) => {
2326
- const gap = kind === "write" ? WRITE_GAP_MS : 350;
2327
- const turn = chain.then(async () => {
2328
- const pause = notBefore - now();
2329
- if (pause > 0) await sleep(pause);
2330
- notBefore = now() + gap;
2331
- });
2332
- chain = turn.catch(() => void 0);
2333
- return turn;
2334
- };
2335
- }
2336
- //#endregion
2337
- //#region src/bws/store.ts
2338
- /**
2339
- * Saving a prompted value into the developer's own `.env`.
2340
- *
2341
- * The development file only, by construction: the path is fixed to
2342
- * `fileFor("development")`, so no caller can point this at a vault target's
2343
- * file. Those are what `env push` uploads, and the values saved here
2344
- * (`BWS_ACCESS_TOKEN`, `BWS_ORG_ID`) exist so they never ride a push. The
2345
- * token is refused there by name anyway; this keeps the write from ever being
2346
- * the thing that needs refusing.
2347
- *
2348
- * `EnvDocument.set` revives the commented line the rendered file already
2349
- * carries (never-store keys ship as `# KEY=""` with their documentation), so a
2350
- * saved value lands under its own doc comment rather than appended bare.
2351
- */
2352
- async function saveToDevEnv(key, value) {
2353
- const { fileFor } = await loadEnv();
2354
- const path = resolve(findRepoRoot(), fileFor("development"));
2355
- let doc;
2356
- try {
2357
- doc = EnvDocument.parse(await readFile(path, "utf8"));
2358
- } catch {
2359
- doc = EnvDocument.empty();
2360
- }
2361
- doc.set(key, value);
2362
- await writeFile(path, doc.toString());
2363
- }
2364
- //#endregion
2365
- //#region src/bws/bw.ts
2366
- /**
2367
- * Where the bundled Bitwarden CLI (`bw`) is, for the vault code that signs in
2368
- * and reads the Secrets Manager token (`vault.ts`).
2369
- *
2370
- * There is no `bw` command of ours: `env` runs `bw login` and `bw unlock`
2371
- * itself when it needs the token. The binary is still resolved from
2372
- * `@bitwarden/cli`, never looked up on PATH. A dependency's bin is only linked
2373
- * into the `node_modules/.bin` of the package that depends on it, so under
2374
- * `pnpm dlx` nothing puts `bw` on PATH and a bare `spawn("bw")` is ENOENT.
2375
- */
2376
- /**
2377
- * The `[command, args]` pair that runs the bundled Bitwarden CLI.
2378
- *
2379
- * Runs `build/bw.js` under this Node rather than through a shim, so it works
2380
- * the same wherever backstage is installed. Falls back to `bw` on PATH when the
2381
- * package cannot be resolved, which keeps a missing install surfacing as the
2382
- * ENOENT every caller already handles.
2383
- */
2384
- function bwCommand(args) {
2385
- try {
2386
- const require_ = createRequire(import.meta.url);
2387
- const pkgPath = require_.resolve("@bitwarden/cli/package.json");
2388
- const pkg = require_(pkgPath);
2389
- return [process.execPath, [join(dirname(pkgPath), pkg.bin.bw), ...args]];
2390
- } catch {
2391
- return ["bw", args];
2392
- }
2393
- }
2394
- //#endregion
2395
- //#region src/bws/vault.ts
2396
- /**
2397
- * The Bitwarden **Password Manager** vault, via the `bw` CLI.
2398
- *
2399
- * A different product from Secrets Manager and a different CLI, which is the
2400
- * point: `bws` has no user authentication at all, so the access token it needs
2401
- * has to be held somewhere a person can log in to. That somewhere is the
2402
- * Password Manager vault, and this module reads it back rather than making
2403
- * somebody paste a token every session.
2404
- *
2405
- * Everything here degrades to `undefined`. A missing `bw`, a locked vault, a
2406
- * declined unlock, a missing item: none of them are errors, because each has a
2407
- * good fallback, which is to ask. Throwing would turn a convenience into a
2408
- * prerequisite.
2409
- *
2410
- * ⚠️ The token never passes through argv in either direction. Reads use
2411
- * `bw get password <search>`, which puts only the item NAME on the command
2412
- * line, and writes pipe base64 JSON through **stdin**, which `bw create item`
2413
- * documents as an accepted input.
2414
- */
2415
- const run$5 = promisify(execFile);
2416
- /**
2417
- * The item this looks for, and creates.
2418
- *
2419
- * Named for what it unlocks rather than for this tool, because whoever finds it
2420
- * in the vault six months from now needs to know what it is, not which script
2421
- * wrote it.
2422
- */
2423
- const VAULT_ITEM_NAME = "DevDogs Secrets Manager access token (admin)";
2424
- /** `bw status`, or `unavailable` when the CLI is not installed. */
2425
- async function vaultStatus() {
2426
- try {
2427
- const { stdout } = await run$5(...bwCommand(bwArgs(["status", "--response"])), { shell: false });
2428
- const parsed = JSON.parse(stdout);
2429
- const status = ("status" in parsed ? parsed.status : void 0) ?? ("data" in parsed ? parsed.data?.template?.status : void 0);
2430
- if (status === "unlocked") return "unlocked";
2431
- if (status === "locked") return "locked";
2432
- return "unauthenticated";
2433
- } catch (err) {
2434
- if (err.code === "ENOENT") return "unavailable";
2435
- return "unauthenticated";
2436
- }
2437
- }
2438
- /**
2439
- * Offers to sign in to the vault when it is not, and does it with the bundled
2440
- * `bw` (`bw login`, interactive). There is no `bw` command of ours to run by
2441
- * hand any more: `env` signs in and unlocks for itself, at the moment it needs
2442
- * the Secrets Manager token. Returns the status afterwards.
2443
- *
2444
- * Asks first, like the unlock below: a master-password prompt that appears
2445
- * unannounced is shaped exactly like the thing people are told never to type
2446
- * their master password into. Without a terminal there is nobody to sign in,
2447
- * so the status comes back unchanged.
2448
- */
2449
- async function signInIfNeeded(status) {
2450
- if (status !== "unauthenticated" || !process.stdin.isTTY) return status;
2451
- if (!unwrap(await confirm({
2452
- message: "You are not signed in to Bitwarden. Sign in now to look for the access token?",
2453
- initialValue: true
2454
- }))) return status;
2455
- if (await new Promise((resolve) => {
2456
- const child = spawn(...bwCommand(["login"]), {
2457
- stdio: "inherit",
2458
- shell: false
2459
- });
2460
- child.on("error", () => resolve(null));
2461
- child.on("close", (exit) => resolve(exit));
2462
- }) !== 0) return status;
2463
- return vaultStatus();
2464
- }
2465
- /**
2466
- * A usable session key, unlocking if the person agrees.
2467
- *
2468
- * `BW_SESSION` is preferred and silent. Otherwise this ASKS before unlocking:
2469
- * a tool that pops a master-password prompt unannounced is shaped exactly like
2470
- * the thing people are told never to type their master password into.
2471
- *
2472
- * The password is typed straight into `bw`. stdin is inherited, so it never
2473
- * passes through this process.
2474
- */
2475
- async function session(status) {
2476
- if (process.env.BW_SESSION) return process.env.BW_SESSION;
2477
- if (status !== "locked") return void 0;
2478
- if (!process.stdin.isTTY) return void 0;
2479
- if (!unwrap(await confirm({
2480
- message: "Your Bitwarden vault is locked. Unlock it to look for the access token?",
2481
- initialValue: true
2482
- }))) return void 0;
2483
- return new Promise((resolve) => {
2484
- const child = spawn(...bwCommand(["unlock", "--raw"]), {
2485
- stdio: [
2486
- "inherit",
2487
- "pipe",
2488
- "inherit"
2489
- ],
2490
- shell: false
2491
- });
2492
- let out = "";
2493
- child.stdout.on("data", (c) => out += c.toString());
2494
- child.on("error", () => resolve(void 0));
2495
- child.on("close", (code) => resolve(code === 0 && out.trim() !== "" ? out.trim() : void 0));
2496
- });
2497
- }
2498
- /**
2499
- * Adds the session key, and `--nointeraction` always.
2500
- *
2501
- * The second one is not belt-and-braces. `bw` prompts for a master password on
2502
- * stdin whenever the vault is locked, as `bw get template item` does against a
2503
- * locked vault, and this module runs `bw` with piped stdio, where that prompt
2504
- * has nobody to answer it and hangs until something gives up. `--nointeraction`
2505
- * turns that into an immediate non-zero exit, which every caller here already
2506
- * treats as "not available, ask instead".
2507
- */
2508
- function bwArgs(args, key) {
2509
- const withNoInteraction = [...args, "--nointeraction"];
2510
- return key ? [
2511
- ...withNoInteraction,
2512
- "--session",
2513
- key
2514
- ] : withNoInteraction;
2515
- }
2516
- /**
2517
- * The stored token, or `undefined` for every reason it might not be there.
2518
- *
2519
- * `bw get password` takes a search term and fails when it matches more than one
2520
- * item, which is the behaviour worth having: two items called something like
2521
- * this means somebody should look, not that this should guess.
2522
- */
2523
- async function readTokenFromVault() {
2524
- const status = await signInIfNeeded(await vaultStatus());
2525
- if (status === "unavailable" || status === "unauthenticated") {
2526
- log.info(explainVault(status));
2527
- return;
2528
- }
2529
- const key = await session(status);
2530
- if (status === "locked" && !key) return void 0;
2531
- const s = spinner();
2532
- s.start("Looking in your Bitwarden vault");
2533
- try {
2534
- const { stdout } = await run$5(...bwCommand(bwArgs([
2535
- "get",
2536
- "password",
2537
- VAULT_ITEM_NAME,
2538
- "--raw"
2539
- ], key)), { shell: false });
2540
- const token = stdout.trim();
2541
- if (token === "") {
2542
- s.stop("Nothing stored in the vault yet");
2543
- return;
2544
- }
2545
- s.stop(`Read the access token from your vault ("${VAULT_ITEM_NAME}")`);
2546
- return token;
2547
- } catch {
2548
- s.stop("No single matching item in your vault");
2549
- return;
2550
- }
2551
- }
2552
- /**
2553
- * Creates the item, with the token arriving on **stdin** as base64 JSON.
2554
- *
2555
- * `bw create item` documents an encoded-JSON positional AND stdin. Using stdin
2556
- * is the difference between a live credential that is invisible and one that
2557
- * sits in `ps` output for the length of the call.
2558
- */
2559
- async function saveTokenToVault(token) {
2560
- const status = await signInIfNeeded(await vaultStatus());
2561
- if (status === "unavailable" || status === "unauthenticated") return false;
2562
- const key = await session(status);
2563
- if (status === "locked" && !key) return false;
2564
- const item = {
2565
- organizationId: null,
2566
- collectionIds: null,
2567
- folderId: null,
2568
- type: 1,
2569
- name: VAULT_ITEM_NAME,
2570
- notes: "Bitwarden Secrets Manager access token for the `admin` machine account, read/write on preflight, staging and production.\n\nRead automatically by `backstage env`. Never put this in a .env file: it unlocks all three projects, and the tool refuses to upload it.",
2571
- favorite: false,
2572
- reprompt: 0,
2573
- login: {
2574
- username: null,
2575
- password: token,
2576
- totp: null
2577
- }
2578
- };
2579
- return new Promise((resolve) => {
2580
- const child = spawn(...bwCommand(bwArgs(["create", "item"], key)), {
2581
- stdio: [
2582
- "pipe",
2583
- "ignore",
2584
- "pipe"
2585
- ],
2586
- shell: false
2587
- });
2588
- let stderr = "";
2589
- child.stderr.on("data", (c) => stderr += c.toString());
2590
- child.on("error", () => resolve(false));
2591
- child.on("close", (code) => {
2592
- if (code !== 0 && stderr.trim() !== "") log.warn(stderr.trim());
2593
- resolve(code === 0);
2594
- });
2595
- child.stdin.end(Buffer.from(JSON.stringify(item)).toString("base64"));
2596
- });
2597
- }
2598
- /** Why the vault could not be used, phrased as something to do about it. */
2599
- function explainVault(status) {
2600
- if (status === "unavailable") return "The `bw` CLI is not installed, so the vault could not be checked. It ships with backstage, so reinstall it.";
2601
- if (status === "unauthenticated") return "You are not signed in to Bitwarden, and with no terminal there is nobody to sign in. Set BWS_ACCESS_TOKEN instead.";
2602
- }
2603
- //#endregion
2604
- //#region src/bws/client.ts
2605
- /**
2606
- * Secrets Manager, through the official SDK. No `bws` binary to install.
2607
- *
2608
- * This wrapped the `bws` CLI until 2026-08-19. The CLI was the right call
2609
- * against the raw REST API: Secrets Manager is end-to-end encrypted, the
2610
- * server stores ciphertext, and the client derives the key that opens it from
2611
- * the access token, so `fetch` returns blobs and reimplementing the crypto is
2612
- * not a trade worth making. `@bitwarden/sdk-napi` is that same client-side
2613
- * crypto (the same Rust core the CLI wraps), loaded in-process, which buys two
2614
- * things the CLI could not:
2615
- *
2616
- * * **Nothing to install.** The SDK is a dependency of this package, so
2617
- * `pnpm install` is the whole setup. No "install bws from the releases
2618
- * page" step, and no version somebody's laptop drifted on.
2619
- * * **No credential in argv.** `bws secret create` took the VALUE as a
2620
- * positional argument, visible to `ps` for the length of the call, a
2621
- * documented property of the tool this wrapper could only apologize for.
2622
- * In-process values never touch a process table.
2623
- *
2624
- * ⚠️ Imported LAZILY, at the first real call. The SDK is a native module, and
2625
- * loading it at import time would tax every `cli:no-env` path, the CI guards
2626
- * included, with a `.node` binary none of them use.
2627
- *
2628
- * The one thing the SDK needs that the CLI did not: the ORGANIZATION ID. The
2629
- * CLI derived it from the access token's login response; the SDK's every list
2630
- * and create takes it as an argument, and nothing in its API discovers it. It
2631
- * is a public identifier (a UUID that confers nothing), read from `BWS_ORG_ID`.
2632
- * See the declaration in `packages/devtools/env.ts`.
2633
- */
2634
- var BwsError = class extends Error {};
2635
- /**
2636
- * One pacer for the process: every Secrets Manager request funnels through
2637
- * it, spaced under the published per-IP limits so the 429 retry stays a
2638
- * backstop instead of the plan. See `pace.ts` for the numbers and sources.
2639
- */
2640
- const pace = makePacer();
2641
- let explicitToken;
2642
- let resolved;
2643
- /** Where a just-typed token goes, picked in `offerSave`, used in `save`. */
2644
- let saveDestination = "no";
2645
- /** Records `--access-token`, before any command runs. */
2646
- function setExplicitAccessToken(token) {
2647
- explicitToken = token;
2648
- resolved = void 0;
2649
- }
2650
- /**
2651
- * The access token, found once per process.
2652
- *
2653
- * Memoized as a **promise**, not a value: resolution can prompt, and a single
2654
- * command makes several Secrets Manager calls. Caching the value alone would
2655
- * still let two concurrent calls open two prompts over each other.
2656
- *
2657
- * Whatever the source, the token reaches the SDK as a function argument in
2658
- * this process, never argv, never a child's environment.
2659
- */
2660
- async function accessToken() {
2661
- resolved ??= resolveToken({
2662
- explicit: explicitToken,
2663
- env: process.env.BWS_ACCESS_TOKEN,
2664
- fromVault: readTokenFromVault,
2665
- prompt: promptForToken,
2666
- offerSave: async () => {
2667
- saveDestination = unwrap(await select({
2668
- message: "Save it, so this is the last time you paste it?",
2669
- options: [
2670
- {
2671
- value: "env",
2672
- label: "yes — into .env",
2673
- hint: "gitignored, this machine only; push refuses it by name"
2674
- },
2675
- {
2676
- value: "vault",
2677
- label: "yes — into my Bitwarden vault",
2678
- hint: "needs `pnpm devtools bw login`; follows you across machines"
2679
- },
2680
- {
2681
- value: "no",
2682
- label: "no — ask me again next time"
2683
- }
2684
- ],
2685
- initialValue: "env"
2686
- }));
2687
- return saveDestination !== "no";
2688
- },
2689
- save: async (token) => {
2690
- if (saveDestination === "env") {
2691
- await saveToDevEnv("BWS_ACCESS_TOKEN", token);
2692
- log.success("Stored in .env — with-env loads it on every run.");
2693
- return;
2694
- }
2695
- if (await saveTokenToVault(token)) log.success(`Stored as "${VAULT_ITEM_NAME}" in your Bitwarden vault.`);
2696
- else log.warn("Could not save it. The command will continue with the token you typed; nothing was written to your vault.");
2697
- },
2698
- onSource: (source) => {
2699
- if (source === "flag") log.warn("--access-token puts a live credential in argv, where `ps` can read it for the length of the call, and in your shell history. Prefer BWS_ACCESS_TOKEN or the vault.");
2700
- }
2701
- }).catch((err) => {
2702
- resolved = void 0;
2703
- throw err instanceof NoAccessTokenError ? new BwsError(err.message) : err;
2704
- });
2705
- return resolved;
2706
- }
2707
- let resolvedOrgId;
2708
- /**
2709
- * The organization the machine account belongs to: the environment, else a
2710
- * prompt with an offer to save.
2711
- *
2712
- * Prompted rather than only refused because it is the one Secrets Manager
2713
- * input with nothing secret about it: a public UUID that identifies without
2714
- * authorizing. Where nobody can answer (a pipe), the named refusal below is
2715
- * the answer, because an SDK error about a malformed UUID would send somebody
2716
- * debugging the token, which is the one thing that is fine.
2717
- */
2718
- function organizationId() {
2719
- resolvedOrgId ??= (async () => {
2720
- const fromEnv = process.env.BWS_ORG_ID;
2721
- if (fromEnv) return fromEnv;
2722
- if (!process.stdin.isTTY) throw new BwsError("BWS_ORG_ID is not set. The Secrets Manager SDK addresses everything by organization id — a public UUID, shown in the Secrets Manager URL (bitwarden.com/#/sm/<org-id>/...) and on the machine-account page. Put it in your .env; it identifies, it does not authorize.");
2723
- const typed = unwrap(await text({
2724
- message: "What is the Bitwarden organization id? (the UUID in the Secrets Manager URL: bitwarden.com/#/sm/<org-id>/...)",
2725
- validate: (v) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test((v ?? "").trim()) ? void 0 : "That is not a UUID."
2726
- })).trim();
2727
- try {
2728
- await saveToDevEnv("BWS_ORG_ID", typed);
2729
- log.success("Stored BWS_ORG_ID in .env.");
2730
- } catch (err) {
2731
- log.warn(`Could not save it to .env: ${err instanceof Error ? err.message : String(err)}. Continuing with the id you typed.`);
2732
- }
2733
- return typed;
2734
- })();
2735
- resolvedOrgId.catch(() => {
2736
- resolvedOrgId = void 0;
2737
- });
2738
- return resolvedOrgId;
2739
- }
2740
- /** The lazily-loaded, logged-in SDK client, one per process. */
2741
- let sdk;
2742
- /**
2743
- * Where the SDK caches its login, so a process is not a fresh authentication.
2744
- *
2745
- * ⚠️ This file is what stops the 429s. Bitwarden's identity endpoint
2746
- * rate-limits repeated access-token logins hard, and "push preflight, audit,
2747
- * push staging, audit, push production, audit" is six devtools processes in
2748
- * two minutes: six logins without this, ONE with it. The `bws` binary kept a
2749
- * state directory for exactly this reason, and the SDK migration dropped it.
2750
- * Per machine account (the token's client-id UUID is in the filename, public
2751
- * by format), under 0700 directories, and outside the repository, because it
2752
- * caches an auth token and must live where nothing syncs or commits it.
2753
- */
2754
- function stateFileFor(accessToken) {
2755
- const clientId = /^\d+\.([0-9a-f-]{36})\./i.exec(accessToken)?.[1] ?? "default";
2756
- return join(homedir(), ".config", "devdogs-devtools", `bws-state-${clientId}.json`);
2757
- }
2758
- async function client() {
2759
- sdk ??= (async () => {
2760
- const token = await accessToken();
2761
- const { BitwardenClient } = await import("@bitwarden/sdk-napi");
2762
- const instance = new BitwardenClient();
2763
- try {
2764
- const stateFile = stateFileFor(token);
2765
- await mkdir(dirname(stateFile), {
2766
- recursive: true,
2767
- mode: 448
2768
- });
2769
- await withRateLimitRetry(async () => {
2770
- await pace("write");
2771
- return instance.auth().loginAccessToken(token, stateFile);
2772
- }, { doing: "the login" });
2773
- } catch (err) {
2774
- throw new BwsError(describeSdkFailure(err, "logging in"));
2775
- }
2776
- return instance;
2777
- })();
2778
- sdk.catch(() => {
2779
- sdk = void 0;
2780
- });
2781
- return sdk;
2782
- }
2783
- /**
2784
- * Turns an SDK failure into something with a next step in it.
2785
- *
2786
- * The two that actually happen are a rejected token and a token that does not
2787
- * cover the project. The latter surfaces as a bare "not found", because a
2788
- * project you cannot see and a project that does not exist are deliberately
2789
- * the same answer.
2790
- */
2791
- function describeSdkFailure(err, doing) {
2792
- const message = err instanceof Error ? err.message : String(err);
2793
- if (/\b429\b|too many requests/i.test(message)) return `${message}\n\nBitwarden rate-limited this even after the automatic retries. Wait a minute and re-run — the login is cached in a state file now, so re-running does not spend another authentication.`;
2794
- if (/404|not.?found/i.test(message)) return `${message}\n\nA 404 here usually means the access token is valid but its machine account is not assigned to this project — Secrets Manager reports 'invisible' and 'absent' identically.`;
2795
- if (/401|unauthoriz|invalid|expired/i.test(message)) return `${message}\n\nThe access token was rejected while ${doing}. It may have been revoked or belong to another organization.`;
2796
- return message || `Secrets Manager failed with no message while ${doing}.`;
2797
- }
2798
- function iso(value) {
2799
- if (value instanceof Date) return value.toISOString();
2800
- if (typeof value === "string" && value !== "") return value;
2801
- }
2802
- /** Resolves a project name to its id. Names are unique within an org. */
2803
- async function projectIdFor(name) {
2804
- const c = await client();
2805
- const org = await organizationId();
2806
- let projects;
2807
- try {
2808
- projects = (await withRateLimitRetry(async () => {
2809
- await pace("read");
2810
- return c.projects().list(org);
2811
- }, { doing: "listing projects" })).data;
2812
- } catch (err) {
2813
- throw new BwsError(describeSdkFailure(err, "listing projects"));
2814
- }
2815
- const match = projects.find((p) => p.name === name);
2816
- if (!match) {
2817
- const visible = projects.map((p) => p.name).sort();
2818
- throw new BwsError(`No project named "${name}".\n` + (visible.length > 0 ? `This token can see: ${visible.join(", ")}` : "This token can see no projects at all, which usually means the machine account has no project assignments yet."));
2819
- }
2820
- return match.id;
2821
- }
2822
- async function listSecrets$1(projectId) {
2823
- const c = await client();
2824
- const org = await organizationId();
2825
- try {
2826
- const identifiers = (await withRateLimitRetry(async () => {
2827
- await pace("read");
2828
- return c.secrets().list(org);
2829
- }, { doing: "listing secrets" })).data;
2830
- if (identifiers.length === 0) return [];
2831
- return (await withRateLimitRetry(async () => {
2832
- await pace("read");
2833
- return c.secrets().getByIds(identifiers.map((i) => i.id));
2834
- }, { doing: "reading secrets" })).data.filter((s) => s.projectId === projectId).map((s) => ({
2835
- id: s.id,
2836
- key: s.key,
2837
- value: s.value,
2838
- note: s.note,
2839
- projectId,
2840
- revisionDate: iso(s.revisionDate)
2841
- }));
2842
- } catch (err) {
2843
- throw new BwsError(describeSdkFailure(err, "listing secrets"));
2844
- }
2845
- }
2846
- async function createSecret(projectId, key, value, note) {
2847
- const c = await client();
2848
- const org = await organizationId();
2849
- try {
2850
- await withRateLimitRetry(async () => {
2851
- await pace("write");
2852
- return c.secrets().create(org, key, value, note, [projectId]);
2853
- }, { doing: `creating ${key}` });
2854
- } catch (err) {
2855
- throw new BwsError(describeSdkFailure(err, `creating ${key}`));
2856
- }
2857
- }
2858
- /**
2859
- * Takes the whole secret rather than an id: the SDK's update is a full
2860
- * replace (organization, key, project list included), and every caller
2861
- * already holds the listed secret it is updating.
2862
- */
2863
- async function updateSecret(secret, value, note) {
2864
- const c = await client();
2865
- const org = await organizationId();
2866
- try {
2867
- await withRateLimitRetry(async () => {
2868
- await pace("write");
2869
- return c.secrets().update(org, secret.id, secret.key, value, note, [secret.projectId]);
2870
- }, { doing: `updating ${secret.key}` });
2871
- } catch (err) {
2872
- throw new BwsError(describeSdkFailure(err, `updating ${secret.key}`));
2873
- }
2874
- }
2875
- function byKey(secrets) {
2876
- return new Map(secrets.map((s) => [s.key, s]));
2877
- }
2878
- //#endregion
2879
1938
  //#region src/gh/client.ts
2880
1939
  /**
2881
1940
  * A thin wrapper over the `gh` CLI, for environment secrets and variables.
@@ -4118,7 +3177,8 @@ async function runEnvAudit(options) {
4118
3177
  const spec = environmentSpecs()[target];
4119
3178
  const path = pathFor(target, options.file);
4120
3179
  const doc = await readDocument(path);
4121
- const bwsSecrets = await listSecrets$1(await projectIdFor(spec.project));
3180
+ const projectId = await projectIdFor(spec.project);
3181
+ const bwsSecrets = await listSecrets$1(projectId);
4122
3182
  const github = [];
4123
3183
  const githubVariables = [];
4124
3184
  const unreachable = [];
@@ -4462,107 +3522,107 @@ async function resolveAppId(org, appSlug) {
4462
3522
  }
4463
3523
  //#endregion
4464
3524
  //#region src/github/rulesets/desired.ts
4465
- /**
4466
- * Canonical fixed-ruleset name -> earlier name(s) this reconciler recognizes
4467
- * as the SAME slot, so the first run renames rather than creating a
4468
- * duplicate that leaves the old one live and unmanaged.
4469
- *
4470
- * Only `main` has a legacy alias today (`main-protection`, live on GitHub as
4471
- * of 2026-09-24). `production`, `~ALL` and `team/**` have no live ruleset to
4472
- * rename — they are created fresh. The tag ruleset keeps its live name,
4473
- * `tag-protection`, unchanged; nothing about its shape is wrong today, so
4474
- * there is no rename to make.
4475
- */
4476
- const LEGACY_NAME_ALIASES = { main: ["main-protection"] };
4477
- /** Ruleset names this reconciler deletes outright — nothing in the desired set replaces them. */
4478
- const DELETE_RULESET_NAMES = ["comp-branches"];
4479
- /**
4480
- * A live ruleset name this reconciler must NEVER touch: one `teamSync.ts`
4481
- * created and owns (`team/<slug>`, an exact-ref ruleset), as opposed to the
4482
- * `team/**` wildcard-creation ruleset this module itself manages.
4483
- *
4484
- * `teamRulesetName()` in DevDogsUGA's `rulesets.ts` builds these as
4485
- * `team/${slug}`, and slugs never contain `/`, so a live name is a per-team
4486
- * ruleset iff it starts with `team/` and is not the literal `team/**` this
4487
- * reconciler owns.
4488
- */
3525
+ /** Earlier live names which occupy the same managed slot. */
3526
+ const LEGACY_NAME_ALIASES = { "main-reviews": ["main", "main-protection"] };
3527
+ /** Obsolete fixed rulesets removed after the production-branch cutover. */
3528
+ const DELETE_RULESET_NAMES = ["comp-branches", "production"];
4489
3529
  function isPerTeamRulesetName(name) {
4490
3530
  return name.startsWith("team/") && name !== "team/**";
4491
3531
  }
3532
+ const main$1 = { ref_name: {
3533
+ include: ["refs/heads/main"],
3534
+ exclude: []
3535
+ } };
3536
+ const always = (actor_id) => ({
3537
+ actor_id,
3538
+ actor_type: "Team",
3539
+ bypass_mode: "always"
3540
+ });
4492
3541
  /**
4493
- * The five fixed rulesets, built from resolved actor ids.
3542
+ * Fixed DevDogsUGA rulesets.
4494
3543
  *
4495
- * Pure — no network, no `gh` — so `desired.test.ts` can assert the shape
4496
- * without mocking anything, and `commands.ts` calls this only after
4497
- * resolving `RulesetActors` from the live org.
3544
+ * The main policy is deliberately split. A bypass actor bypasses every rule
3545
+ * in one ruleset, so combining immutable history, PR reviews, updates and CI
3546
+ * would make a direct-push exception an accidental force-push exception too.
4498
3547
  */
4499
3548
  function buildDesiredRulesets(actors) {
4500
3549
  return [
4501
3550
  {
4502
- name: "main",
3551
+ name: "main-integrity",
4503
3552
  target: "branch",
4504
3553
  enforcement: "active",
4505
- conditions: { ref_name: {
4506
- include: ["refs/heads/main"],
4507
- exclude: []
4508
- } },
4509
- bypass_actors: [{
4510
- actor_id: actors.devopsTeamId,
4511
- actor_type: "Team",
4512
- bypass_mode: "always"
4513
- }],
4514
- rules: [
4515
- { type: "deletion" },
4516
- { type: "non_fast_forward" },
3554
+ conditions: main$1,
3555
+ bypass_actors: [],
3556
+ rules: [{ type: "deletion" }, { type: "non_fast_forward" }]
3557
+ },
3558
+ {
3559
+ name: "main-updates",
3560
+ target: "branch",
3561
+ enforcement: "active",
3562
+ conditions: main$1,
3563
+ bypass_actors: [
4517
3564
  {
4518
- type: "update",
4519
- parameters: { update_allows_fetch_and_merge: false }
3565
+ actor_id: actors.reviewersTeamId,
3566
+ actor_type: "Team",
3567
+ bypass_mode: "pull_request"
4520
3568
  },
4521
- {
4522
- type: "pull_request",
4523
- parameters: {
4524
- allowed_merge_methods: ["squash"],
4525
- dismiss_stale_reviews_on_push: false,
4526
- require_code_owner_review: false,
4527
- require_last_push_approval: false,
4528
- required_approving_review_count: 0,
4529
- required_review_thread_resolution: false
4530
- }
3569
+ always(actors.devopsTeamId),
3570
+ always(actors.adminsTeamId)
3571
+ ],
3572
+ rules: [{
3573
+ type: "update",
3574
+ parameters: { update_allows_fetch_and_merge: false }
3575
+ }]
3576
+ },
3577
+ {
3578
+ name: "main-reviews",
3579
+ target: "branch",
3580
+ enforcement: "active",
3581
+ conditions: main$1,
3582
+ bypass_actors: [always(actors.devopsTeamId), always(actors.adminsTeamId)],
3583
+ rules: [{
3584
+ type: "pull_request",
3585
+ parameters: {
3586
+ allowed_merge_methods: ["squash"],
3587
+ dismiss_stale_reviews_on_push: false,
3588
+ require_code_owner_review: true,
3589
+ require_last_push_approval: true,
3590
+ required_approving_review_count: 1,
3591
+ required_review_thread_resolution: true
4531
3592
  }
4532
- ]
3593
+ }]
4533
3594
  },
4534
3595
  {
4535
- name: "production",
3596
+ name: "main-ci",
4536
3597
  target: "branch",
4537
3598
  enforcement: "active",
4538
- conditions: { ref_name: {
4539
- include: ["refs/heads/production"],
4540
- exclude: []
4541
- } },
4542
- bypass_actors: [{
4543
- actor_id: actors.devopsTeamId,
4544
- actor_type: "Team",
4545
- bypass_mode: "pull_request"
4546
- }],
4547
- rules: [
4548
- { type: "deletion" },
4549
- { type: "non_fast_forward" },
4550
- {
4551
- type: "update",
4552
- parameters: { update_allows_fetch_and_merge: false }
4553
- },
4554
- {
4555
- type: "pull_request",
4556
- parameters: {
4557
- allowed_merge_methods: ["merge"],
4558
- dismiss_stale_reviews_on_push: false,
4559
- require_code_owner_review: true,
4560
- require_last_push_approval: false,
4561
- required_approving_review_count: 1,
4562
- required_review_thread_resolution: false
4563
- }
3599
+ conditions: main$1,
3600
+ bypass_actors: [always(actors.devopsTeamId), always(actors.adminsTeamId)],
3601
+ rules: [{
3602
+ type: "required_status_checks",
3603
+ parameters: {
3604
+ do_not_enforce_on_create: false,
3605
+ strict_required_status_checks_policy: false,
3606
+ required_status_checks: [
3607
+ {
3608
+ context: "validate",
3609
+ integration_id: 15368
3610
+ },
3611
+ {
3612
+ context: "database",
3613
+ integration_id: 15368
3614
+ },
3615
+ {
3616
+ context: "format",
3617
+ integration_id: 15368
3618
+ },
3619
+ {
3620
+ context: "flutter",
3621
+ integration_id: 15368
3622
+ }
3623
+ ]
4564
3624
  }
4565
- ]
3625
+ }]
4566
3626
  },
4567
3627
  {
4568
3628
  name: "~ALL",
@@ -4570,17 +3630,17 @@ function buildDesiredRulesets(actors) {
4570
3630
  enforcement: "active",
4571
3631
  conditions: { ref_name: {
4572
3632
  include: ["~ALL"],
4573
- exclude: ["refs/heads/team/**"]
3633
+ exclude: ["refs/heads/main", "refs/heads/team/**"]
4574
3634
  } },
4575
- bypass_actors: [{
4576
- actor_id: actors.devopsTeamId,
4577
- actor_type: "Team",
4578
- bypass_mode: "always"
4579
- }, ...actors.renovateAppId !== void 0 ? [{
4580
- actor_id: actors.renovateAppId,
4581
- actor_type: "Integration",
4582
- bypass_mode: "always"
4583
- }] : []],
3635
+ bypass_actors: [
3636
+ always(actors.devopsTeamId),
3637
+ always(actors.adminsTeamId),
3638
+ ...actors.renovateAppId === void 0 ? [] : [{
3639
+ actor_id: actors.renovateAppId,
3640
+ actor_type: "Integration",
3641
+ bypass_mode: "always"
3642
+ }]
3643
+ ],
4584
3644
  rules: [
4585
3645
  {
4586
3646
  type: "update",
@@ -4599,15 +3659,15 @@ function buildDesiredRulesets(actors) {
4599
3659
  include: ["refs/heads/team/**"],
4600
3660
  exclude: []
4601
3661
  } },
4602
- bypass_actors: [{
4603
- actor_id: actors.appId,
4604
- actor_type: "Integration",
4605
- bypass_mode: "always"
4606
- }, {
4607
- actor_id: actors.devopsTeamId,
4608
- actor_type: "Team",
4609
- bypass_mode: "always"
4610
- }],
3662
+ bypass_actors: [
3663
+ {
3664
+ actor_id: actors.appId,
3665
+ actor_type: "Integration",
3666
+ bypass_mode: "always"
3667
+ },
3668
+ always(actors.devopsTeamId),
3669
+ always(actors.adminsTeamId)
3670
+ ],
4611
3671
  rules: [
4612
3672
  { type: "creation" },
4613
3673
  { type: "deletion" },
@@ -4622,15 +3682,7 @@ function buildDesiredRulesets(actors) {
4622
3682
  include: ["refs/tags/**"],
4623
3683
  exclude: []
4624
3684
  } },
4625
- bypass_actors: [{
4626
- actor_id: actors.adminsTeamId,
4627
- actor_type: "Team",
4628
- bypass_mode: "always"
4629
- }, {
4630
- actor_id: actors.devopsTeamId,
4631
- actor_type: "Team",
4632
- bypass_mode: "always"
4633
- }],
3685
+ bypass_actors: [always(actors.adminsTeamId), always(actors.devopsTeamId)],
4634
3686
  rules: [
4635
3687
  { type: "creation" },
4636
3688
  { type: "update" },
@@ -4702,6 +3754,17 @@ function canonicalizeRule(rule) {
4702
3754
  }
4703
3755
  };
4704
3756
  }
3757
+ if (rule.type === "required_status_checks") return {
3758
+ type: "required_status_checks",
3759
+ parameters: {
3760
+ do_not_enforce_on_create: rule.parameters.do_not_enforce_on_create,
3761
+ strict_required_status_checks_policy: rule.parameters.strict_required_status_checks_policy,
3762
+ required_status_checks: [...rule.parameters.required_status_checks].sort((a, b) => a.context.localeCompare(b.context)).map((check) => ({
3763
+ context: check.context,
3764
+ integration_id: check.integration_id
3765
+ }))
3766
+ }
3767
+ };
4705
3768
  return { type: rule.type };
4706
3769
  }
4707
3770
  function sortRules(rules) {
@@ -4824,6 +3887,7 @@ const DEFAULT_REPO$1 = "DevDogsUGA";
4824
3887
  const DEFAULT_APP_SLUG = "devdogs-platform";
4825
3888
  const DEFAULT_DEVOPS_SLUG = "devops";
4826
3889
  const DEFAULT_ADMINS_SLUG = "admins";
3890
+ const DEFAULT_REVIEWERS_SLUG = "reviewers";
4827
3891
  /** TASK-299 — installed separately from the platform App; may not exist yet. */
4828
3892
  const DEFAULT_RENOVATE_SLUG = "renovate";
4829
3893
  function parseOptions$1(argv) {
@@ -4903,11 +3967,13 @@ async function runGithubRulesets(argv) {
4903
3967
  };
4904
3968
  let devopsTeamId;
4905
3969
  let adminsTeamId;
3970
+ let reviewersTeamId;
4906
3971
  let appId;
4907
3972
  try {
4908
- [devopsTeamId, adminsTeamId, appId] = await Promise.all([
3973
+ [devopsTeamId, adminsTeamId, reviewersTeamId, appId] = await Promise.all([
4909
3974
  resolveTeamId(opts.org, DEFAULT_DEVOPS_SLUG),
4910
3975
  resolveTeamId(opts.org, DEFAULT_ADMINS_SLUG),
3976
+ resolveTeamId(opts.org, DEFAULT_REVIEWERS_SLUG),
4911
3977
  resolveAppId(opts.org, opts.appSlug)
4912
3978
  ]);
4913
3979
  } catch (err) {
@@ -4923,6 +3989,7 @@ async function runGithubRulesets(argv) {
4923
3989
  const actors = {
4924
3990
  devopsTeamId,
4925
3991
  adminsTeamId,
3992
+ reviewersTeamId,
4926
3993
  appId,
4927
3994
  renovateAppId
4928
3995
  };
@@ -6202,7 +5269,8 @@ const HANDLERS = {
6202
5269
  github: handleGithub,
6203
5270
  graphics: async (rest) => (await import("./commands-Dv5TXjvb.js")).handleGraphics(rest),
6204
5271
  qr: async (rest) => (await import("./commands-Bk7IbhPD.js")).handleQr(rest),
6205
- newsletter: async (rest) => (await import("./commands-D345F-Od.js")).handleNewsletter(rest)
5272
+ newsletter: async (rest) => (await import("./commands-D345F-Od.js")).handleNewsletter(rest),
5273
+ creds: async (rest) => (await import("./commands-Dl3UlhBf.js")).handleCreds(rest)
6206
5274
  };
6207
5275
  /**
6208
5276
  * Names that were commands of the contributor CLI, refused with where they