coderifts 4.8.0 → 4.9.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 CHANGED
@@ -1,5 +1,36 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.9.0
4
+
5
+ ### Fixed
6
+ - **P0 / ID621 — `enforce --check` no longer reports `inescapable_deploy: true`
7
+ for an unenforced configuration.** External audit 2026-08-24 reproduced two
8
+ holes on the published CLI: (a) the required check matched by **name only**
9
+ (`required_status_checks.checks[]` with no `app_id`), so any repository
10
+ writer could satisfy it with a same-named success status; (b) the two local
11
+ workflow checks were **text searches**, so `# uses: coderifts/contract-gate@v0`
12
+ in a comment counted as VERIFIED. Both now fail closed.
13
+
14
+ ### Changed
15
+ - **`--json` spec `provider-enforcement-evidence.v2`.** The meaning of
16
+ `VERIFIED` changed. A layer whose evidence cannot be bound to an issuer
17
+ (CodeRifts GitHub App id, public `GET /apps/coderifts`) or to an **active**
18
+ workflow job is `NOT_VERIFIED` or `UNVERIFIABLE` — never `VERIFIED`.
19
+ `inescapable_deploy` is true only when every layer is VERIFIED under these
20
+ rules.
21
+ - **Issuer binding:** `required_check` is VERIFIED only when `checks[]`
22
+ carries our App's numeric `app_id`. Legacy `contexts: string[]` is
23
+ `legacy_contexts_no_issuer_binding`. Name-only `checks[]` is
24
+ `no_issuer_binding`. A different `app_id` is `issuer_app_id_mismatch`.
25
+ - **YAML AST, not grep:** workflow files are parsed with `js-yaml` (already a
26
+ CLI dependency — no new package). VERIFIED requires a job that is not
27
+ `if: false` and a step whose `uses:` is `coderifts/contract-gate` at a
28
+ pinned major, or a `run:` that actually invokes `coderifts deploy-gate`.
29
+ Comments, string literals in other keys, and commented-out steps fail.
30
+ - **Environment correlation:** `environment_protection` is VERIFIED only when
31
+ `--env` names a protected GitHub Environment **and** an active deploy-gate
32
+ job sets that same `environment:` key (`environment_mismatch` otherwise).
33
+
3
34
  ## 4.8.0
4
35
 
5
36
  ### Fixed
package/README.md CHANGED
@@ -437,12 +437,14 @@ Provider-native pipeline-protection evidence. This is **not** a host claim:
437
437
 
438
438
  | Layer | VERIFIED when |
439
439
  |-------|----------------|
440
- | `required_check` | Default-branch protection requires `CodeRifts / contract-gate` |
440
+ | `required_check` | Default-branch protection `required_status_checks.checks[]` lists `CodeRifts / contract-gate` **bound to the CodeRifts GitHub App `app_id`** (public metadata: `GET https://api.github.com/apps/coderifts`). Name-only `checks[]` (`app_id` null) is `no_issuer_binding`. The legacy `contexts` array has no issuer field → `legacy_contexts_no_issuer_binding`. Both are **NOT_VERIFIED** (ID621). |
441
441
  | `enforce_admins` | `enforce_admins.enabled` is **true** (admins cannot bypass). `false` = NOT_VERIFIED (admin bypass open) |
442
442
  | `strict_status_checks` | `required_status_checks.strict` is true |
443
- | `environment_protection` | `--env` names a GitHub Environment with protection rules or a deployment branch policy |
444
- | `workflow_contract_gate` | a local `.github/workflows` file references `coderifts/contract-gate@v0` |
445
- | `workflow_deploy_gate` | a local workflow runs `coderifts deploy-gate` |
443
+ | `environment_protection` | `--env` names a GitHub Environment with protection rules **and** an active deploy-gate job sets that same `environment:` (mismatch → `environment_mismatch`) |
444
+ | `workflow_contract_gate` | an **active** job (not `if: false`) has a step whose `uses:` is `coderifts/contract-gate` at a pinned major. Comments, string literals, and commented-out steps are **NOT_VERIFIED** |
445
+ | `workflow_deploy_gate` | an **active** job step **invokes** `coderifts deploy-gate` (not `echo` of the string; not a comment) |
446
+
447
+ **Invariant:** any layer whose evidence cannot be bound to an issuer (our GitHub App id) or to an active job is `NOT_VERIFIED` or `UNVERIFIABLE` — never `VERIFIED`. `inescapable_deploy` remains true only when every layer is `VERIFIED` under that rule.
446
448
 
447
449
  Statuses: `VERIFIED` / `NOT_VERIFIED` / `UNVERIFIABLE(reason)`. Auth:
448
450
  `GITHUB_TOKEN` (never printed). **No token** → API layers `UNVERIFIABLE` with
@@ -451,9 +453,11 @@ CodeRifts GitHub App **cannot** query branch protection (permissions are
451
453
  contents / pull requests / checks — not `administration:read`).
452
454
 
453
455
  `--json` is the quotable evidence document (`spec`:
454
- `provider-enforcement-evidence.v1`): each layer's status, the API endpoint
455
- consulted, `queried_at`. Exit **0** when a report was produced (evidence, not
456
- a gate). GitLab/Bitbucket: no adapter `UNVERIFIABLE`, never stubbed.
456
+ `provider-enforcement-evidence.v2` VERIFIED now requires issuer binding and
457
+ an active YAML-AST job; v1 treated name-only checks and text search as
458
+ VERIFIED): each layer's status, `reason_code`, the API endpoint consulted,
459
+ `queried_at`. Exit **0** when a report was produced (evidence, not a gate).
460
+ GitLab/Bitbucket: no adapter — `UNVERIFIABLE`, never stubbed.
457
461
 
458
462
  ### Agent host files (`coderifts agent-setup`)
459
463
 
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: "4.8.0",
3031
+ version: "4.9.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",
@@ -200889,9 +200889,20 @@ var require_decision_result_v1_producer = __commonJS({
200889
200889
  source: {
200890
200890
  type: "string",
200891
200891
  enum: [
200892
- "github_compare"
200892
+ "github_compare",
200893
+ "gitlab_compare",
200894
+ "bitbucket_compare"
200893
200895
  ],
200894
- description: "How the server listed the change-set. github_compare = GitHub Compare API via the App installation."
200896
+ description: "How the server listed the change-set. github_compare = GitHub Compare API via the App installation. gitlab_compare / bitbucket_compare = platform Compare via a caller-supplied short-lived SCM token (never stored)."
200897
+ },
200898
+ platform: {
200899
+ type: "string",
200900
+ enum: [
200901
+ "github",
200902
+ "gitlab",
200903
+ "bitbucket"
200904
+ ],
200905
+ description: "Additive P1-6. SCM platform that produced this SERVER_DERIVED set. Taken from the proven binding / context.platform \u2014 never guessed from the repository string. Absent on envelopes minted before this field existed."
200895
200906
  },
200896
200907
  base_sha: {
200897
200908
  type: "string",
@@ -201799,6 +201810,9 @@ var require_change_set_completeness = __commonJS({
201799
201810
  if (v == null) return "";
201800
201811
  return typeof v === "string" ? v : JSON.stringify(v);
201801
201812
  }
201813
+ function afterContentHash(content) {
201814
+ return sha256hexLocal(specStr(content));
201815
+ }
201802
201816
  function artifactPath(artifact) {
201803
201817
  if (!artifact || typeof artifact !== "object") return "";
201804
201818
  if (typeof artifact.path === "string" && artifact.path.trim()) return artifact.path.trim();
@@ -202097,7 +202111,8 @@ var require_change_set_completeness = __commonJS({
202097
202111
  contract_selector_hash,
202098
202112
  completeness_count: normalizedLeaves.length,
202099
202113
  submitted_set_digest,
202100
- expected_channel: "webhook_full_file_list"
202114
+ expected_channel: "webhook_full_file_list",
202115
+ leaves: normalizedLeaves
202101
202116
  };
202102
202117
  }
202103
202118
  function assertCompletenessRequirement(block, requireLevel) {
@@ -202181,9 +202196,45 @@ var require_change_set_completeness = __commonJS({
202181
202196
  contract_selector_hash: computeContractSelectorHash(DEFAULT_CONTRACT_SELECTOR),
202182
202197
  completeness_count: leaves.length,
202183
202198
  submitted_set_digest,
202184
- expected_channel: null
202199
+ expected_channel: null,
202200
+ leaves
202185
202201
  };
202186
202202
  }
202203
+ function authorizedPathsFromArtifacts(artifacts) {
202204
+ const list = Array.isArray(artifacts) ? artifacts : [];
202205
+ const seen = /* @__PURE__ */ new Set();
202206
+ const rows = [];
202207
+ for (const a of list) {
202208
+ const path = artifactPath(a);
202209
+ if (!path || seen.has(path)) continue;
202210
+ seen.add(path);
202211
+ rows.push({
202212
+ path,
202213
+ after_hex: sha256hexLocal(specStr(a && a.after)),
202214
+ change_type: deriveChangeType(a && a.before, a && a.after)
202215
+ });
202216
+ }
202217
+ return rows;
202218
+ }
202219
+ function authorizedPathsForPersist(block, artifacts) {
202220
+ const submitted = authorizedPathsFromArtifacts(artifacts);
202221
+ const leaves = block && Array.isArray(block.leaves) ? block.leaves : null;
202222
+ if (!leaves || leaves.length === 0) return submitted;
202223
+ const byPath = /* @__PURE__ */ new Map();
202224
+ for (const L of leaves) {
202225
+ if (!L || typeof L !== "object") continue;
202226
+ const path = String(L.path == null ? "" : L.path).trim();
202227
+ if (!path) continue;
202228
+ const after_hex = String(L.after_hex == null ? "" : L.after_hex).replace(/^sha256:/i, "").toLowerCase();
202229
+ if (!/^[0-9a-f]{64}$/.test(after_hex)) continue;
202230
+ byPath.set(path, {
202231
+ path,
202232
+ after_hex,
202233
+ change_type: normalizeChangeType(L.change_type)
202234
+ });
202235
+ }
202236
+ return submitted.map((row) => byPath.get(row.path) || row);
202237
+ }
202187
202238
  function completenessToEnvelopeFields(block) {
202188
202239
  if (!block || block.completeness_mode === "UNBOUND") {
202189
202240
  return {
@@ -202230,7 +202281,10 @@ var require_change_set_completeness = __commonJS({
202230
202281
  assertNoBooleanCompleteness,
202231
202282
  completenessRequiredError,
202232
202283
  sha256hexLocal,
202233
- specStr
202284
+ specStr,
202285
+ afterContentHash,
202286
+ authorizedPathsFromArtifacts,
202287
+ authorizedPathsForPersist
202234
202288
  };
202235
202289
  }
202236
202290
  });
@@ -202795,7 +202849,8 @@ var require_change_set = __commonJS({
202795
202849
  assertCompletenessRequirement,
202796
202850
  completenessToEnvelopeFields,
202797
202851
  applyProvenRepoBinding,
202798
- assertNoBooleanCompleteness
202852
+ assertNoBooleanCompleteness,
202853
+ authorizedPathsForPersist
202799
202854
  } = require_change_set_completeness();
202800
202855
  var { authorFingerprintBindingEnvelopeFields } = require_fingerprint_binding_gate();
202801
202856
  var NUL = "";
@@ -203522,6 +203577,7 @@ var require_change_set = __commonJS({
203522
203577
  }
203523
203578
  }
203524
203579
  const completenessEnvelope = completenessToEnvelopeFields(completenessBlock);
203580
+ const authorizedPaths = preflightMode === "authorize" ? authorizedPathsForPersist(completenessBlock, artifacts) : [];
203525
203581
  const fingerprintBindingEnvelope = authorFingerprintBindingEnvelopeFields({
203526
203582
  preflightMode
203527
203583
  });
@@ -203583,6 +203639,7 @@ var require_change_set = __commonJS({
203583
203639
  // body_hash stays byte-identical. Covered by body_hash when set; NOT in fingerprint.
203584
203640
  ...input && input.derivation_meta && typeof input.derivation_meta === "object" ? { derivation: {
203585
203641
  source: String(input.derivation_meta.source || "github_compare"),
203642
+ platform: String(input.derivation_meta.platform || "github"),
203586
203643
  base_sha: String(input.derivation_meta.base_sha || ""),
203587
203644
  head_sha: String(input.derivation_meta.head_sha || "")
203588
203645
  } } : {},
@@ -203777,6 +203834,12 @@ var require_change_set = __commonJS({
203777
203834
  }
203778
203835
  } catch (_) {
203779
203836
  }
203837
+ if (preflightMode === "authorize" && Array.isArray(authorizedPaths) && authorizedPaths.length > 0) {
203838
+ Object.defineProperty(out, "_authorized_paths", {
203839
+ value: Object.freeze(authorizedPaths.map((r) => Object.freeze({ ...r }))),
203840
+ enumerable: false
203841
+ });
203842
+ }
203780
203843
  return out;
203781
203844
  }
203782
203845
  module2.exports = {
@@ -237167,6 +237230,162 @@ var require_status = __commonJS({
237167
237230
  }
237168
237231
  });
237169
237232
 
237233
+ // src/provider/workflow-scan.js
237234
+ var require_workflow_scan = __commonJS({
237235
+ "src/provider/workflow-scan.js"(exports2, module2) {
237236
+ "use strict";
237237
+ var yaml = require_js_yaml();
237238
+ var CONTRACT_GATE_ACTION = "coderifts/contract-gate";
237239
+ var DEPLOY_GATE_INVOCATION = /^(?:npx(?:\s+--yes)?\s+|npm\s+exec\s+|pnpm\s+(?:dlx|exec)\s+|yarn(?:\s+(?:dlx|exec))?\s+)?coderifts\s+deploy-gate(?:\s|$)/;
237240
+ var NON_SHELL = /* @__PURE__ */ new Set(["python", "python3", "node", "ruby", "perl", "php", "pwsh", "powershell"]);
237241
+ function isPinnedMajor(ref) {
237242
+ const r = String(ref || "");
237243
+ if (/^v\d+(\.\d+){0,2}$/.test(r)) return true;
237244
+ if (/^[0-9a-f]{40}$/i.test(r)) return true;
237245
+ return false;
237246
+ }
237247
+ function parseActionUses(uses) {
237248
+ const raw = String(uses || "").trim();
237249
+ const at = raw.lastIndexOf("@");
237250
+ if (at <= 0) return { name: raw, ref: "", pinnedMajor: false };
237251
+ return {
237252
+ name: raw.slice(0, at),
237253
+ ref: raw.slice(at + 1),
237254
+ pinnedMajor: isPinnedMajor(raw.slice(at + 1))
237255
+ };
237256
+ }
237257
+ function isContractGateUses(uses) {
237258
+ const parsed = parseActionUses(uses);
237259
+ return parsed.name === CONTRACT_GATE_ACTION && parsed.pinnedMajor === true;
237260
+ }
237261
+ function isExplicitlyDisabled(node) {
237262
+ if (!node || typeof node !== "object" || !Object.prototype.hasOwnProperty.call(node, "if")) {
237263
+ return false;
237264
+ }
237265
+ const ifVal = node.if;
237266
+ if (ifVal === false || ifVal === 0 || ifVal === null) return true;
237267
+ if (typeof ifVal === "string") {
237268
+ const s = ifVal.trim().toLowerCase();
237269
+ if (s === "false" || s === "0" || s === "" || s === "null") return true;
237270
+ if (/^\$\{\{\s*(?:false|0|null)\s*\}\}$/.test(s)) return true;
237271
+ }
237272
+ return false;
237273
+ }
237274
+ function jobEnvironmentName(job) {
237275
+ if (!job || typeof job !== "object") return null;
237276
+ const env = job.environment;
237277
+ if (typeof env === "string") {
237278
+ const name = env.trim();
237279
+ return name || null;
237280
+ }
237281
+ if (env && typeof env === "object" && typeof env.name === "string") {
237282
+ const name = env.name.trim();
237283
+ return name || null;
237284
+ }
237285
+ return null;
237286
+ }
237287
+ function isNonPosixShell(shell) {
237288
+ if (shell == null || shell === "") return false;
237289
+ const first = String(shell).trim().split(/\s+/)[0].replace(/^.*\//, "").toLowerCase();
237290
+ return NON_SHELL.has(first);
237291
+ }
237292
+ function isDeployGateInvocation(run, shell) {
237293
+ if (isNonPosixShell(shell)) return false;
237294
+ const text = String(run || "");
237295
+ let heredoc = null;
237296
+ let quote = null;
237297
+ for (const rawLine of text.split("\n")) {
237298
+ let line = String(rawLine || "");
237299
+ if (heredoc) {
237300
+ if (line.trim() === heredoc) heredoc = null;
237301
+ continue;
237302
+ }
237303
+ const hd = /<<[-]?\s*['"]?(\w+)['"]?/.exec(line);
237304
+ if (hd) heredoc = hd[1];
237305
+ if (quote) {
237306
+ for (let i = 0; i < line.length; i++) {
237307
+ const ch = line[i];
237308
+ if (ch === quote && line[i - 1] !== "\\") {
237309
+ quote = null;
237310
+ break;
237311
+ }
237312
+ }
237313
+ if (quote) continue;
237314
+ }
237315
+ const trimmed = line.trim();
237316
+ if (!trimmed || trimmed.startsWith("#")) continue;
237317
+ let cmd = trimmed.replace(/^(?:\w+=\S+\s+)+/, "");
237318
+ if (/^(echo|printf|cat|false|true|:)\b/.test(cmd)) {
237319
+ for (let i = 0; i < cmd.length; i++) {
237320
+ const ch = cmd[i];
237321
+ if ((ch === "'" || ch === '"') && cmd[i - 1] !== "\\") {
237322
+ quote = quote ? quote === ch ? null : quote : ch;
237323
+ }
237324
+ }
237325
+ continue;
237326
+ }
237327
+ if (DEPLOY_GATE_INVOCATION.test(cmd)) return true;
237328
+ }
237329
+ return false;
237330
+ }
237331
+ function stepsOf(job) {
237332
+ if (!job || typeof job !== "object") return [];
237333
+ return Array.isArray(job.steps) ? job.steps : [];
237334
+ }
237335
+ function inspectJob(job) {
237336
+ const out = {
237337
+ contractGate: false,
237338
+ deployGate: false,
237339
+ environment: jobEnvironmentName(job),
237340
+ disabled: isExplicitlyDisabled(job)
237341
+ };
237342
+ if (out.disabled) return out;
237343
+ for (const step of stepsOf(job)) {
237344
+ if (!step || typeof step !== "object") continue;
237345
+ if (isExplicitlyDisabled(step)) continue;
237346
+ if (step.uses != null && isContractGateUses(step.uses)) out.contractGate = true;
237347
+ if (step.run != null && isDeployGateInvocation(step.run, step.shell)) out.deployGate = true;
237348
+ }
237349
+ return out;
237350
+ }
237351
+ function inspectWorkflowYaml(body) {
237352
+ const empty = { contractGate: false, deployGate: false, deployEnvironments: [] };
237353
+ let doc;
237354
+ try {
237355
+ doc = yaml.load(String(body || ""));
237356
+ } catch {
237357
+ return empty;
237358
+ }
237359
+ if (!doc || typeof doc !== "object" || Array.isArray(doc)) return empty;
237360
+ const jobs = doc.jobs && typeof doc.jobs === "object" && !Array.isArray(doc.jobs) ? doc.jobs : null;
237361
+ if (!jobs) return empty;
237362
+ let contractGate = false;
237363
+ let deployGate = false;
237364
+ const deployEnvironments = [];
237365
+ for (const name of Object.keys(jobs)) {
237366
+ const info = inspectJob(jobs[name]);
237367
+ if (info.contractGate) contractGate = true;
237368
+ if (info.deployGate) {
237369
+ deployGate = true;
237370
+ if (info.environment) deployEnvironments.push(info.environment);
237371
+ }
237372
+ }
237373
+ return { contractGate, deployGate, deployEnvironments };
237374
+ }
237375
+ module2.exports = {
237376
+ CONTRACT_GATE_ACTION,
237377
+ isPinnedMajor,
237378
+ parseActionUses,
237379
+ isContractGateUses,
237380
+ isExplicitlyDisabled,
237381
+ jobEnvironmentName,
237382
+ isDeployGateInvocation,
237383
+ inspectJob,
237384
+ inspectWorkflowYaml
237385
+ };
237386
+ }
237387
+ });
237388
+
237170
237389
  // src/provider/github-enforcement.js
237171
237390
  var require_github_enforcement = __commonJS({
237172
237391
  "src/provider/github-enforcement.js"(exports2, module2) {
@@ -237178,7 +237397,107 @@ var require_github_enforcement = __commonJS({
237178
237397
  extractRequiredContexts
237179
237398
  } = require_setup_required_check();
237180
237399
  var { GATE_ACTION } = require_contract_gate_workflow();
237181
- var EVIDENCE_SPEC = "provider-enforcement-evidence.v1";
237400
+ var { inspectWorkflowYaml } = require_workflow_scan();
237401
+ var EVIDENCE_SPEC = "provider-enforcement-evidence.v2";
237402
+ var CODERIFTS_GITHUB_APP_SLUG = "coderifts";
237403
+ var CODERIFTS_GITHUB_APP_ID = 2860592;
237404
+ var REASON = Object.freeze({
237405
+ ISSUER_BOUND: "issuer_bound",
237406
+ NO_ISSUER_BINDING: "no_issuer_binding",
237407
+ LEGACY_CONTEXTS_NO_ISSUER_BINDING: "legacy_contexts_no_issuer_binding",
237408
+ ISSUER_APP_ID_MISMATCH: "issuer_app_id_mismatch",
237409
+ ISSUER_APP_ID_UNRESOLVABLE: "issuer_app_id_unresolvable",
237410
+ REQUIRED_CHECK_ABSENT: "required_check_absent",
237411
+ ACTIVE_JOB: "active_job",
237412
+ NO_ACTIVE_JOB: "no_active_job",
237413
+ ENVIRONMENT_CORRELATED: "environment_correlated",
237414
+ ENVIRONMENT_MISMATCH: "environment_mismatch"
237415
+ });
237416
+ function verifiedRequiresBinding(layer2) {
237417
+ if (!layer2 || layer2.status !== STATUS.VERIFIED) return true;
237418
+ if (layer2.id === "required_check") return layer2.reason_code === REASON.ISSUER_BOUND;
237419
+ if (layer2.id === "workflow_contract_gate" || layer2.id === "workflow_deploy_gate") {
237420
+ return layer2.reason_code === REASON.ACTIVE_JOB;
237421
+ }
237422
+ if (layer2.id === "environment_protection") {
237423
+ return layer2.reason_code === REASON.ENVIRONMENT_CORRELATED;
237424
+ }
237425
+ return true;
237426
+ }
237427
+ function expectedIssuerAppId() {
237428
+ const id = Number(CODERIFTS_GITHUB_APP_ID);
237429
+ return Number.isInteger(id) && id > 0 ? id : null;
237430
+ }
237431
+ function ourCheckEntries(rsc) {
237432
+ if (!rsc || !Array.isArray(rsc.checks)) return [];
237433
+ return rsc.checks.filter((c) => c && typeof c.context === "string" && c.context === CHECK_NAME);
237434
+ }
237435
+ function numericAppId(value) {
237436
+ if (value == null || value === "") return null;
237437
+ const n = Number(value);
237438
+ return Number.isInteger(n) && n > 0 ? n : null;
237439
+ }
237440
+ function evaluateRequiredCheck(protection) {
237441
+ const expected = expectedIssuerAppId();
237442
+ const rsc = protection && protection.required_status_checks;
237443
+ const contexts = extractRequiredContexts(protection);
237444
+ if (expected == null) {
237445
+ return {
237446
+ status: STATUS.UNVERIFIABLE,
237447
+ reason_code: REASON.ISSUER_APP_ID_UNRESOLVABLE,
237448
+ reason: "CodeRifts GitHub App id could not be determined from public metadata; required_check is UNVERIFIABLE (never VERIFIED without an issuer id)",
237449
+ required_contexts: contexts,
237450
+ app_id: null
237451
+ };
237452
+ }
237453
+ const ours = ourCheckEntries(rsc);
237454
+ if (ours.length > 0) {
237455
+ const bound = ours.find((c) => numericAppId(c.app_id) === expected);
237456
+ if (bound) {
237457
+ return {
237458
+ status: STATUS.VERIFIED,
237459
+ reason_code: REASON.ISSUER_BOUND,
237460
+ reason: `required_status_checks.checks binds "${CHECK_NAME}" to GitHub App id ${expected} (slug ${CODERIFTS_GITHUB_APP_SLUG})`,
237461
+ required_contexts: contexts,
237462
+ app_id: expected
237463
+ };
237464
+ }
237465
+ const other = ours.find((c) => numericAppId(c.app_id) != null);
237466
+ if (other) {
237467
+ return {
237468
+ status: STATUS.NOT_VERIFIED,
237469
+ reason_code: REASON.ISSUER_APP_ID_MISMATCH,
237470
+ reason: `required_status_checks.checks binds "${CHECK_NAME}" to app_id ${numericAppId(other.app_id)}, not CodeRifts App id ${expected}`,
237471
+ required_contexts: contexts,
237472
+ app_id: numericAppId(other.app_id),
237473
+ expected_app_id: expected
237474
+ };
237475
+ }
237476
+ return {
237477
+ status: STATUS.NOT_VERIFIED,
237478
+ reason_code: REASON.NO_ISSUER_BINDING,
237479
+ reason: `required_status_checks.checks lists "${CHECK_NAME}" with no app_id (name-only; any repository writer can post a success status with that context)`,
237480
+ required_contexts: contexts,
237481
+ app_id: null
237482
+ };
237483
+ }
237484
+ if (contexts.includes(CHECK_NAME)) {
237485
+ return {
237486
+ status: STATUS.NOT_VERIFIED,
237487
+ reason_code: REASON.LEGACY_CONTEXTS_NO_ISSUER_BINDING,
237488
+ reason: `required_status_checks.contexts lists "${CHECK_NAME}" but the legacy contexts array has no app_id at all (legacy_contexts_no_issuer_binding)`,
237489
+ required_contexts: contexts,
237490
+ app_id: null
237491
+ };
237492
+ }
237493
+ return {
237494
+ status: STATUS.NOT_VERIFIED,
237495
+ reason_code: REASON.REQUIRED_CHECK_ABSENT,
237496
+ reason: `required_status_checks does not include "${CHECK_NAME}" (have: ${contexts.join(", ") || "none"})`,
237497
+ required_contexts: contexts,
237498
+ app_id: null
237499
+ };
237500
+ }
237182
237501
  var APP_INSTALLATION = Object.freeze({
237183
237502
  can_query_protection: false,
237184
237503
  permissions: Object.freeze([
@@ -237344,12 +237663,14 @@ var require_github_enforcement = __commonJS({
237344
237663
  } else {
237345
237664
  const protection = prot.body && typeof prot.body === "object" ? prot.body : {};
237346
237665
  const endpoint = prot.endpoint ? `GET ${prot.endpoint}` : null;
237347
- const contexts = extractRequiredContexts(protection);
237348
- const hasCheck = contexts.includes(CHECK_NAME);
237349
- layers.push(layer("required_check", hasCheck ? STATUS.VERIFIED : STATUS.NOT_VERIFIED, {
237350
- reason: hasCheck ? `required_status_checks includes "${CHECK_NAME}"` : `required_status_checks does not include "${CHECK_NAME}" (have: ${contexts.join(", ") || "none"})`,
237666
+ const required = evaluateRequiredCheck(protection);
237667
+ layers.push(layer("required_check", required.status, {
237668
+ reason: required.reason,
237669
+ reason_code: required.reason_code,
237351
237670
  endpoint,
237352
- required_contexts: contexts
237671
+ required_contexts: required.required_contexts,
237672
+ app_id: required.app_id,
237673
+ ...required.expected_app_id != null ? { expected_app_id: required.expected_app_id } : {}
237353
237674
  }));
237354
237675
  const admins = enforceAdminsEnabled(protection);
237355
237676
  layers.push(layer("enforce_admins", admins ? STATUS.VERIFIED : STATUS.NOT_VERIFIED, {
@@ -237401,24 +237722,55 @@ var require_github_enforcement = __commonJS({
237401
237722
  }));
237402
237723
  } else {
237403
237724
  const ok = environmentProtected(environmentRead.body);
237404
- layers.push(layer("environment_protection", ok ? STATUS.VERIFIED : STATUS.NOT_VERIFIED, {
237405
- reason: ok ? `GitHub Environment "${environmentName}" has protection rules or a deployment branch policy` : `GitHub Environment "${environmentName}" exists but has no protection_rules and no deployment_branch_policy`,
237406
- endpoint: environmentRead.endpoint ? `GET ${environmentRead.endpoint}` : null,
237407
- environment: environmentName
237408
- }));
237725
+ const referenced = (localFiles.deployGateEnvironments || []).map(String);
237726
+ const correlated = ok && environmentName && referenced.includes(environmentName);
237727
+ if (correlated) {
237728
+ layers.push(layer("environment_protection", STATUS.VERIFIED, {
237729
+ reason: `GitHub Environment "${environmentName}" has protection rules and is referenced by an active deploy-gate job (environment:)`,
237730
+ reason_code: REASON.ENVIRONMENT_CORRELATED,
237731
+ endpoint: environmentRead.endpoint ? `GET ${environmentRead.endpoint}` : null,
237732
+ environment: environmentName,
237733
+ workflow_environments: referenced
237734
+ }));
237735
+ } else if (!ok) {
237736
+ layers.push(layer("environment_protection", STATUS.NOT_VERIFIED, {
237737
+ reason: `GitHub Environment "${environmentName}" exists but has no protection_rules and no deployment_branch_policy`,
237738
+ reason_code: REASON.ENVIRONMENT_MISMATCH,
237739
+ endpoint: environmentRead.endpoint ? `GET ${environmentRead.endpoint}` : null,
237740
+ environment: environmentName,
237741
+ workflow_environments: referenced
237742
+ }));
237743
+ } else {
237744
+ layers.push(layer("environment_protection", STATUS.NOT_VERIFIED, {
237745
+ reason: `GitHub Environment "${environmentName}" is protected but no active deploy-gate job sets environment: ${environmentName} (have: ${referenced.join(", ") || "none"})`,
237746
+ reason_code: REASON.ENVIRONMENT_MISMATCH,
237747
+ endpoint: environmentRead.endpoint ? `GET ${environmentRead.endpoint}` : null,
237748
+ environment: environmentName,
237749
+ workflow_environments: referenced
237750
+ }));
237751
+ }
237409
237752
  }
237410
237753
  const cg = localFiles.contractGateFiles || [];
237411
237754
  layers.push(layer("workflow_contract_gate", cg.length ? STATUS.VERIFIED : STATUS.NOT_VERIFIED, {
237412
- reason: cg.length ? `workflow references ${GATE_ACTION} (${cg.join(", ")})` : `no .github/workflows file references ${GATE_ACTION}`,
237755
+ reason: cg.length ? `active job step uses ${GATE_ACTION} (${cg.join(", ")})` : `no active .github/workflows job uses ${GATE_ACTION} (comments, disabled jobs, and string literals do not count)`,
237756
+ reason_code: cg.length ? REASON.ACTIVE_JOB : REASON.NO_ACTIVE_JOB,
237413
237757
  endpoint: "local:.github/workflows",
237414
237758
  files: cg
237415
237759
  }));
237416
237760
  const dg = localFiles.deployGateFiles || [];
237417
237761
  layers.push(layer("workflow_deploy_gate", dg.length ? STATUS.VERIFIED : STATUS.NOT_VERIFIED, {
237418
- reason: dg.length ? `workflow runs coderifts deploy-gate (${dg.join(", ")})` : "no .github/workflows file runs coderifts deploy-gate",
237762
+ reason: dg.length ? `active job step invokes coderifts deploy-gate (${dg.join(", ")})` : "no active .github/workflows job invokes coderifts deploy-gate (comments, disabled jobs, and string literals do not count)",
237763
+ reason_code: dg.length ? REASON.ACTIVE_JOB : REASON.NO_ACTIVE_JOB,
237419
237764
  endpoint: "local:.github/workflows",
237420
237765
  files: dg
237421
237766
  }));
237767
+ for (const l of layers) {
237768
+ if (l.status === STATUS.VERIFIED && !verifiedRequiresBinding(l)) {
237769
+ l.status = STATUS.NOT_VERIFIED;
237770
+ l.reason_code = l.reason_code || REASON.NO_ISSUER_BINDING;
237771
+ l.reason = `${l.reason || l.id} \u2014 downgraded: VERIFIED requires issuer or active-job binding`;
237772
+ }
237773
+ }
237422
237774
  const byId = Object.fromEntries(layers.map((l) => [l.id, l]));
237423
237775
  const inescapable = LAYER_IDS.every((id) => byId[id] && byId[id].status === STATUS.VERIFIED);
237424
237776
  return {
@@ -237426,7 +237778,7 @@ var require_github_enforcement = __commonJS({
237426
237778
  inescapable_deploy: inescapable,
237427
237779
  claim: {
237428
237780
  inescapable_deploy: inescapable,
237429
- basis: inescapable ? "all six layers VERIFIED from queried provider evidence + local workflow files" : "inescapable_deploy stays false until every layer is VERIFIED"
237781
+ basis: inescapable ? "all six layers VERIFIED (issuer-bound required check + active workflow jobs + correlated environment)" : "inescapable_deploy stays false until every layer is VERIFIED"
237430
237782
  }
237431
237783
  };
237432
237784
  }
@@ -237437,12 +237789,13 @@ var require_github_enforcement = __commonJS({
237437
237789
  const dir = path.join(cwd, ".github", "workflows");
237438
237790
  const contractGateFiles = [];
237439
237791
  const deployGateFiles = [];
237440
- if (!exists(dir)) return { contractGateFiles, deployGateFiles };
237792
+ const deployGateEnvironments = [];
237793
+ if (!exists(dir)) return { contractGateFiles, deployGateFiles, deployGateEnvironments };
237441
237794
  let names = [];
237442
237795
  try {
237443
237796
  names = readdir(dir);
237444
237797
  } catch {
237445
- return { contractGateFiles, deployGateFiles };
237798
+ return { contractGateFiles, deployGateFiles, deployGateEnvironments };
237446
237799
  }
237447
237800
  for (const name of names) {
237448
237801
  if (!/\.ya?ml$/i.test(name)) continue;
@@ -237453,10 +237806,18 @@ var require_github_enforcement = __commonJS({
237453
237806
  } catch {
237454
237807
  continue;
237455
237808
  }
237456
- if (body.includes(GATE_ACTION)) contractGateFiles.push(rel);
237457
- if (/\bcoderifts\s+deploy-gate\b/.test(body)) deployGateFiles.push(rel);
237809
+ const evidence = inspectWorkflowYaml(body);
237810
+ if (evidence.contractGate) contractGateFiles.push(rel);
237811
+ if (evidence.deployGate) {
237812
+ deployGateFiles.push(rel);
237813
+ for (const envName of evidence.deployEnvironments) {
237814
+ if (envName && !deployGateEnvironments.includes(envName)) {
237815
+ deployGateEnvironments.push(envName);
237816
+ }
237817
+ }
237818
+ }
237458
237819
  }
237459
- return { contractGateFiles, deployGateFiles };
237820
+ return { contractGateFiles, deployGateFiles, deployGateEnvironments };
237460
237821
  }
237461
237822
  async function queryGitHubEnforcement({
237462
237823
  owner,
@@ -237491,12 +237852,17 @@ var require_github_enforcement = __commonJS({
237491
237852
  NO_TOKEN_INSTRUCTION,
237492
237853
  LAYER_IDS,
237493
237854
  STATUS,
237855
+ REASON,
237494
237856
  CHECK_NAME,
237495
237857
  GATE_ACTION,
237858
+ CODERIFTS_GITHUB_APP_ID,
237859
+ CODERIFTS_GITHUB_APP_SLUG,
237496
237860
  resolveGitHubAuth,
237497
237861
  redactSecrets,
237498
237862
  createGitHubHttpClient,
237499
237863
  evaluateLayers,
237864
+ evaluateRequiredCheck,
237865
+ verifiedRequiresBinding,
237500
237866
  scanLocalWorkflows,
237501
237867
  queryGitHubEnforcement,
237502
237868
  enforceAdminsEnabled,
@@ -237572,7 +237938,8 @@ var require_enforce_check = __commonJS({
237572
237938
  const missing = report.layers.filter((l) => l.status !== "VERIFIED");
237573
237939
  for (const l of missing) {
237574
237940
  if (l.id === "required_check") {
237575
- lines.push(chalk.dim(' require "CodeRifts / contract-gate" on default-branch protection'));
237941
+ lines.push(chalk.dim(' require "CodeRifts / contract-gate" bound to the CodeRifts GitHub App (app_id)'));
237942
+ lines.push(chalk.dim(" on default-branch protection \u2014 name-only and legacy contexts[] are NOT_VERIFIED"));
237576
237943
  lines.push(chalk.dim(" (coderifts setup-required-check --apply)"));
237577
237944
  } else if (l.id === "enforce_admins") {
237578
237945
  lines.push(chalk.dim(" set enforce_admins: true so administrators cannot bypass"));
@@ -237580,12 +237947,13 @@ var require_enforce_check = __commonJS({
237580
237947
  } else if (l.id === "strict_status_checks") {
237581
237948
  lines.push(chalk.dim(" set required_status_checks.strict: true"));
237582
237949
  } else if (l.id === "environment_protection") {
237583
- lines.push(chalk.dim(" add a GitHub Environment with protection rules, pass --env <name>"));
237950
+ lines.push(chalk.dim(" add a GitHub Environment with protection rules, pass --env <name>,"));
237951
+ lines.push(chalk.dim(" and set that same name on the deploy-gate job's environment: key"));
237584
237952
  } else if (l.id === "workflow_contract_gate") {
237585
- lines.push(chalk.dim(" add .github/workflows/coderifts.yml using coderifts/contract-gate@v0"));
237586
- lines.push(chalk.dim(" (coderifts init --agents)"));
237953
+ lines.push(chalk.dim(" add an active job step: uses: coderifts/contract-gate@v0"));
237954
+ lines.push(chalk.dim(" (comments / if: false / wrong org do not count; coderifts init --agents)"));
237587
237955
  } else if (l.id === "workflow_deploy_gate") {
237588
- lines.push(chalk.dim(" add a CD step: coderifts deploy-gate --env <env> --artifact <id> --receipt <file>"));
237956
+ lines.push(chalk.dim(" add an active CD step that invokes: coderifts deploy-gate --env <env> --artifact <id> --receipt <file>"));
237589
237957
  }
237590
237958
  }
237591
237959
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coderifts",
3
- "version": "4.8.0",
3
+ "version": "4.9.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",