@biffo/cli 0.146.2 → 0.147.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +109 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -7736,6 +7736,72 @@ import { Command as Command23 } from "commander";
|
|
|
7736
7736
|
import { Octokit as Octokit2 } from "@octokit/rest";
|
|
7737
7737
|
import { execa as execa5 } from "execa";
|
|
7738
7738
|
|
|
7739
|
+
// src/lib/branch-protection-apply.ts
|
|
7740
|
+
var CONTEXT_CONSISTENCY_THRESHOLD = 2 / 3;
|
|
7741
|
+
var RECENT_COMMIT_WINDOW = 6;
|
|
7742
|
+
function deriveRequiredContexts(observed) {
|
|
7743
|
+
const shasNewestFirst = [];
|
|
7744
|
+
for (const { headSha } of observed) {
|
|
7745
|
+
if (!shasNewestFirst.includes(headSha)) shasNewestFirst.push(headSha);
|
|
7746
|
+
}
|
|
7747
|
+
const window = shasNewestFirst.slice(0, RECENT_COMMIT_WINDOW);
|
|
7748
|
+
if (window.length === 0) return [];
|
|
7749
|
+
const seenOn = /* @__PURE__ */ new Map();
|
|
7750
|
+
for (const { name, headSha } of observed) {
|
|
7751
|
+
if (!window.includes(headSha)) continue;
|
|
7752
|
+
const set = seenOn.get(name) ?? /* @__PURE__ */ new Set();
|
|
7753
|
+
set.add(headSha);
|
|
7754
|
+
seenOn.set(name, set);
|
|
7755
|
+
}
|
|
7756
|
+
const required = Math.ceil(window.length * CONTEXT_CONSISTENCY_THRESHOLD);
|
|
7757
|
+
return [...seenOn.entries()].filter(([, shas]) => shas.size >= required).map(([name]) => name).sort();
|
|
7758
|
+
}
|
|
7759
|
+
function protectionParamsFor(contexts) {
|
|
7760
|
+
return {
|
|
7761
|
+
required_status_checks: { strict: true, contexts: [...contexts].sort() },
|
|
7762
|
+
enforce_admins: false,
|
|
7763
|
+
required_pull_request_reviews: {
|
|
7764
|
+
required_approving_review_count: 0,
|
|
7765
|
+
dismiss_stale_reviews: false
|
|
7766
|
+
},
|
|
7767
|
+
restrictions: null,
|
|
7768
|
+
required_linear_history: true,
|
|
7769
|
+
allow_force_pushes: false,
|
|
7770
|
+
allow_deletions: false
|
|
7771
|
+
};
|
|
7772
|
+
}
|
|
7773
|
+
function planProtection(branch, exists, observed) {
|
|
7774
|
+
if (!exists) {
|
|
7775
|
+
return {
|
|
7776
|
+
branch,
|
|
7777
|
+
action: "skip",
|
|
7778
|
+
reason: "branch does not exist",
|
|
7779
|
+
contexts: []
|
|
7780
|
+
};
|
|
7781
|
+
}
|
|
7782
|
+
const contexts = deriveRequiredContexts(observed);
|
|
7783
|
+
if (contexts.length === 0) {
|
|
7784
|
+
return {
|
|
7785
|
+
branch,
|
|
7786
|
+
action: "skip",
|
|
7787
|
+
reason: "no check has reported consistently enough to require \u2014 applying protection with an empty context list would make the branch look protected while admitting any PR",
|
|
7788
|
+
contexts: []
|
|
7789
|
+
};
|
|
7790
|
+
}
|
|
7791
|
+
return {
|
|
7792
|
+
branch,
|
|
7793
|
+
action: "apply",
|
|
7794
|
+
reason: `requiring ${contexts.length} context(s) observed on recent commits`,
|
|
7795
|
+
contexts
|
|
7796
|
+
};
|
|
7797
|
+
}
|
|
7798
|
+
function formatPlans(plans) {
|
|
7799
|
+
return plans.map(
|
|
7800
|
+
(p) => p.action === "apply" ? ` ${p.branch}: apply \u2014 ${p.reason}
|
|
7801
|
+
${p.contexts.map((c) => ` \u2022 ${c}`).join("\n")}` : ` ${p.branch}: skip \u2014 ${p.reason}`
|
|
7802
|
+
).join("\n");
|
|
7803
|
+
}
|
|
7804
|
+
|
|
7739
7805
|
// src/lib/branch-protection-audit.ts
|
|
7740
7806
|
function auditBranch(branch, protection) {
|
|
7741
7807
|
if (protection === null) {
|
|
@@ -7805,7 +7871,16 @@ async function resolveRepo(explicit) {
|
|
|
7805
7871
|
}
|
|
7806
7872
|
return { owner: m[1], repo: m[2] };
|
|
7807
7873
|
}
|
|
7808
|
-
async function
|
|
7874
|
+
async function observedChecks(octokit, owner, repo, branch) {
|
|
7875
|
+
const { data } = await octokit.actions.listWorkflowRunsForRepo({
|
|
7876
|
+
owner,
|
|
7877
|
+
repo,
|
|
7878
|
+
branch,
|
|
7879
|
+
per_page: 60
|
|
7880
|
+
});
|
|
7881
|
+
return data.workflow_runs.filter((r) => r.status === "completed").map((r) => ({ name: r.name ?? "", headSha: r.head_sha })).filter((c) => c.name !== "");
|
|
7882
|
+
}
|
|
7883
|
+
async function runBranchProtectionCheck(explicitRepo, options = {}) {
|
|
7809
7884
|
const { owner, repo } = await resolveRepo(explicitRepo);
|
|
7810
7885
|
const octokit = new Octokit2({
|
|
7811
7886
|
auth: tokenFromEnv(),
|
|
@@ -7851,12 +7926,39 @@ async function runBranchProtectionCheck(explicitRepo) {
|
|
|
7851
7926
|
);
|
|
7852
7927
|
process.exit(1);
|
|
7853
7928
|
}
|
|
7929
|
+
if (findings.length > 0 && options.fix) {
|
|
7930
|
+
const defaultBranch = (await octokit.repos.get({ owner, repo })).data.default_branch;
|
|
7931
|
+
const observed = await observedChecks(octokit, owner, repo, defaultBranch);
|
|
7932
|
+
const plans = audited.map((branch) => planProtection(branch, true, observed));
|
|
7933
|
+
console.log(`Backfilling protection on ${owner}/${repo}:
|
|
7934
|
+
`);
|
|
7935
|
+
console.log(formatPlans(plans));
|
|
7936
|
+
let applied = 0;
|
|
7937
|
+
for (const plan of plans.filter((p) => p.action === "apply")) {
|
|
7938
|
+
await octokit.repos.updateBranchProtection({
|
|
7939
|
+
owner,
|
|
7940
|
+
repo,
|
|
7941
|
+
branch: plan.branch,
|
|
7942
|
+
...protectionParamsFor(plan.contexts)
|
|
7943
|
+
});
|
|
7944
|
+
applied += 1;
|
|
7945
|
+
}
|
|
7946
|
+
if (applied === 0) {
|
|
7947
|
+
console.error(
|
|
7948
|
+
"\n\u2717 nothing applied \u2014 no branch had a check reporting consistently enough to require.\n Protection with an empty context list is worse than none: a PR reads CLEAN before\n any run registers. Get CI reporting on this repo first."
|
|
7949
|
+
);
|
|
7950
|
+
process.exit(1);
|
|
7951
|
+
}
|
|
7952
|
+
console.log(`
|
|
7953
|
+
\u2713 protection applied to ${applied} branch(es). Re-run without --fix to verify.`);
|
|
7954
|
+
return;
|
|
7955
|
+
}
|
|
7854
7956
|
if (findings.length > 0) {
|
|
7855
7957
|
console.error(`\u2717 branch-protection guard: ${owner}/${repo}
|
|
7856
7958
|
`);
|
|
7857
7959
|
console.error(formatFindings(findings));
|
|
7858
7960
|
console.error(
|
|
7859
|
-
"\n Protection is applied once at scaffold time and skipped silently on a 403 (#715)
|
|
7961
|
+
"\n Protection is applied once at scaffold time and skipped silently on a 403 (#715),\n and standalone plugin repos are created outside the CLI so it never runs at all\n (#714). Re-run with --fix to backfill it from the checks this repo actually reports."
|
|
7860
7962
|
);
|
|
7861
7963
|
process.exit(1);
|
|
7862
7964
|
}
|
|
@@ -8269,8 +8371,11 @@ checkCommand.command("plugin-terraform").description("Verify every template-owne
|
|
|
8269
8371
|
});
|
|
8270
8372
|
checkCommand.command("branch-protection").description(
|
|
8271
8373
|
"Verify dev/staging/main are actually protected \u2014 scaffolding skips this on a 403 (#715)"
|
|
8272
|
-
).option("--repo <owner/name>", "Repo to audit; defaults to this checkout's origin remote").
|
|
8273
|
-
|
|
8374
|
+
).option("--repo <owner/name>", "Repo to audit; defaults to this checkout's origin remote").option(
|
|
8375
|
+
"--fix",
|
|
8376
|
+
"Backfill protection from the checks this repo actually reports (#714, #715). Refuses to apply an empty required-check list, which would look protected and admit anything."
|
|
8377
|
+
).action(async (opts) => {
|
|
8378
|
+
await runBranchProtectionCheck(opts.repo, { fix: opts.fix });
|
|
8274
8379
|
});
|
|
8275
8380
|
function rawArgsAfter(subcommand) {
|
|
8276
8381
|
const at = process.argv.indexOf(subcommand);
|