coderifts 5.0.1 → 6.0.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/CHANGELOG.md +86 -1
- package/dist/cli.js +225 -11
- package/package.json +2 -2
- package/scripts/assert-changelog-version.js +88 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 6.0.0
|
|
4
|
+
|
|
5
|
+
_Nothing yet._
|
|
6
|
+
|
|
7
|
+
## 5.0.1
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- **A fresh install shipped the guard with the conditional-write hole.**
|
|
11
|
+
`coderifts@5.0.0` declared `@coderifts/agent-guard: ^11.0.0`, which resolved to
|
|
12
|
+
11.0.1 — precisely the tarball the guard's own 12.0.0 round reproduced the
|
|
13
|
+
defect against, where `requireConditionalWrite` did not fire on an ordinary
|
|
14
|
+
mutation and the side effect **ran** with `enforced: true`. The guard is a
|
|
15
|
+
**direct** dependency with no `peerDependencies` and no bundled copy, so this
|
|
16
|
+
range *is* the adopter's resolution, not advice. Now `^12.0.0`, with
|
|
17
|
+
`MIN_GUARD_MAJOR` raised to 12. Cost measured as zero: the CLI imports only
|
|
18
|
+
`deployGate`, `asVerifiedDeployReceiptView`, `DEPLOY_RECEIPT_VIEW_SPEC` and
|
|
19
|
+
`matchGlob`, whose modules are byte-identical 11.0.1 → 12.0.0.
|
|
20
|
+
- **Two defects in the publish gate itself**, both proven by replaying the real
|
|
21
|
+
script. The resolved check only ever compared *downward* — `resolved >= floor`,
|
|
22
|
+
never whether the resolved version sits **inside** the declared range — so
|
|
23
|
+
declaring `^11.0.0` with 12.0.0 staged for a cost measurement printed OK and
|
|
24
|
+
exited 0, which is exactly the shape that makes "we measured 12 and shipped
|
|
25
|
+
^11" indistinguishable from a correct release. And an unresolvable install was
|
|
26
|
+
a **pass**. Three failure modes added (resolved below the declared range at
|
|
27
|
+
full precision, resolved above it, and not resolvable at all), with no
|
|
28
|
+
opt-out — an `allowUnresolved` flag was written and deliberately removed, and
|
|
29
|
+
a test asserts it stays absent.
|
|
30
|
+
|
|
31
|
+
## 5.0.0
|
|
4
32
|
|
|
5
33
|
### Fixed
|
|
6
34
|
- **P0 — `setup-required-check --apply` no longer reports `APPLIED` over a
|
|
@@ -59,6 +87,63 @@
|
|
|
59
87
|
template is unchanged and stays non-blocking; it carries the same three lines
|
|
60
88
|
commented, with a note saying why the two differ.
|
|
61
89
|
|
|
90
|
+
## 4.13.0 — never published (folded into 5.0.0)
|
|
91
|
+
|
|
92
|
+
Bumped in-repo (`917611d`) but never tagged or released to npm; 5.0.0 superseded
|
|
93
|
+
it, so adopters received this content **as part of 5.0.0**. Recorded separately
|
|
94
|
+
because the version number appears in the source history and in
|
|
95
|
+
`assert-guard-major.js`'s floor-raise log.
|
|
96
|
+
|
|
97
|
+
### Changed
|
|
98
|
+
- **Guard floor raised to 11.** Cost measured as zero the way the 10-raise was —
|
|
99
|
+
the CLI imports only `deployGate`, `asVerifiedDeployReceiptView`,
|
|
100
|
+
`DEPLOY_RECEIPT_VIEW_SPEC` and `matchGlob`, none of which 11.0.0 touched, and
|
|
101
|
+
the suite staged against 11 returned an identical 532/530/2 with a
|
|
102
|
+
byte-identical failure set. It rises anyway, because a semantics floor is about
|
|
103
|
+
what the CLI may **run** against, not what it happens to import today: 10.x's
|
|
104
|
+
`computeBundleFingerprint` disagreed with the server, which is exactly the
|
|
105
|
+
class this gate exists to refuse.
|
|
106
|
+
- The two tests pinning the old floor were updated, and the invariant made
|
|
107
|
+
explicit: **the floor only ever rises**.
|
|
108
|
+
|
|
109
|
+
### Added
|
|
110
|
+
- `/api/v1/agent/preflight` returns `scorer_version` — the one field that makes
|
|
111
|
+
the single-spec fingerprint reproducible by a consumer holding their own
|
|
112
|
+
before/after/policy. Purely additive; no surface moves and `tools_sha256` is
|
|
113
|
+
unchanged.
|
|
114
|
+
|
|
115
|
+
## 4.12.0
|
|
116
|
+
|
|
117
|
+
### Changed
|
|
118
|
+
- **Guard floor raised to 10.** Cost measured as zero: the CLI imports only
|
|
119
|
+
`deployGate`, `asVerifiedDeployReceiptView`, `DEPLOY_RECEIPT_VIEW_SPEC` and
|
|
120
|
+
`matchGlob` — never `withCodeRifts` — so 10.0.0's strict-composition break
|
|
121
|
+
cannot reach it. Verified by staging guard 10 into the install and running the
|
|
122
|
+
suite (guard-conformance 6/6, the same two pre-existing failures as the guard-9
|
|
123
|
+
baseline). The gate was satisfied, not weakened: before the install it
|
|
124
|
+
correctly reported FAIL on the stale resolve.
|
|
125
|
+
|
|
126
|
+
## 4.11.0
|
|
127
|
+
|
|
128
|
+
### Fixed
|
|
129
|
+
- **P0 — the issuer binding pointed at a check that cannot block.** GitHub passes
|
|
130
|
+
a required status check on `success`, `skipped` **or `neutral`**, so our App's
|
|
131
|
+
advisory check genuinely cannot block; a false comment in
|
|
132
|
+
`webhook-integration.js` claiming otherwise is what produced the regression.
|
|
133
|
+
The enforcing issuer is `app_id 15368` (github-actions), which the contract-gate
|
|
134
|
+
Action posts under, so that binding is now VERIFIED — with the shared-identity
|
|
135
|
+
residual **named** rather than implied: anyone who can edit
|
|
136
|
+
`.github/workflows` can post under it. A binding to our own App id grades
|
|
137
|
+
UNVERIFIABLE rather than NOT_VERIFIED, because `MERGEGATE_ENFORCE` is
|
|
138
|
+
server-side and invisible from here.
|
|
139
|
+
- **P0 — the installer wrote what our own verifier rejects.** It now writes
|
|
140
|
+
`checks[]` with an explicit `app_id`, because an omitted `app_id` silently
|
|
141
|
+
resolves to whoever posted last, which is unauditable. It never re-binds
|
|
142
|
+
another team's existing check, and the success path prints what it does **not**
|
|
143
|
+
achieve. A coupling test asserts the installer's and the verifier's issuer
|
|
144
|
+
constants are equal. The verifier was not weakened — the name-only and
|
|
145
|
+
legacy-contexts paths are byte-identical, asserted by two unchanged tests.
|
|
146
|
+
|
|
62
147
|
## 4.10.0
|
|
63
148
|
|
|
64
149
|
### Added
|
package/dist/cli.js
CHANGED
|
@@ -3028,7 +3028,7 @@ var require_package = __commonJS({
|
|
|
3028
3028
|
"package.json"(exports2, module2) {
|
|
3029
3029
|
module2.exports = {
|
|
3030
3030
|
name: "coderifts",
|
|
3031
|
-
version: "
|
|
3031
|
+
version: "6.0.0",
|
|
3032
3032
|
description: "Detect breaking API changes from the command line. Works locally or with the CodeRifts cloud API.",
|
|
3033
3033
|
author: "CodeRifts <hello@coderifts.com>",
|
|
3034
3034
|
license: "MIT",
|
|
@@ -3067,7 +3067,7 @@ var require_package = __commonJS({
|
|
|
3067
3067
|
},
|
|
3068
3068
|
scripts: {
|
|
3069
3069
|
build: "node scripts/copy-corpus.js && node scripts/build.js",
|
|
3070
|
-
prepublishOnly: "node scripts/assert-guard-major.js && bash ../../scripts/freeze-gate.sh && npm run build",
|
|
3070
|
+
prepublishOnly: "node scripts/assert-guard-major.js && node scripts/assert-changelog-version.js && bash ../../scripts/freeze-gate.sh && npm run build",
|
|
3071
3071
|
"assert-guard-major": "node scripts/assert-guard-major.js",
|
|
3072
3072
|
postinstall: "node scripts/postinstall.js",
|
|
3073
3073
|
test: "node --test test/*.test.js"
|
|
@@ -253975,6 +253975,16 @@ var require_github_enforcement = __commonJS({
|
|
|
253975
253975
|
RULESET_ABSENT: "ruleset_absent",
|
|
253976
253976
|
/** Rulesets could not be read at all. */
|
|
253977
253977
|
RULESET_UNREADABLE: "ruleset_unreadable",
|
|
253978
|
+
/** A ruleset requires our context but does not TARGET the branch that matters (1104a). */
|
|
253979
|
+
RULESET_REF_NOT_TARGETED: "ruleset_ref_not_targeted",
|
|
253980
|
+
/** Active, bound, targeted — but strict latest-head is off (1104b). Recorded, not fatal. */
|
|
253981
|
+
RULESET_STRICT_POLICY_OFF: "ruleset_strict_policy_off",
|
|
253982
|
+
/** 1102 — no active targeting ruleset requires a workflow at all. */
|
|
253983
|
+
REQUIRED_WORKFLOW_ABSENT: "required_workflow_absent",
|
|
253984
|
+
/** 1102 — a workflows rule exists but carries no sha, so it names a path and not bytes. */
|
|
253985
|
+
REQUIRED_WORKFLOW_UNPINNED: "required_workflow_unpinned",
|
|
253986
|
+
/** 1102 — a required workflow is pinned to a full commit sha. */
|
|
253987
|
+
REQUIRED_WORKFLOW_SHA_PINNED: "required_workflow_sha_pinned",
|
|
253978
253988
|
REQUIRED_CHECK_ABSENT: "required_check_absent",
|
|
253979
253989
|
ACTIVE_JOB: "active_job",
|
|
253980
253990
|
NO_ACTIVE_JOB: "no_active_job",
|
|
@@ -254005,6 +254015,41 @@ var require_github_enforcement = __commonJS({
|
|
|
254005
254015
|
const n = Number(value);
|
|
254006
254016
|
return Number.isInteger(n) && n > 0 ? n : null;
|
|
254007
254017
|
}
|
|
254018
|
+
function refPatterns(rs, key) {
|
|
254019
|
+
const rn = rs && rs.conditions && rs.conditions.ref_name;
|
|
254020
|
+
if (!rn || !Array.isArray(rn[key])) return null;
|
|
254021
|
+
return rn[key].map((x) => String(x));
|
|
254022
|
+
}
|
|
254023
|
+
function targetsDefaultBranch(entry, defaultBranch) {
|
|
254024
|
+
if (entry.target === "tag") return false;
|
|
254025
|
+
const inc = entry.ref_include;
|
|
254026
|
+
if (!Array.isArray(inc)) return null;
|
|
254027
|
+
const ref = defaultBranch ? `refs/heads/${defaultBranch}` : null;
|
|
254028
|
+
const matches = (pat) => {
|
|
254029
|
+
if (pat === "~ALL" || pat === "~DEFAULT_BRANCH") return true;
|
|
254030
|
+
if (!ref) return null;
|
|
254031
|
+
if (pat === ref) return true;
|
|
254032
|
+
if (!pat.includes("*") && !pat.includes("[") && !pat.includes("?")) return false;
|
|
254033
|
+
const rx = new RegExp(`^${pat.replace(/[.+^${}()|\\]/g, "\\$&").replace(/\*/g, "[^/]*").replace(/\?/g, ".")}$`);
|
|
254034
|
+
return rx.test(ref);
|
|
254035
|
+
};
|
|
254036
|
+
let included = false;
|
|
254037
|
+
for (const pat of inc) {
|
|
254038
|
+
const m = matches(pat);
|
|
254039
|
+
if (m === null) return null;
|
|
254040
|
+
if (m) {
|
|
254041
|
+
included = true;
|
|
254042
|
+
break;
|
|
254043
|
+
}
|
|
254044
|
+
}
|
|
254045
|
+
if (!included) return false;
|
|
254046
|
+
for (const pat of entry.ref_exclude || []) {
|
|
254047
|
+
const m = matches(pat);
|
|
254048
|
+
if (m === null) return null;
|
|
254049
|
+
if (m) return false;
|
|
254050
|
+
}
|
|
254051
|
+
return true;
|
|
254052
|
+
}
|
|
254008
254053
|
function rulesetRequiredCheckEntries(rulesets) {
|
|
254009
254054
|
const out = [];
|
|
254010
254055
|
const list = Array.isArray(rulesets) ? rulesets : [];
|
|
@@ -254023,7 +254068,15 @@ var require_github_enforcement = __commonJS({
|
|
|
254023
254068
|
enforcement: typeof rs.enforcement === "string" ? rs.enforcement : null,
|
|
254024
254069
|
integration_id: numericAppId(c && c.integration_id),
|
|
254025
254070
|
bypass_visible: Object.prototype.hasOwnProperty.call(rs, "bypass_actors"),
|
|
254026
|
-
bypass_count: Array.isArray(rs.bypass_actors) ? rs.bypass_actors.length : null
|
|
254071
|
+
bypass_count: Array.isArray(rs.bypass_actors) ? rs.bypass_actors.length : null,
|
|
254072
|
+
// 1104a — a ruleset can be ACTIVE and target nothing that matters. MEASURED against the
|
|
254073
|
+
// live API: `target` and `conditions.ref_name` are both in the detail document we already
|
|
254074
|
+
// fetch, so this is a grading change and needs no new call.
|
|
254075
|
+
target: typeof rs.target === "string" ? rs.target : null,
|
|
254076
|
+
ref_include: refPatterns(rs, "include"),
|
|
254077
|
+
ref_exclude: refPatterns(rs, "exclude"),
|
|
254078
|
+
// 1104b — same document, one boolean.
|
|
254079
|
+
strict_policy: typeof params.strict_required_status_checks_policy === "boolean" ? params.strict_required_status_checks_policy : null
|
|
254027
254080
|
});
|
|
254028
254081
|
}
|
|
254029
254082
|
}
|
|
@@ -254031,7 +254084,7 @@ var require_github_enforcement = __commonJS({
|
|
|
254031
254084
|
return out;
|
|
254032
254085
|
}
|
|
254033
254086
|
var RULESET_BYPASS_CAVEAT = " Bypass actors were not readable on this surface, so this does not establish that nobody can bypass the ruleset.";
|
|
254034
|
-
function evaluateRuleset(rulesets) {
|
|
254087
|
+
function evaluateRuleset(rulesets, defaultBranch = null) {
|
|
254035
254088
|
if (rulesets == null) {
|
|
254036
254089
|
return {
|
|
254037
254090
|
status: STATUS.UNVERIFIABLE,
|
|
@@ -254062,12 +254115,41 @@ var require_github_enforcement = __commonJS({
|
|
|
254062
254115
|
}
|
|
254063
254116
|
const bound = active.find((e) => e.integration_id === ENFORCING_ISSUER_APP_ID);
|
|
254064
254117
|
if (bound) {
|
|
254118
|
+
const targeted = targetsDefaultBranch(bound, defaultBranch);
|
|
254119
|
+
if (targeted === false) {
|
|
254120
|
+
return {
|
|
254121
|
+
status: STATUS.NOT_VERIFIED,
|
|
254122
|
+
reason_code: REASON.RULESET_REF_NOT_TARGETED,
|
|
254123
|
+
reason: `active ruleset "${bound.ruleset}" requires "${CHECK_NAME}" bound to the enforcing issuer, but its conditions do not target ${defaultBranch ? `refs/heads/${defaultBranch}` : "the default branch"}${bound.target === "tag" ? " (the ruleset targets tags, which never gate a merge to a branch)" : ""} \u2014 include: ${JSON.stringify(bound.ref_include)}, exclude: ${JSON.stringify(bound.ref_exclude || [])}`,
|
|
254124
|
+
ruleset_names: names,
|
|
254125
|
+
app_id: ENFORCING_ISSUER_APP_ID,
|
|
254126
|
+
ref_targeted: false
|
|
254127
|
+
};
|
|
254128
|
+
}
|
|
254129
|
+
if (targeted === null) {
|
|
254130
|
+
return {
|
|
254131
|
+
status: STATUS.UNVERIFIABLE,
|
|
254132
|
+
reason_code: REASON.RULESET_REF_NOT_TARGETED,
|
|
254133
|
+
reason: `active ruleset "${bound.ruleset}" binds "${CHECK_NAME}" to the enforcing issuer, but its ref conditions could not be read, so whether it targets the branch that matters CANNOT BE DETERMINED FROM HERE.${RULESET_BYPASS_CAVEAT}`,
|
|
254134
|
+
ruleset_names: names,
|
|
254135
|
+
app_id: ENFORCING_ISSUER_APP_ID,
|
|
254136
|
+
ref_targeted: null
|
|
254137
|
+
};
|
|
254138
|
+
}
|
|
254065
254139
|
return {
|
|
254066
254140
|
status: STATUS.VERIFIED,
|
|
254067
254141
|
reason_code: REASON.RULESET_ISSUER_BOUND,
|
|
254068
|
-
reason: `active ruleset "${bound.ruleset}" requires "${CHECK_NAME}" bound to integration_id ${ENFORCING_ISSUER_APP_ID} (${GITHUB_ACTIONS_APP_SLUG}) \u2014 the contract-gate Action, which concludes failure and blocks the merge
|
|
254142
|
+
reason: `active ruleset "${bound.ruleset}" requires "${CHECK_NAME}" bound to integration_id ${ENFORCING_ISSUER_APP_ID} (${GITHUB_ACTIONS_APP_SLUG}) \u2014 the contract-gate Action, which concludes failure and blocks the merge. It targets ${defaultBranch ? `refs/heads/${defaultBranch}` : "the default branch"}.` + (bound.strict_policy === true ? " strict_required_status_checks_policy is ON, so a stale head cannot merge." : bound.strict_policy === false ? " RESIDUAL: strict_required_status_checks_policy is OFF, so a branch whose head is behind the base can merge on a check that passed against older bytes." : " strict_required_status_checks_policy was not readable here.") + RULESET_BYPASS_CAVEAT,
|
|
254069
254143
|
ruleset_names: names,
|
|
254070
|
-
app_id: ENFORCING_ISSUER_APP_ID
|
|
254144
|
+
app_id: ENFORCING_ISSUER_APP_ID,
|
|
254145
|
+
ref_targeted: true,
|
|
254146
|
+
strict_policy: bound.strict_policy,
|
|
254147
|
+
strict_policy_residual: bound.strict_policy === false ? REASON.RULESET_STRICT_POLICY_OFF : null,
|
|
254148
|
+
// 1104c — NEVER an absence we cannot see. bypass_actors is in the ruleset schema but was
|
|
254149
|
+
// absent from all six live rulesets read without a credential (measured 2026-08-27), so
|
|
254150
|
+
// "not present in the response" is reported as NOT VISIBLE and never as "none configured".
|
|
254151
|
+
bypass_visible: bound.bypass_visible === true,
|
|
254152
|
+
bypass_count: bound.bypass_visible === true ? bound.bypass_count : null
|
|
254071
254153
|
};
|
|
254072
254154
|
}
|
|
254073
254155
|
const other = active.find((e) => e.integration_id != null);
|
|
@@ -254088,9 +254170,66 @@ var require_github_enforcement = __commonJS({
|
|
|
254088
254170
|
ruleset_names: names
|
|
254089
254171
|
};
|
|
254090
254172
|
}
|
|
254091
|
-
function
|
|
254173
|
+
function evaluateRequiredWorkflowPin(rulesets, defaultBranch = null) {
|
|
254174
|
+
if (rulesets == null) {
|
|
254175
|
+
return { status: STATUS.UNVERIFIABLE, reason_code: REASON.RULESET_UNREADABLE, pinned: null, workflows: [] };
|
|
254176
|
+
}
|
|
254177
|
+
const found = [];
|
|
254178
|
+
for (const rs of Array.isArray(rulesets) ? rulesets : []) {
|
|
254179
|
+
if (!rs || typeof rs !== "object") continue;
|
|
254180
|
+
if (rs.enforcement !== "active") continue;
|
|
254181
|
+
const entryShape = {
|
|
254182
|
+
target: typeof rs.target === "string" ? rs.target : null,
|
|
254183
|
+
ref_include: refPatterns(rs, "include"),
|
|
254184
|
+
ref_exclude: refPatterns(rs, "exclude")
|
|
254185
|
+
};
|
|
254186
|
+
if (targetsDefaultBranch(entryShape, defaultBranch) !== true) continue;
|
|
254187
|
+
for (const rule of Array.isArray(rs.rules) ? rs.rules : []) {
|
|
254188
|
+
if (!rule || rule.type !== "workflows") continue;
|
|
254189
|
+
const list = rule.parameters && Array.isArray(rule.parameters.workflows) ? rule.parameters.workflows : [];
|
|
254190
|
+
for (const w of list) {
|
|
254191
|
+
if (!w || typeof w !== "object") continue;
|
|
254192
|
+
found.push({
|
|
254193
|
+
ruleset: String(rs.name || rs.id || "ruleset"),
|
|
254194
|
+
path: typeof w.path === "string" ? w.path : null,
|
|
254195
|
+
repository_id: Number.isInteger(w.repository_id) ? w.repository_id : null,
|
|
254196
|
+
ref: typeof w.ref === "string" ? w.ref : null,
|
|
254197
|
+
// The whole question. sha is optional in the schema, so its ABSENCE is the finding.
|
|
254198
|
+
sha: typeof w.sha === "string" && /^[0-9a-f]{40}$/i.test(w.sha) ? w.sha : null
|
|
254199
|
+
});
|
|
254200
|
+
}
|
|
254201
|
+
}
|
|
254202
|
+
}
|
|
254203
|
+
if (found.length === 0) {
|
|
254204
|
+
return {
|
|
254205
|
+
status: STATUS.NOT_VERIFIED,
|
|
254206
|
+
reason_code: REASON.REQUIRED_WORKFLOW_ABSENT,
|
|
254207
|
+
pinned: false,
|
|
254208
|
+
workflows: [],
|
|
254209
|
+
reason: `no active ruleset targeting this branch requires a workflow, so the check that gates the merge is bound only to the SHARED issuer ${ENFORCING_ISSUER_APP_ID} (${GITHUB_ACTIONS_APP_SLUG}) \u2014 anyone who can edit .github/workflows can post under it`
|
|
254210
|
+
};
|
|
254211
|
+
}
|
|
254212
|
+
const pinned = found.filter((w) => w.sha != null);
|
|
254213
|
+
if (pinned.length === 0) {
|
|
254214
|
+
return {
|
|
254215
|
+
status: STATUS.NOT_VERIFIED,
|
|
254216
|
+
reason_code: REASON.REQUIRED_WORKFLOW_UNPINNED,
|
|
254217
|
+
pinned: false,
|
|
254218
|
+
workflows: found,
|
|
254219
|
+
reason: `a required workflow is configured (${found.map((w) => w.path || "?").join(", ")}) but NONE carries a sha. \`sha\` is optional in the rule schema, and without it the ruleset names a path rather than bytes, so rewriting that file still changes what runs`
|
|
254220
|
+
};
|
|
254221
|
+
}
|
|
254222
|
+
return {
|
|
254223
|
+
status: STATUS.VERIFIED,
|
|
254224
|
+
reason_code: REASON.REQUIRED_WORKFLOW_SHA_PINNED,
|
|
254225
|
+
pinned: true,
|
|
254226
|
+
workflows: found,
|
|
254227
|
+
reason: `active ruleset "${pinned[0].ruleset}" requires workflow ${pinned[0].path} pinned to sha ${pinned[0].sha} in repository_id ${pinned[0].repository_id}. Moving that pin requires editing the ruleset (repository administration), which is a different permission from workflow write. This does NOT establish that any pull request ran it, nor that bypass actors cannot skip the ruleset.`
|
|
254228
|
+
};
|
|
254229
|
+
}
|
|
254230
|
+
function evaluateRequiredCheckAcrossSurfaces(protection, rulesets, defaultBranch = null) {
|
|
254092
254231
|
const classic = evaluateRequiredCheck(protection);
|
|
254093
|
-
const ruleset = evaluateRuleset(rulesets);
|
|
254232
|
+
const ruleset = evaluateRuleset(rulesets, defaultBranch);
|
|
254094
254233
|
const merged = (base) => ({
|
|
254095
254234
|
...base,
|
|
254096
254235
|
surfaces_inspected: ["classic_branch_protection", rulesets == null ? "rulesets:unread" : "rulesets"],
|
|
@@ -254210,8 +254349,22 @@ var require_github_enforcement = __commonJS({
|
|
|
254210
254349
|
var STATUS = Object.freeze({
|
|
254211
254350
|
VERIFIED: "VERIFIED",
|
|
254212
254351
|
NOT_VERIFIED: "NOT_VERIFIED",
|
|
254213
|
-
UNVERIFIABLE: "UNVERIFIABLE"
|
|
254214
|
-
|
|
254352
|
+
UNVERIFIABLE: "UNVERIFIABLE",
|
|
254353
|
+
UNSUPPORTED_PLAN: "UNSUPPORTED_PLAN",
|
|
254354
|
+
PROVIDER_ERROR: "PROVIDER_ERROR"
|
|
254355
|
+
});
|
|
254356
|
+
var FRESHNESS = Object.freeze({ FRESH: "FRESH", STALE_EVIDENCE: "STALE_EVIDENCE" });
|
|
254357
|
+
function supportsStrongClaim(status) {
|
|
254358
|
+
return status === STATUS.VERIFIED;
|
|
254359
|
+
}
|
|
254360
|
+
var EVIDENCE_TTL_SECONDS = 3600;
|
|
254361
|
+
function evidenceFreshness(report, nowIso) {
|
|
254362
|
+
if (!report || !report.expires_at) return null;
|
|
254363
|
+
const now = Date.parse(nowIso || (/* @__PURE__ */ new Date()).toISOString());
|
|
254364
|
+
const exp = Date.parse(report.expires_at);
|
|
254365
|
+
if (!Number.isFinite(now) || !Number.isFinite(exp)) return null;
|
|
254366
|
+
return now > exp ? FRESHNESS.STALE_EVIDENCE : FRESHNESS.FRESH;
|
|
254367
|
+
}
|
|
254215
254368
|
function resolveGitHubAuth(env = process.env) {
|
|
254216
254369
|
const user = String(env && (env.GITHUB_TOKEN || env.GH_TOKEN) || "").trim();
|
|
254217
254370
|
if (user) {
|
|
@@ -254542,6 +254695,10 @@ var require_github_enforcement = __commonJS({
|
|
|
254542
254695
|
NO_TOKEN_INSTRUCTION,
|
|
254543
254696
|
LAYER_IDS,
|
|
254544
254697
|
STATUS,
|
|
254698
|
+
FRESHNESS,
|
|
254699
|
+
EVIDENCE_TTL_SECONDS,
|
|
254700
|
+
supportsStrongClaim,
|
|
254701
|
+
evidenceFreshness,
|
|
254545
254702
|
REASON,
|
|
254546
254703
|
CHECK_NAME,
|
|
254547
254704
|
GATE_ACTION,
|
|
@@ -254555,6 +254712,8 @@ var require_github_enforcement = __commonJS({
|
|
|
254555
254712
|
evaluateLayers,
|
|
254556
254713
|
evaluateRequiredCheck,
|
|
254557
254714
|
evaluateRuleset,
|
|
254715
|
+
evaluateRequiredWorkflowPin,
|
|
254716
|
+
targetsDefaultBranch,
|
|
254558
254717
|
evaluateRequiredCheckAcrossSurfaces,
|
|
254559
254718
|
verifiedRequiresBinding,
|
|
254560
254719
|
scanLocalWorkflows,
|
|
@@ -254941,7 +255100,7 @@ var require_setup_required_check = __commonJS({
|
|
|
254941
255100
|
}
|
|
254942
255101
|
} catch {
|
|
254943
255102
|
}
|
|
254944
|
-
const rulesetVerdict = evaluateRuleset(rulesetDocs);
|
|
255103
|
+
const rulesetVerdict = evaluateRuleset(rulesetDocs, branch);
|
|
254945
255104
|
const rulesetNamed = detectRulesetRequiredCheck(rulesetDocs || [], CHECK_NAME);
|
|
254946
255105
|
const ruleset = {
|
|
254947
255106
|
enforced: rulesetVerdict.status === STATUS.VERIFIED,
|
|
@@ -255314,6 +255473,8 @@ var require_enforce_check = __commonJS({
|
|
|
255314
255473
|
var chalk = require_source();
|
|
255315
255474
|
var {
|
|
255316
255475
|
EVIDENCE_SPEC,
|
|
255476
|
+
EVIDENCE_TTL_SECONDS,
|
|
255477
|
+
supportsStrongClaim,
|
|
255317
255478
|
APP_INSTALLATION,
|
|
255318
255479
|
NO_TOKEN_INSTRUCTION,
|
|
255319
255480
|
resolveGitHubAuth,
|
|
@@ -255325,6 +255486,35 @@ var require_enforce_check = __commonJS({
|
|
|
255325
255486
|
var { isValidRepo } = require_status();
|
|
255326
255487
|
var { resolveOwnerRepo } = require_setup_required_check();
|
|
255327
255488
|
if (process.env.NO_COLOR) chalk.level = 0;
|
|
255489
|
+
var crypto = require("node:crypto");
|
|
255490
|
+
var EVIDENCE_SCOPE = Object.freeze({
|
|
255491
|
+
grades: "repository configuration \u2014 what the provider would require of any pull request",
|
|
255492
|
+
covers: Object.freeze([
|
|
255493
|
+
"required status check present and named exactly",
|
|
255494
|
+
"the check bound to an issuer that can block (integration_id / app_id)",
|
|
255495
|
+
"ruleset enforcement is active rather than evaluate",
|
|
255496
|
+
"the ruleset targets the branch that matters",
|
|
255497
|
+
"strict latest-head policy recorded",
|
|
255498
|
+
"bypass actors reported as VISIBLE or NOT VISIBLE, never as absent"
|
|
255499
|
+
]),
|
|
255500
|
+
does_not_cover: Object.freeze([
|
|
255501
|
+
"which GitHub App actually posted a check run on any pull request",
|
|
255502
|
+
"the head_sha a check run executed against",
|
|
255503
|
+
"whether any check run concluded success",
|
|
255504
|
+
"a recomputable external proof hash over a specific run"
|
|
255505
|
+
]),
|
|
255506
|
+
why_not: "Those are facts about one pull request at one moment, not about configuration. Reading them needs a per-PR check-runs query this command does not make; a passing config is not a passing run."
|
|
255507
|
+
});
|
|
255508
|
+
function responseDigest(read) {
|
|
255509
|
+
if (!read) return null;
|
|
255510
|
+
return {
|
|
255511
|
+
endpoint: read.endpoint ? `GET ${read.endpoint}` : null,
|
|
255512
|
+
http_status: read.status == null ? null : read.status,
|
|
255513
|
+
// Hash of the BODY as we received it. Not the verdict — the INPUT to the verdict, so a
|
|
255514
|
+
// disagreement can be localised to the answer or to our grading of it.
|
|
255515
|
+
body_sha256: read.body === void 0 || read.body === null ? null : `sha256:${crypto.createHash("sha256").update(JSON.stringify(read.body)).digest("hex")}`
|
|
255516
|
+
};
|
|
255517
|
+
}
|
|
255328
255518
|
var USAGE = [
|
|
255329
255519
|
"Usage: coderifts enforce --check --repo owner/repo [--env <github-environment>] [--branch <name>] [--json]",
|
|
255330
255520
|
"",
|
|
@@ -255437,7 +255627,12 @@ var require_enforce_check = __commonJS({
|
|
|
255437
255627
|
const report2 = {
|
|
255438
255628
|
command: "enforce --check",
|
|
255439
255629
|
spec: EVIDENCE_SPEC,
|
|
255630
|
+
// OBSERVED_AT WAS NOT ADDED. `queried_at` already carries exactly that fact, and a
|
|
255631
|
+
// second name for one fact is the defect this round was told to look for.
|
|
255440
255632
|
queried_at: queriedAt,
|
|
255633
|
+
expires_at: new Date(Date.parse(queriedAt) + EVIDENCE_TTL_SECONDS * 1e3).toISOString(),
|
|
255634
|
+
evidence_ttl_seconds: EVIDENCE_TTL_SECONDS,
|
|
255635
|
+
evidence_scope: EVIDENCE_SCOPE,
|
|
255441
255636
|
repo: `${ownerRepo.owner}/${ownerRepo.repo}`,
|
|
255442
255637
|
branch: options.branch || null,
|
|
255443
255638
|
environment: environmentName,
|
|
@@ -255481,14 +255676,33 @@ var require_enforce_check = __commonJS({
|
|
|
255481
255676
|
const report = {
|
|
255482
255677
|
command: "enforce --check",
|
|
255483
255678
|
spec: EVIDENCE_SPEC,
|
|
255679
|
+
// OBSERVED_AT WAS NOT ADDED. `queried_at` already carries exactly that fact, and a
|
|
255680
|
+
// second name for one fact is the defect this round was told to look for.
|
|
255484
255681
|
queried_at: queriedAt,
|
|
255682
|
+
expires_at: new Date(Date.parse(queriedAt) + EVIDENCE_TTL_SECONDS * 1e3).toISOString(),
|
|
255683
|
+
evidence_ttl_seconds: EVIDENCE_TTL_SECONDS,
|
|
255684
|
+
evidence_scope: EVIDENCE_SCOPE,
|
|
255485
255685
|
repo: `${ownerRepo.owner}/${ownerRepo.repo}`,
|
|
255486
255686
|
branch,
|
|
255487
255687
|
environment: environmentName,
|
|
255488
255688
|
provider: "github",
|
|
255489
255689
|
auth: authPublic,
|
|
255690
|
+
// The raw answers behind the verdict, so a disagreement can be localised to the provider's
|
|
255691
|
+
// response or to our grading of it.
|
|
255692
|
+
responses: {
|
|
255693
|
+
protection: responseDigest(protectionRead),
|
|
255694
|
+
environment: responseDigest(environmentRead)
|
|
255695
|
+
},
|
|
255490
255696
|
...evaluated
|
|
255491
255697
|
};
|
|
255698
|
+
if (report.claim && report.claim.inescapable_deploy === true) {
|
|
255699
|
+
const unsupported = (report.layers || []).filter((l) => !supportsStrongClaim(l.status));
|
|
255700
|
+
if (unsupported.length > 0) {
|
|
255701
|
+
throw new Error(
|
|
255702
|
+
`enforce --check: inescapable_deploy is true with ${unsupported.length} layer(s) not VERIFIED (${unsupported.map((l) => `${l.id}=${l.status}`).join(", ")}) \u2014 refusing to emit`
|
|
255703
|
+
);
|
|
255704
|
+
}
|
|
255705
|
+
}
|
|
255492
255706
|
if (options.json) log(JSON.stringify(report, null, 2));
|
|
255493
255707
|
else log(renderHuman(report));
|
|
255494
255708
|
return { exitCode: 0, report };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coderifts",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"description": "Detect breaking API changes from the command line. Works locally or with the CodeRifts cloud API.",
|
|
5
5
|
"author": "CodeRifts <hello@coderifts.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"scripts": {
|
|
41
41
|
"build": "node scripts/copy-corpus.js && node scripts/build.js",
|
|
42
|
-
"prepublishOnly": "node scripts/assert-guard-major.js && bash ../../scripts/freeze-gate.sh && npm run build",
|
|
42
|
+
"prepublishOnly": "node scripts/assert-guard-major.js && node scripts/assert-changelog-version.js && bash ../../scripts/freeze-gate.sh && npm run build",
|
|
43
43
|
"assert-guard-major": "node scripts/assert-guard-major.js",
|
|
44
44
|
"postinstall": "node scripts/postinstall.js",
|
|
45
45
|
"test": "node --test test/*.test.js"
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* CLI release gate (prepublishOnly): refuse to publish a version the CHANGELOG does not name.
|
|
6
|
+
*
|
|
7
|
+
* WHY THIS EXISTS — measured, not hypothetical. On 2026-08-27 packages/cli/CHANGELOG.md listed
|
|
8
|
+
* 4.10.0 and then "Unreleased", while npm carried 4.11.0, 4.12.0, 5.0.0 and 5.0.1. Four published
|
|
9
|
+
* versions were undocumented and a fifth (5.0.0) was published while still labelled Unreleased.
|
|
10
|
+
* Two distinct failure modes produced that, and BOTH are caught by the one check below:
|
|
11
|
+
*
|
|
12
|
+
* 1. The bump commit never touched the changelog at all. Measured: of the last eight CLI version
|
|
13
|
+
* bumps, four omitted it (4.11.0, 4.12.0, 4.13.0, 5.0.1) — and those four are exactly the
|
|
14
|
+
* versions that went missing. The correlation is not partial.
|
|
15
|
+
* 2. The bump commit DID write the entry, but under "## Unreleased", and nobody retitled it at
|
|
16
|
+
* publish. Atomicity alone does not save you: @coderifts/agent-guard 12.0.0 was written
|
|
17
|
+
* atomically and still shipped as "Unreleased", and 11.0.1 before it had to retitle a stale
|
|
18
|
+
* heading by hand.
|
|
19
|
+
*
|
|
20
|
+
* So the invariant is NOT "the bump touched the changelog" — it is that the version being published
|
|
21
|
+
* appears as its own heading. A human step that must be remembered at publish time is the step that
|
|
22
|
+
* gets skipped; this makes it fail the publish instead.
|
|
23
|
+
*
|
|
24
|
+
* Dependency-free on purpose: a publish gate that needs an install to run is a gate that stops
|
|
25
|
+
* running (same reasoning as assert-guard-major.js).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const fs = require('fs');
|
|
29
|
+
const path = require('path');
|
|
30
|
+
|
|
31
|
+
const PKG_PATH = path.join(__dirname, '..', 'package.json');
|
|
32
|
+
const CHANGELOG_PATH = path.join(__dirname, '..', 'CHANGELOG.md');
|
|
33
|
+
|
|
34
|
+
function fail(msg) {
|
|
35
|
+
console.error(`assert-changelog-version: FAIL — ${msg}`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Headings this file recognises as "this version is documented".
|
|
41
|
+
* A heading may carry a trailing note (e.g. "## 4.13.0 — never published"), so match the version
|
|
42
|
+
* token at a word boundary rather than requiring the line to be exactly the version.
|
|
43
|
+
*/
|
|
44
|
+
function headingNamesVersion(line, version) {
|
|
45
|
+
if (!/^##\s+/.test(line)) return false;
|
|
46
|
+
const rest = line.replace(/^##\s+/, '').trim();
|
|
47
|
+
const esc = version.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
48
|
+
return new RegExp(`^v?${esc}(\\b|$)`).test(rest);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function main() {
|
|
52
|
+
if (!fs.existsSync(PKG_PATH)) fail(`package.json not found at ${PKG_PATH}`);
|
|
53
|
+
if (!fs.existsSync(CHANGELOG_PATH)) fail(`CHANGELOG.md not found at ${CHANGELOG_PATH}`);
|
|
54
|
+
|
|
55
|
+
let version;
|
|
56
|
+
try {
|
|
57
|
+
version = JSON.parse(fs.readFileSync(PKG_PATH, 'utf8')).version;
|
|
58
|
+
} catch (err) {
|
|
59
|
+
fail(`package.json is not readable JSON: ${err.message}`);
|
|
60
|
+
}
|
|
61
|
+
if (typeof version !== 'string' || !version.trim()) fail('package.json has no version string');
|
|
62
|
+
|
|
63
|
+
const lines = fs.readFileSync(CHANGELOG_PATH, 'utf8').split('\n');
|
|
64
|
+
const headings = lines.filter((l) => /^##\s+/.test(l));
|
|
65
|
+
if (headings.length === 0) fail('CHANGELOG.md has no "## " headings at all');
|
|
66
|
+
|
|
67
|
+
const match = headings.find((l) => headingNamesVersion(l, version));
|
|
68
|
+
if (!match) {
|
|
69
|
+
const listed = headings.slice(0, 4).map((h) => h.replace(/^##\s+/, '').trim());
|
|
70
|
+
fail(
|
|
71
|
+
`package.json is ${version} but CHANGELOG.md has no "## ${version}" heading.\n`
|
|
72
|
+
+ ` Top headings are: ${listed.join(' · ')}\n`
|
|
73
|
+
+ ' Publishing now is how a version goes undocumented. Retitle the Unreleased section to\n'
|
|
74
|
+
+ ` ${version}, or add its section, then publish.`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// "Unreleased" must never be the section that documents the version being published.
|
|
79
|
+
if (/^##\s+unreleased\b/i.test(match)) {
|
|
80
|
+
fail(`the section naming ${version} is the Unreleased heading — retitle it before publishing`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.log(`assert-changelog-version: OK — ${version} is documented ("${match.trim()}")`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
main();
|
|
87
|
+
|
|
88
|
+
module.exports = { headingNamesVersion, PKG_PATH, CHANGELOG_PATH };
|