@biffo/cli 0.199.1 → 0.199.3
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/dist/index.js +175 -69
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -216,6 +216,34 @@ function latestCoreVersionFromTags(repo, git = defaultTagRunner, options = {}) {
|
|
|
216
216
|
var CORE_TAG_PREFIX = "core-v";
|
|
217
217
|
var defaultTagRunner = (args) => execFileSync("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
218
218
|
|
|
219
|
+
// src/lib/git-tracked-files.ts
|
|
220
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
221
|
+
import { realpathSync } from "fs";
|
|
222
|
+
var defaultGit = (args) => execFileSync2("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
223
|
+
function gitTrackedFiles(root, git = defaultGit) {
|
|
224
|
+
let top;
|
|
225
|
+
try {
|
|
226
|
+
top = git(["-C", root, "rev-parse", "--show-toplevel"]).trim();
|
|
227
|
+
} catch {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
if (!top) return null;
|
|
231
|
+
try {
|
|
232
|
+
if (realpathSync(top) !== realpathSync(root)) return null;
|
|
233
|
+
} catch {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
let out;
|
|
237
|
+
try {
|
|
238
|
+
out = git(["-C", root, "ls-files", "-z"]);
|
|
239
|
+
} catch {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
const files = out.split("\0").filter((p) => p !== "");
|
|
243
|
+
if (files.length === 0) return null;
|
|
244
|
+
return new Set(files);
|
|
245
|
+
}
|
|
246
|
+
|
|
219
247
|
// src/lib/core-manifest.ts
|
|
220
248
|
var CORE_MANIFEST_FILE = "core-manifest.json";
|
|
221
249
|
var CoreManifestSchema2 = z2.object({
|
|
@@ -315,8 +343,9 @@ function isTemplateOwned(relPath, manifest) {
|
|
|
315
343
|
function toPosix(p) {
|
|
316
344
|
return sep === "/" ? p : p.split(sep).join("/");
|
|
317
345
|
}
|
|
318
|
-
function listTemplateOwnedFiles(root, manifest) {
|
|
346
|
+
function listTemplateOwnedFiles(root, manifest, options = {}) {
|
|
319
347
|
const out = [];
|
|
348
|
+
const tracked = options.trackedOnly ? options.git ? gitTrackedFiles(root, options.git) : gitTrackedFiles(root) : null;
|
|
320
349
|
function walk(dir) {
|
|
321
350
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
322
351
|
if (entry.isDirectory() && HARD_EXCLUDED_DIRS.has(entry.name)) continue;
|
|
@@ -325,6 +354,7 @@ function listTemplateOwnedFiles(root, manifest) {
|
|
|
325
354
|
walk(abs);
|
|
326
355
|
} else if (entry.isFile()) {
|
|
327
356
|
const rel = toPosix(relative(root, abs));
|
|
357
|
+
if (tracked && !tracked.has(rel)) continue;
|
|
328
358
|
if (isTemplateOwned(rel, manifest)) out.push(rel);
|
|
329
359
|
}
|
|
330
360
|
}
|
|
@@ -335,10 +365,11 @@ function listTemplateOwnedFiles(root, manifest) {
|
|
|
335
365
|
function sameFile(a, b) {
|
|
336
366
|
return readFileSync2(a).equals(readFileSync2(b));
|
|
337
367
|
}
|
|
338
|
-
function computeCoreDiff(templateRoot, instanceRoot, manifest, baseRoot) {
|
|
339
|
-
const
|
|
368
|
+
function computeCoreDiff(templateRoot, instanceRoot, manifest, baseRoot, options = {}) {
|
|
369
|
+
const trackedOnly = options.git ? { trackedOnly: true, git: options.git } : { trackedOnly: true };
|
|
370
|
+
const templateFiles = new Set(listTemplateOwnedFiles(templateRoot, manifest, trackedOnly));
|
|
340
371
|
const instanceFiles = new Set(listTemplateOwnedFiles(instanceRoot, manifest));
|
|
341
|
-
const baseFiles = baseRoot ? new Set(listTemplateOwnedFiles(baseRoot, manifest)) : void 0;
|
|
372
|
+
const baseFiles = baseRoot ? new Set(listTemplateOwnedFiles(baseRoot, manifest, trackedOnly)) : void 0;
|
|
342
373
|
const diff = {
|
|
343
374
|
added: [],
|
|
344
375
|
removed: [],
|
|
@@ -687,9 +718,10 @@ var EMPTY_SUMMARY = () => ({
|
|
|
687
718
|
});
|
|
688
719
|
async function planCoreUpgrade(options) {
|
|
689
720
|
const mergeFile = options.mergeFile ?? gitMergeFile;
|
|
690
|
-
const
|
|
721
|
+
const trackedOnly = { trackedOnly: true };
|
|
722
|
+
const base = new Set(listTemplateOwnedFiles(options.baseDir, options.manifest, trackedOnly));
|
|
691
723
|
const ours = new Set(listTemplateOwnedFiles(options.oursDir, options.manifest));
|
|
692
|
-
const theirs = new Set(listTemplateOwnedFiles(options.theirsDir, options.manifest));
|
|
724
|
+
const theirs = new Set(listTemplateOwnedFiles(options.theirsDir, options.manifest, trackedOnly));
|
|
693
725
|
const divergentPrefixes = readDivergenceConfig(options.oursDir).warnOnly.map((e) => e.prefix);
|
|
694
726
|
const isDeclaredDivergent = (path) => divergentPrefixes.some((prefix) => path.startsWith(prefix));
|
|
695
727
|
const paths = [.../* @__PURE__ */ new Set([...base, ...ours, ...theirs])].sort();
|
|
@@ -1190,16 +1222,37 @@ function formatBranchProtectionSummary(outcomes) {
|
|
|
1190
1222
|
];
|
|
1191
1223
|
for (const outcome of unprotected) {
|
|
1192
1224
|
const left = outcome.unprotectedBranches.join(", ") || "unknown";
|
|
1193
|
-
|
|
1194
|
-
|
|
1225
|
+
lines.push(
|
|
1226
|
+
` ${outcome.org}/${outcome.repo} \u2014 unprotected: ${left} (${whyUnprotected(outcome)})`
|
|
1227
|
+
);
|
|
1195
1228
|
if (outcome.reason) lines.push(` ${outcome.reason}`);
|
|
1196
1229
|
}
|
|
1197
1230
|
lines.push(
|
|
1198
|
-
" Direct pushes, force-pushes and merges with red or missing checks are all allowed on those branches right now."
|
|
1199
|
-
|
|
1231
|
+
" Direct pushes, force-pushes and merges with red or missing checks are all allowed on those branches right now."
|
|
1232
|
+
);
|
|
1233
|
+
if (unprotected.some((o) => o.status === "skipped-no-contexts")) {
|
|
1234
|
+
lines.push(
|
|
1235
|
+
" Where the required checks could not be determined, the fix is the CI job names, not the plan: make the workflow report named jobs, let one run complete, then backfill."
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
const has403 = unprotected.some((o) => o.status === "skipped-403");
|
|
1239
|
+
lines.push(
|
|
1240
|
+
" Fix it with: biffo check branch-protection --fix " + (has403 ? "(after upgrading the plan, or making the repo public)" : "(it derives the required checks from the runs the repo has actually reported)")
|
|
1200
1241
|
);
|
|
1201
1242
|
return lines;
|
|
1202
1243
|
}
|
|
1244
|
+
function whyUnprotected(outcome) {
|
|
1245
|
+
switch (outcome.status) {
|
|
1246
|
+
case "skipped-403":
|
|
1247
|
+
return "GitHub returned 403 \u2014 the org's plan does not allow branch protection on this repo";
|
|
1248
|
+
case "skipped-no-contexts":
|
|
1249
|
+
return "the required status checks could not be determined, and protection requiring nothing reads as configured while gating on nothing";
|
|
1250
|
+
case "failed":
|
|
1251
|
+
return "branch protection failed";
|
|
1252
|
+
default:
|
|
1253
|
+
return "branch protection incomplete";
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1203
1256
|
function reportBranchProtectionSummary() {
|
|
1204
1257
|
const outcomes = pending.splice(0, pending.length);
|
|
1205
1258
|
const lines = formatBranchProtectionSummary(outcomes);
|
|
@@ -1722,51 +1775,91 @@ var GitHubAdapter = class {
|
|
|
1722
1775
|
* `statusChecks` must be non-empty. Protection that requires nothing is worse
|
|
1723
1776
|
* than none: it reads as configured while gating on nothing, so callers that
|
|
1724
1777
|
* could not determine the contexts must not silently land here.
|
|
1778
|
+
*
|
|
1779
|
+
* Like `configureBranchProtection`, this **returns and records** what actually
|
|
1780
|
+
* happened (#1001). It used to return `Promise<void>` and swallow the 403 with
|
|
1781
|
+
* two log warnings, so `biffo plugin create --org` printed its next-steps
|
|
1782
|
+
* block and exited 0 with nothing anywhere recording that the repo it had just
|
|
1783
|
+
* created had an unprotected `dev` — the identical defect #715 described for
|
|
1784
|
+
* the deployable path, on the path #999 deliberately left out of scope.
|
|
1725
1785
|
*/
|
|
1726
1786
|
async protectSingleBranch(org, repo, branch, statusChecks, protectionIntervalMs = 3e3) {
|
|
1727
1787
|
if (statusChecks.length === 0) {
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1788
|
+
const message = `Refusing to protect ${org}/${repo}@${branch} with no required status checks \u2014 that reads as protected while gating on nothing. Determine the workflow's job contexts first.`;
|
|
1789
|
+
recordBranchProtectionOutcome({
|
|
1790
|
+
status: "skipped-no-contexts",
|
|
1791
|
+
org,
|
|
1792
|
+
repo,
|
|
1793
|
+
protectedBranches: [],
|
|
1794
|
+
unprotectedBranches: [branch],
|
|
1795
|
+
reason: message
|
|
1796
|
+
});
|
|
1797
|
+
throw new Error(message);
|
|
1731
1798
|
}
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1799
|
+
try {
|
|
1800
|
+
log.info(`Waiting for ${branch} branch to be ready...`);
|
|
1801
|
+
await this.waitForBranch(org, repo, branch);
|
|
1802
|
+
log.info(`Configuring branch protection on ${branch}...`);
|
|
1803
|
+
const params = {
|
|
1804
|
+
owner: org,
|
|
1805
|
+
repo,
|
|
1806
|
+
branch,
|
|
1807
|
+
required_status_checks: { strict: true, contexts: statusChecks },
|
|
1808
|
+
enforce_admins: false,
|
|
1809
|
+
required_pull_request_reviews: {
|
|
1810
|
+
required_approving_review_count: 0,
|
|
1811
|
+
dismiss_stale_reviews: false
|
|
1812
|
+
},
|
|
1813
|
+
restrictions: null,
|
|
1814
|
+
required_linear_history: true,
|
|
1815
|
+
allow_force_pushes: false,
|
|
1816
|
+
allow_deletions: false
|
|
1817
|
+
};
|
|
1818
|
+
const deadline = Date.now() + 3e4;
|
|
1819
|
+
while (true) {
|
|
1820
|
+
try {
|
|
1821
|
+
await this.octokit.repos.updateBranchProtection(params);
|
|
1822
|
+
break;
|
|
1823
|
+
} catch (err) {
|
|
1824
|
+
const status = err.status;
|
|
1825
|
+
if (status === 403) {
|
|
1826
|
+
log.warn(`Branch protection unavailable for ${org}/${repo}: ${err.message}`);
|
|
1827
|
+
log.warn(" Add it later via GitHub once the plan allows it, or make the repo public.");
|
|
1828
|
+
return recordBranchProtectionOutcome({
|
|
1829
|
+
status: "skipped-403",
|
|
1830
|
+
org,
|
|
1831
|
+
repo,
|
|
1832
|
+
protectedBranches: [],
|
|
1833
|
+
unprotectedBranches: [branch],
|
|
1834
|
+
reason: err.message
|
|
1835
|
+
});
|
|
1836
|
+
}
|
|
1837
|
+
if (status !== 404 || Date.now() >= deadline) throw err;
|
|
1838
|
+
log.info("Branch protection endpoint not yet ready, retrying...");
|
|
1839
|
+
await new Promise((resolve19) => setTimeout(resolve19, protectionIntervalMs));
|
|
1761
1840
|
}
|
|
1762
|
-
if (status !== 404 || Date.now() >= deadline) throw err;
|
|
1763
|
-
log.info("Branch protection endpoint not yet ready, retrying...");
|
|
1764
|
-
await new Promise((resolve19) => setTimeout(resolve19, protectionIntervalMs));
|
|
1765
1841
|
}
|
|
1842
|
+
} catch (err) {
|
|
1843
|
+
recordBranchProtectionOutcome({
|
|
1844
|
+
status: "failed",
|
|
1845
|
+
org,
|
|
1846
|
+
repo,
|
|
1847
|
+
protectedBranches: [],
|
|
1848
|
+
unprotectedBranches: [branch],
|
|
1849
|
+
reason: err.message
|
|
1850
|
+
});
|
|
1851
|
+
throw err;
|
|
1766
1852
|
}
|
|
1767
1853
|
log.success(
|
|
1768
1854
|
`Branch protection configured on ${branch} (${statusChecks.length} required check(s))`
|
|
1769
1855
|
);
|
|
1856
|
+
return recordBranchProtectionOutcome({
|
|
1857
|
+
status: "applied",
|
|
1858
|
+
org,
|
|
1859
|
+
repo,
|
|
1860
|
+
protectedBranches: [branch],
|
|
1861
|
+
unprotectedBranches: []
|
|
1862
|
+
});
|
|
1770
1863
|
}
|
|
1771
1864
|
async createEnvironments(config) {
|
|
1772
1865
|
const { org, repo } = config.source_control.config;
|
|
@@ -2401,7 +2494,7 @@ function applyMigrationCarry(instanceDir, plan) {
|
|
|
2401
2494
|
}
|
|
2402
2495
|
|
|
2403
2496
|
// src/lib/core-template-trees.ts
|
|
2404
|
-
import { execFileSync as
|
|
2497
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
2405
2498
|
import { mkdtempSync as mkdtempSync3, rmSync as rmSync3 } from "fs";
|
|
2406
2499
|
import { tmpdir as tmpdir3 } from "os";
|
|
2407
2500
|
import { join as join8 } from "path";
|
|
@@ -2409,8 +2502,8 @@ function coreTag(version) {
|
|
|
2409
2502
|
parseCoreVersion(version);
|
|
2410
2503
|
return `core-v${version}`;
|
|
2411
2504
|
}
|
|
2412
|
-
var
|
|
2413
|
-
function workingTreeMatchesTag(repo, version, git =
|
|
2505
|
+
var defaultGit2 = (args) => execFileSync3("git", args, { encoding: "utf8" });
|
|
2506
|
+
function workingTreeMatchesTag(repo, version, git = defaultGit2) {
|
|
2414
2507
|
const tag = coreTag(version);
|
|
2415
2508
|
try {
|
|
2416
2509
|
const head = git(["-C", repo, "rev-parse", "HEAD"]).trim();
|
|
@@ -2430,7 +2523,7 @@ function tagExists(repo, tag, git) {
|
|
|
2430
2523
|
return false;
|
|
2431
2524
|
}
|
|
2432
2525
|
}
|
|
2433
|
-
function materializeTemplateAtTag(repo, version, git =
|
|
2526
|
+
function materializeTemplateAtTag(repo, version, git = defaultGit2) {
|
|
2434
2527
|
const tag = coreTag(version);
|
|
2435
2528
|
if (!tagExists(repo, tag, git)) {
|
|
2436
2529
|
try {
|
|
@@ -2447,7 +2540,7 @@ function materializeTemplateAtTag(repo, version, git = defaultGit) {
|
|
|
2447
2540
|
const tarball = join8(dir, ".tree.tar");
|
|
2448
2541
|
try {
|
|
2449
2542
|
git(["-C", repo, "archive", "--format=tar", "-o", tarball, tag]);
|
|
2450
|
-
|
|
2543
|
+
execFileSync3("tar", ["-x", "-f", tarball, "-C", dir]);
|
|
2451
2544
|
rmSync3(tarball, { force: true });
|
|
2452
2545
|
} catch (err) {
|
|
2453
2546
|
rmSync3(dir, { recursive: true, force: true });
|
|
@@ -7150,25 +7243,29 @@ var pluginCreateCommand = new Command13("create").description("Scaffold a new pl
|
|
|
7150
7243
|
async (name, options) => {
|
|
7151
7244
|
const cwd = options.cwd ? resolve11(options.cwd) : process.cwd();
|
|
7152
7245
|
try {
|
|
7153
|
-
|
|
7154
|
-
|
|
7155
|
-
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
|
|
7160
|
-
|
|
7161
|
-
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
7168
|
-
|
|
7169
|
-
|
|
7170
|
-
|
|
7171
|
-
|
|
7246
|
+
try {
|
|
7247
|
+
await runPluginCreate(
|
|
7248
|
+
name,
|
|
7249
|
+
{
|
|
7250
|
+
firstParty: options.firstParty ?? false,
|
|
7251
|
+
standalone: options.standalone ?? false,
|
|
7252
|
+
...options.org ? { org: options.org } : {},
|
|
7253
|
+
...options.runnerLabel ? { runnerLabel: options.runnerLabel } : {},
|
|
7254
|
+
register: options.register !== false,
|
|
7255
|
+
...options.skeleton ? { skeletonRoot: resolve11(options.skeleton) } : {},
|
|
7256
|
+
dryRun: options.dryRun ?? false,
|
|
7257
|
+
commit: options.commit !== false,
|
|
7258
|
+
cwd
|
|
7259
|
+
},
|
|
7260
|
+
{
|
|
7261
|
+
git: new GitAdapter(),
|
|
7262
|
+
makeGitHub: (token) => new GitHubAdapter(token),
|
|
7263
|
+
resolveToken: resolveGithubToken3
|
|
7264
|
+
}
|
|
7265
|
+
);
|
|
7266
|
+
} finally {
|
|
7267
|
+
reportBranchProtectionSummary();
|
|
7268
|
+
}
|
|
7172
7269
|
} catch (err) {
|
|
7173
7270
|
log.error(err.message);
|
|
7174
7271
|
process.exit(1);
|
|
@@ -7184,6 +7281,7 @@ async function runPluginCreate(name, options, deps) {
|
|
|
7184
7281
|
);
|
|
7185
7282
|
}
|
|
7186
7283
|
await runStandaloneCreate(names, options, deps);
|
|
7284
|
+
reportBranchProtectionSummary();
|
|
7187
7285
|
return;
|
|
7188
7286
|
}
|
|
7189
7287
|
const isInstance = existsSync23(join23(options.cwd, INSTANCE_CORE_FILE));
|
|
@@ -7309,6 +7407,14 @@ async function createAndPushStandaloneRepo(org, names, destDir, options, deps) {
|
|
|
7309
7407
|
log.warn(
|
|
7310
7408
|
`Could not determine required status checks from ${ciPath} \u2014 skipping branch protection. Configure it manually on dev once you know the CI job names.`
|
|
7311
7409
|
);
|
|
7410
|
+
recordBranchProtectionOutcome({
|
|
7411
|
+
status: "skipped-no-contexts",
|
|
7412
|
+
org,
|
|
7413
|
+
repo: names.dist,
|
|
7414
|
+
protectedBranches: [],
|
|
7415
|
+
unprotectedBranches: ["dev"],
|
|
7416
|
+
reason: `No status-check contexts could be derived from ${ciPath}, so there was nothing to require. Protection with an empty context list reads as configured while admitting every PR, so none was applied.`
|
|
7417
|
+
});
|
|
7312
7418
|
} else {
|
|
7313
7419
|
await github.protectSingleBranch(org, names.dist, "dev", contexts);
|
|
7314
7420
|
}
|