@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.
- package/README.md +45 -1
- package/dist/{catalog-BOBE4ee7.js → catalog-BuNLUu08.js} +145 -237
- package/dist/{cli-YLCe3Zrx.js → cli-BI5Vn9sR.js} +134 -1066
- package/dist/client-B7HhuTaA.js +429 -0
- package/dist/client-DzXQHd58.js +2 -0
- package/dist/commands-Dl3UlhBf.js +1118 -0
- package/dist/connection-D3XsOE3S.js +7 -0
- package/dist/launch.js +4 -2
- package/dist/peers-B0KDkI6a.js +229 -0
- package/dist/vault-C8LYJFOQ.js +563 -0
- package/package.json +4 -4
|
@@ -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 {
|
|
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 {
|
|
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,
|
|
11
|
-
import { confirm, intro, log, note, outro,
|
|
12
|
-
import { chmod,
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
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
|
-
*
|
|
3542
|
+
* Fixed DevDogsUGA rulesets.
|
|
4494
3543
|
*
|
|
4495
|
-
*
|
|
4496
|
-
*
|
|
4497
|
-
*
|
|
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:
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
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
|
-
|
|
4519
|
-
|
|
3565
|
+
actor_id: actors.reviewersTeamId,
|
|
3566
|
+
actor_type: "Team",
|
|
3567
|
+
bypass_mode: "pull_request"
|
|
4520
3568
|
},
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
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: "
|
|
3596
|
+
name: "main-ci",
|
|
4536
3597
|
target: "branch",
|
|
4537
3598
|
enforcement: "active",
|
|
4538
|
-
conditions:
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
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
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
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
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
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
|