coderifts 4.6.0 → 4.7.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,38 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.7.0
4
+
5
+ ### Added
6
+ - **`coderifts enforce --check`** — provider-native CD enforcement evidence.
7
+ Queries GitHub (and local workflow files) and reports, per layer,
8
+ `VERIFIED` / `NOT_VERIFIED` / `UNVERIFIABLE(reason)`:
9
+ 1. required check `CodeRifts / contract-gate` on default-branch protection
10
+ 2. `enforce_admins` (admin bypass)
11
+ 3. `required_status_checks.strict`
12
+ 4. GitHub Environment protection rules (`--env`)
13
+ 5. workflow references `coderifts/contract-gate@v0` (local)
14
+ 6. workflow runs `coderifts deploy-gate` (local)
15
+
16
+ `inescapable_deploy` is **true only when every layer is VERIFIED**. Otherwise
17
+ the field stays false and the JSON carries the queried facts. `--json`
18
+ includes each layer's status, the API endpoint consulted, and `queried_at`.
19
+
20
+ Auth: `GITHUB_TOKEN` / `GH_TOKEN` (user). App installation token
21
+ (`GITHUB_APP_TOKEN`) is accepted but **cannot** query branch protection —
22
+ the GitHub App permission set is contents:read, pull_requests, checks
23
+ (AUDIT.md §2.4); `GET .../protection` needs `administration:read`. No
24
+ token → `UNVERIFIABLE` with the exact instruction. Token values are never
25
+ printed. GitLab/Bitbucket: no adapter (UNVERIFIABLE, never stubbed).
26
+
27
+ ### Changed
28
+ - **`observeCDEnforcement`:** `CODERIFTS_DEPLOY_NO_BYPASS` is recorded as
29
+ `host_claim_no_bypass` and no longer sets `bypass_possible: false`.
30
+ `--enforce` still marks the step ENFORCING (exit is already fail-closed)
31
+ but is a **host claim**. `bypass_possible` is false only when
32
+ `providerEvidence.inescapable_deploy === true` from `enforce --check`.
33
+ Guard `deployGate` still consumes `{ enforcement, bypass_possible }` — the
34
+ CLI must not pass a flag-derived “no bypass”.
35
+
3
36
  ## 4.6.0
4
37
 
5
38
  **Version pick:** the parallel Cursor-hook round claimed **4.5.0**
package/README.md CHANGED
@@ -168,8 +168,11 @@ gate reason + `renderDecisionWhy` when a body exists) and
168
168
  unsigned file still prints the unsigned-receipt truth loudly.
169
169
 
170
170
  **`--enforce` / `CODERIFTS_DEPLOY_ENFORCE=true`:** accepted silently (no-op
171
- for exit — default is already fail-closed). Still attests ENFORCING for the
172
- `inescapable_deploy` claim.
171
+ for exit — default is already fail-closed). This is a **host claim** that the
172
+ step is ENFORCING. It does **not** set `inescapable_deploy`. That field is
173
+ true only from queried pipeline protection (`coderifts enforce --check`).
174
+ `CODERIFTS_DEPLOY_NO_BYPASS` is recorded as a host claim and does not clear
175
+ `bypass_possible`.
173
176
 
174
177
  **Infra vs policy:** missing `--env` / `--artifact`, malformed receipt JSON, or
175
178
  an unreachable `--keys` registry prints distinct `INFRA` wording plus
@@ -382,13 +385,44 @@ failed, **1** on missing repo/key, API error, or any apply failure.
382
385
  |------|-------------|---------|
383
386
  | `[repo]` / `--repo <owner/repo>` | Repository | none (required) |
384
387
  | `--apply` | Run underlying setup commands | dry-run |
388
+ | `--check` | Query GitHub + local workflows (read-only evidence) | `false` |
389
+ | `--env <name>` | With `--check`: GitHub Environment to query | none |
390
+ | `--branch <name>` | With `--check`: branch (default: repo default) | default branch |
385
391
  | `--json` | Machine-readable JSON result | `false` |
386
392
 
387
393
  ```bash
388
394
  coderifts enforce owner/repo
389
395
  coderifts enforce --repo owner/repo --apply
396
+ coderifts enforce --check --repo owner/repo --env production --json
390
397
  ```
391
398
 
399
+ ### `coderifts enforce --check`
400
+
401
+ Provider-native pipeline-protection evidence. This is **not** a host claim:
402
+ `--enforce` and `CODERIFTS_DEPLOY_NO_BYPASS` do **not** set
403
+ `inescapable_deploy`. The field is true only when every layer below is
404
+ `VERIFIED`.
405
+
406
+ | Layer | VERIFIED when |
407
+ |-------|----------------|
408
+ | `required_check` | Default-branch protection requires `CodeRifts / contract-gate` |
409
+ | `enforce_admins` | `enforce_admins.enabled` is **true** (admins cannot bypass). `false` = NOT_VERIFIED (admin bypass open) |
410
+ | `strict_status_checks` | `required_status_checks.strict` is true |
411
+ | `environment_protection` | `--env` names a GitHub Environment with protection rules or a deployment branch policy |
412
+ | `workflow_contract_gate` | a local `.github/workflows` file references `coderifts/contract-gate@v0` |
413
+ | `workflow_deploy_gate` | a local workflow runs `coderifts deploy-gate` |
414
+
415
+ Statuses: `VERIFIED` / `NOT_VERIFIED` / `UNVERIFIABLE(reason)`. Auth:
416
+ `GITHUB_TOKEN` (never printed). **No token** → API layers `UNVERIFIABLE` with
417
+ the instruction to set `GITHUB_TOKEN` (repo + `administration:read`). The
418
+ CodeRifts GitHub App **cannot** query branch protection (permissions are
419
+ contents / pull requests / checks — not `administration:read`).
420
+
421
+ `--json` is the quotable evidence document (`spec`:
422
+ `provider-enforcement-evidence.v1`): each layer's status, the API endpoint
423
+ consulted, `queried_at`. Exit **0** when a report was produced (evidence, not
424
+ a gate). GitLab/Bitbucket: no adapter — `UNVERIFIABLE`, never stubbed.
425
+
392
426
  ### Agent host files (`coderifts agent-setup`)
393
427
 
394
428
  Writes the six CodeRifts agent-host rule files **plus** the Claude Code
package/bin/coderifts.js CHANGED
@@ -184,9 +184,13 @@ program
184
184
  // No new GitHub writes with the CodeRifts API key (user's gh for merge; local fs for hook).
185
185
  program
186
186
  .command('enforce [repo]')
187
- .description('Close enforcement gaps by chaining setup-required-check / hook / deploy guidance (dry-run default; use --apply to mutate)')
187
+ .description('Close enforcement gaps (dry-run default), or --check to query provider-native pipeline protection evidence')
188
188
  .option('--repo <owner/repo>', 'Repository (owner/repo); also accepted as a positional argument')
189
189
  .option('--apply', 'Actually run underlying setup commands (default: dry-run only)')
190
+ .option('--check', 'Query GitHub + local workflows; report VERIFIED / NOT_VERIFIED / UNVERIFIABLE (read-only)')
191
+ .option('--env <name>', 'With --check: GitHub Environment name to query for protection rules')
192
+ .option('--branch <name>', 'With --check: branch to query (default: repository default branch)')
193
+ .option('--provider <name>', 'With --check: provider adapter (github only; others UNVERIFIABLE, never stubbed)', 'github')
190
194
  .option('--json', 'Machine-readable JSON result')
191
195
  .action(async (repoPositional, options) => {
192
196
  const { runEnforce } = require('../src/commands/enforce');
@@ -194,6 +198,7 @@ program
194
198
  ...options,
195
199
  repo: options.repo || repoPositional || null,
196
200
  apply: !!options.apply,
201
+ check: !!options.check,
197
202
  });
198
203
  if (result && typeof result.exitCode === 'number') {
199
204
  process.exitCode = result.exitCode;
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.6.0",
3031
+ version: "4.7.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",
@@ -207461,18 +207461,31 @@ var require_deploy_gate2 = __commonJS({
207461
207461
  return isEnvFlag(e, "CODERIFTS_DEPLOY_ADVISORY") || isEnvFlag(e, "CODERIFTS_ADVISORY");
207462
207462
  }
207463
207463
  function observeCDEnforcement(options = {}) {
207464
+ const evidence = options.providerEvidence;
207465
+ const hostClaimNoBypass = String(process.env.CODERIFTS_DEPLOY_NO_BYPASS || "").toLowerCase() === "true";
207466
+ if (evidence && evidence.inescapable_deploy === true) {
207467
+ return {
207468
+ enforcement: "ENFORCING",
207469
+ bypass_possible: false,
207470
+ step_is_required: true,
207471
+ required_step_name: "CodeRifts / deploy-gate",
207472
+ attestation_source: "provider_query",
207473
+ host_claim_no_bypass: hostClaimNoBypass
207474
+ };
207475
+ }
207464
207476
  const envVal = String(process.env.CODERIFTS_DEPLOY_ENFORCE || "").toLowerCase();
207465
207477
  let enforcement;
207466
207478
  if (enforceSignal(options)) enforcement = "ENFORCING";
207467
207479
  else if (envVal === "unknown") enforcement = "UNKNOWN";
207468
207480
  else enforcement = "ADVISORY";
207469
- const bypass_possible = String(process.env.CODERIFTS_DEPLOY_NO_BYPASS || "").toLowerCase() !== "true";
207470
207481
  return {
207471
207482
  enforcement,
207472
- bypass_possible,
207483
+ bypass_possible: true,
207484
+ // host NO_BYPASS is a claim, not queried evidence
207473
207485
  step_is_required: enforcement === "ENFORCING",
207474
207486
  required_step_name: "CodeRifts / deploy-gate",
207475
- attestation_source: "cli_flag"
207487
+ attestation_source: "cli_flag",
207488
+ host_claim_no_bypass: hostClaimNoBypass
207476
207489
  };
207477
207490
  }
207478
207491
  function deployReportResiduals(state, enforcement_inescapable, enforcement, change_set_rebound) {
@@ -207795,7 +207808,7 @@ var require_deploy_gate2 = __commonJS({
207795
207808
  if (bind.report_residuals.length) lines.push(` Residuals: ${bind.report_residuals.join(", ")}`);
207796
207809
  if (bind.must_re_preflight) lines.push(chalk.yellow(" Action: re-preflight this { environment, artifact } \u2014 the receipt does not authorize it."));
207797
207810
  lines.push("");
207798
- lines.push(enforce ? chalk.dim(" Attested ENFORCING (inescapable_deploy claim). Exit is fail-closed by default.") : chalk.dim(" Fail-closed default \u2014 a failing gate exits non-zero. " + SOFTEN_HINT + "."));
207811
+ lines.push(enforce ? chalk.dim(" Attested ENFORCING via --enforce (host claim). inescapable_deploy requires queried pipeline protection (`coderifts enforce --check`). Exit is fail-closed by default.") : chalk.dim(" Fail-closed default \u2014 a failing gate exits non-zero. " + SOFTEN_HINT + "."));
207799
207812
  lines.push("");
207800
207813
  return lines.join("\n");
207801
207814
  }
@@ -236999,6 +237012,538 @@ var require_status = __commonJS({
236999
237012
  }
237000
237013
  });
237001
237014
 
237015
+ // src/provider/github-enforcement.js
237016
+ var require_github_enforcement = __commonJS({
237017
+ "src/provider/github-enforcement.js"(exports2, module2) {
237018
+ "use strict";
237019
+ var fs = require("fs");
237020
+ var path = require("path");
237021
+ var {
237022
+ CHECK_NAME,
237023
+ extractRequiredContexts
237024
+ } = require_setup_required_check();
237025
+ var { GATE_ACTION } = require_contract_gate_workflow();
237026
+ var EVIDENCE_SPEC = "provider-enforcement-evidence.v1";
237027
+ var APP_INSTALLATION = Object.freeze({
237028
+ can_query_protection: false,
237029
+ permissions: Object.freeze([
237030
+ "contents:read",
237031
+ "pull_requests:read+write",
237032
+ "checks:read+write"
237033
+ ]),
237034
+ missing_scope: "administration:read",
237035
+ reason: "GET /repos/{owner}/{repo}/branches/{branch}/protection requires administration:read (or repo admin). The CodeRifts GitHub App permission set is contents:read, pull_requests:read+write, checks:read+write \u2014 administration:read is not granted."
237036
+ });
237037
+ var NO_TOKEN_INSTRUCTION = "NO token \u2014 set GITHUB_TOKEN (repo + administration:read; Environments: Read if querying --env). The CodeRifts GitHub App installation cannot query branch protection (permissions: contents:read, pull_requests:read+write, checks:read+write \u2014 missing administration:read).";
237038
+ var LAYER_IDS = Object.freeze([
237039
+ "required_check",
237040
+ "enforce_admins",
237041
+ "strict_status_checks",
237042
+ "environment_protection",
237043
+ "workflow_contract_gate",
237044
+ "workflow_deploy_gate"
237045
+ ]);
237046
+ var STATUS = Object.freeze({
237047
+ VERIFIED: "VERIFIED",
237048
+ NOT_VERIFIED: "NOT_VERIFIED",
237049
+ UNVERIFIABLE: "UNVERIFIABLE"
237050
+ });
237051
+ function resolveGitHubAuth(env = process.env) {
237052
+ const user = String(env && (env.GITHUB_TOKEN || env.GH_TOKEN) || "").trim();
237053
+ if (user) {
237054
+ return { ok: true, source: "github_token", token: user };
237055
+ }
237056
+ const app = String(env && (env.GITHUB_APP_TOKEN || env.CODERIFTS_GITHUB_APP_TOKEN) || "").trim();
237057
+ if (app) {
237058
+ return { ok: true, source: "github_app_installation", token: app };
237059
+ }
237060
+ return { ok: false, source: "none", token: null, reason: NO_TOKEN_INSTRUCTION };
237061
+ }
237062
+ function redactSecrets(text) {
237063
+ return String(text || "").replace(/Bearer\s+\S+/gi, "Bearer [redacted]").replace(/ghp_[A-Za-z0-9_]+/g, "ghp_[redacted]").replace(/github_pat_[A-Za-z0-9_]+/g, "github_pat_[redacted]");
237064
+ }
237065
+ function createGitHubHttpClient(opts = {}) {
237066
+ const token = opts.token;
237067
+ const fetchFn = opts.fetchImpl || (typeof fetch === "function" ? fetch.bind(globalThis) : null);
237068
+ const apiBase = (opts.apiBase || "https://api.github.com").replace(/\/$/, "");
237069
+ return {
237070
+ name: "github",
237071
+ async get(apiPath) {
237072
+ const endpoint = `${apiBase}/${String(apiPath).replace(/^\//, "")}`;
237073
+ if (!fetchFn) {
237074
+ return {
237075
+ status: 0,
237076
+ body: null,
237077
+ endpoint,
237078
+ error: "fetch is not available in this runtime"
237079
+ };
237080
+ }
237081
+ const res = await fetchFn(endpoint, {
237082
+ method: "GET",
237083
+ headers: {
237084
+ Authorization: `Bearer ${token}`,
237085
+ Accept: "application/vnd.github+json",
237086
+ "X-GitHub-Api-Version": "2022-11-28",
237087
+ "User-Agent": "coderifts-cli"
237088
+ }
237089
+ });
237090
+ const text = await res.text();
237091
+ let body = null;
237092
+ try {
237093
+ body = text ? JSON.parse(text) : null;
237094
+ } catch {
237095
+ body = null;
237096
+ }
237097
+ return { status: res.status, body, endpoint };
237098
+ }
237099
+ };
237100
+ }
237101
+ function enforceAdminsEnabled(protection) {
237102
+ if (!protection || typeof protection !== "object") return false;
237103
+ const ea = protection.enforce_admins;
237104
+ if (ea === true) return true;
237105
+ if (ea && typeof ea === "object" && ea.enabled === true) return true;
237106
+ return false;
237107
+ }
237108
+ function strictEnabled(protection) {
237109
+ if (!protection || typeof protection !== "object") return false;
237110
+ const rsc = protection.required_status_checks;
237111
+ return !!(rsc && typeof rsc === "object" && rsc.strict === true);
237112
+ }
237113
+ function environmentProtected(body) {
237114
+ if (!body || typeof body !== "object") return false;
237115
+ const rules = Array.isArray(body.protection_rules) ? body.protection_rules : [];
237116
+ if (rules.length > 0) return true;
237117
+ const policy = body.deployment_branch_policy;
237118
+ if (policy && typeof policy === "object") {
237119
+ return policy.protected_branches === true || policy.custom_branch_policies === true;
237120
+ }
237121
+ return false;
237122
+ }
237123
+ function layer(id, status, extra = {}) {
237124
+ return { id, status, ...extra };
237125
+ }
237126
+ function evaluateLayers({
237127
+ protectionRead,
237128
+ environmentRead,
237129
+ environmentName,
237130
+ local,
237131
+ auth
237132
+ } = {}) {
237133
+ const layers = [];
237134
+ const localFiles = local || { contractGateFiles: [], deployGateFiles: [] };
237135
+ const noAuth = !auth || auth.source === "none" || !auth.ok;
237136
+ const prot = protectionRead || null;
237137
+ if (noAuth && !prot) {
237138
+ const why = auth && auth.reason || NO_TOKEN_INSTRUCTION;
237139
+ for (const id of ["required_check", "enforce_admins", "strict_status_checks"]) {
237140
+ layers.push(layer(id, STATUS.UNVERIFIABLE, {
237141
+ reason: why,
237142
+ endpoint: null,
237143
+ missing_scope: APP_INSTALLATION.missing_scope
237144
+ }));
237145
+ }
237146
+ } else if (!prot) {
237147
+ const why = "branch protection was not queried";
237148
+ for (const id of ["required_check", "enforce_admins", "strict_status_checks"]) {
237149
+ layers.push(layer(id, STATUS.UNVERIFIABLE, { reason: why, endpoint: null }));
237150
+ }
237151
+ } else if (prot.status === 403) {
237152
+ const why = "GitHub HTTP 403 \u2014 missing scope administration:read (branch protection). Grant Administration: Read on the token, or use a repo-admin token.";
237153
+ const endpoint = prot.endpoint ? `GET ${prot.endpoint}` : null;
237154
+ for (const id of ["required_check", "enforce_admins", "strict_status_checks"]) {
237155
+ layers.push(layer(id, STATUS.UNVERIFIABLE, {
237156
+ reason: why,
237157
+ endpoint,
237158
+ missing_scope: "administration:read",
237159
+ http_status: 403
237160
+ }));
237161
+ }
237162
+ } else if (prot.status === 404) {
237163
+ const endpoint = prot.endpoint ? `GET ${prot.endpoint}` : null;
237164
+ layers.push(layer("required_check", STATUS.NOT_VERIFIED, {
237165
+ reason: `classic branch protection is absent \u2014 required check "${CHECK_NAME}" is not required`,
237166
+ endpoint,
237167
+ http_status: 404
237168
+ }));
237169
+ layers.push(layer("enforce_admins", STATUS.NOT_VERIFIED, {
237170
+ reason: "classic branch protection is absent \u2014 administrators are unrestricted",
237171
+ endpoint,
237172
+ http_status: 404
237173
+ }));
237174
+ layers.push(layer("strict_status_checks", STATUS.NOT_VERIFIED, {
237175
+ reason: "classic branch protection is absent \u2014 required_status_checks.strict is not set",
237176
+ endpoint,
237177
+ http_status: 404
237178
+ }));
237179
+ } else if (prot.status >= 400 || prot.status === 0) {
237180
+ const endpoint = prot.endpoint ? `GET ${prot.endpoint}` : null;
237181
+ const why = `GitHub HTTP ${prot.status || 0} reading branch protection \u2014 cannot verify`;
237182
+ for (const id of ["required_check", "enforce_admins", "strict_status_checks"]) {
237183
+ layers.push(layer(id, STATUS.UNVERIFIABLE, {
237184
+ reason: redactSecrets(why),
237185
+ endpoint,
237186
+ http_status: prot.status
237187
+ }));
237188
+ }
237189
+ } else {
237190
+ const protection = prot.body && typeof prot.body === "object" ? prot.body : {};
237191
+ const endpoint = prot.endpoint ? `GET ${prot.endpoint}` : null;
237192
+ const contexts = extractRequiredContexts(protection);
237193
+ const hasCheck = contexts.includes(CHECK_NAME);
237194
+ layers.push(layer("required_check", hasCheck ? STATUS.VERIFIED : STATUS.NOT_VERIFIED, {
237195
+ reason: hasCheck ? `required_status_checks includes "${CHECK_NAME}"` : `required_status_checks does not include "${CHECK_NAME}" (have: ${contexts.join(", ") || "none"})`,
237196
+ endpoint,
237197
+ required_contexts: contexts
237198
+ }));
237199
+ const admins = enforceAdminsEnabled(protection);
237200
+ layers.push(layer("enforce_admins", admins ? STATUS.VERIFIED : STATUS.NOT_VERIFIED, {
237201
+ reason: admins ? "enforce_admins.enabled is true \u2014 administrators cannot bypass required checks" : "enforce_admins.enabled is false \u2014 repository administrators can bypass required checks",
237202
+ endpoint,
237203
+ enforce_admins: admins
237204
+ }));
237205
+ const strict = strictEnabled(protection);
237206
+ layers.push(layer("strict_status_checks", strict ? STATUS.VERIFIED : STATUS.NOT_VERIFIED, {
237207
+ reason: strict ? "required_status_checks.strict is true" : "required_status_checks.strict is not true \u2014 a branch need not be up to date before merge",
237208
+ endpoint,
237209
+ strict
237210
+ }));
237211
+ }
237212
+ if (!environmentName) {
237213
+ layers.push(layer("environment_protection", STATUS.UNVERIFIABLE, {
237214
+ reason: "no --env supplied; pass --env <GitHub Environment name> to query environment protection rules",
237215
+ endpoint: null
237216
+ }));
237217
+ } else if (noAuth && !environmentRead) {
237218
+ layers.push(layer("environment_protection", STATUS.UNVERIFIABLE, {
237219
+ reason: auth && auth.reason || NO_TOKEN_INSTRUCTION,
237220
+ endpoint: null,
237221
+ missing_scope: "actions:read / Environments: Read"
237222
+ }));
237223
+ } else if (!environmentRead) {
237224
+ layers.push(layer("environment_protection", STATUS.UNVERIFIABLE, {
237225
+ reason: "environment was not queried",
237226
+ endpoint: null
237227
+ }));
237228
+ } else if (environmentRead.status === 403) {
237229
+ layers.push(layer("environment_protection", STATUS.UNVERIFIABLE, {
237230
+ reason: "GitHub HTTP 403 \u2014 missing scope to read environments (Actions/Environments: Read, or repo admin).",
237231
+ endpoint: environmentRead.endpoint ? `GET ${environmentRead.endpoint}` : null,
237232
+ missing_scope: "environments:read",
237233
+ http_status: 403
237234
+ }));
237235
+ } else if (environmentRead.status === 404) {
237236
+ layers.push(layer("environment_protection", STATUS.NOT_VERIFIED, {
237237
+ reason: `GitHub Environment "${environmentName}" does not exist (or has no protection)`,
237238
+ endpoint: environmentRead.endpoint ? `GET ${environmentRead.endpoint}` : null,
237239
+ http_status: 404
237240
+ }));
237241
+ } else if (environmentRead.status >= 400 || environmentRead.status === 0) {
237242
+ layers.push(layer("environment_protection", STATUS.UNVERIFIABLE, {
237243
+ reason: `GitHub HTTP ${environmentRead.status || 0} reading environment "${environmentName}"`,
237244
+ endpoint: environmentRead.endpoint ? `GET ${environmentRead.endpoint}` : null,
237245
+ http_status: environmentRead.status
237246
+ }));
237247
+ } else {
237248
+ const ok = environmentProtected(environmentRead.body);
237249
+ layers.push(layer("environment_protection", ok ? STATUS.VERIFIED : STATUS.NOT_VERIFIED, {
237250
+ 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`,
237251
+ endpoint: environmentRead.endpoint ? `GET ${environmentRead.endpoint}` : null,
237252
+ environment: environmentName
237253
+ }));
237254
+ }
237255
+ const cg = localFiles.contractGateFiles || [];
237256
+ layers.push(layer("workflow_contract_gate", cg.length ? STATUS.VERIFIED : STATUS.NOT_VERIFIED, {
237257
+ reason: cg.length ? `workflow references ${GATE_ACTION} (${cg.join(", ")})` : `no .github/workflows file references ${GATE_ACTION}`,
237258
+ endpoint: "local:.github/workflows",
237259
+ files: cg
237260
+ }));
237261
+ const dg = localFiles.deployGateFiles || [];
237262
+ layers.push(layer("workflow_deploy_gate", dg.length ? STATUS.VERIFIED : STATUS.NOT_VERIFIED, {
237263
+ reason: dg.length ? `workflow runs coderifts deploy-gate (${dg.join(", ")})` : "no .github/workflows file runs coderifts deploy-gate",
237264
+ endpoint: "local:.github/workflows",
237265
+ files: dg
237266
+ }));
237267
+ const byId = Object.fromEntries(layers.map((l) => [l.id, l]));
237268
+ const inescapable = LAYER_IDS.every((id) => byId[id] && byId[id].status === STATUS.VERIFIED);
237269
+ return {
237270
+ layers,
237271
+ inescapable_deploy: inescapable,
237272
+ claim: {
237273
+ inescapable_deploy: inescapable,
237274
+ basis: inescapable ? "all six layers VERIFIED from queried provider evidence + local workflow files" : "inescapable_deploy stays false until every layer is VERIFIED"
237275
+ }
237276
+ };
237277
+ }
237278
+ function scanLocalWorkflows(cwd, deps = {}) {
237279
+ const exists = deps.exists || fs.existsSync.bind(fs);
237280
+ const readdir = deps.readdir || ((p) => fs.readdirSync(p));
237281
+ const readFile = deps.readFile || ((p) => fs.readFileSync(p, "utf8"));
237282
+ const dir = path.join(cwd, ".github", "workflows");
237283
+ const contractGateFiles = [];
237284
+ const deployGateFiles = [];
237285
+ if (!exists(dir)) return { contractGateFiles, deployGateFiles };
237286
+ let names = [];
237287
+ try {
237288
+ names = readdir(dir);
237289
+ } catch {
237290
+ return { contractGateFiles, deployGateFiles };
237291
+ }
237292
+ for (const name of names) {
237293
+ if (!/\.ya?ml$/i.test(name)) continue;
237294
+ const rel = `.github/workflows/${name}`;
237295
+ let body = "";
237296
+ try {
237297
+ body = String(readFile(path.join(dir, name)));
237298
+ } catch {
237299
+ continue;
237300
+ }
237301
+ if (body.includes(GATE_ACTION)) contractGateFiles.push(rel);
237302
+ if (/\bcoderifts\s+deploy-gate\b/.test(body)) deployGateFiles.push(rel);
237303
+ }
237304
+ return { contractGateFiles, deployGateFiles };
237305
+ }
237306
+ async function queryGitHubEnforcement({
237307
+ owner,
237308
+ repo,
237309
+ branch,
237310
+ environmentName,
237311
+ client
237312
+ } = {}) {
237313
+ const protectionPath = branch ? `repos/${owner}/${repo}/branches/${encodeURIComponent(branch)}/protection` : null;
237314
+ const envPath = environmentName ? `repos/${owner}/${repo}/environments/${encodeURIComponent(environmentName)}` : null;
237315
+ let resolvedBranch = branch || null;
237316
+ let repoRead = null;
237317
+ if (!resolvedBranch && client) {
237318
+ repoRead = await client.get(`repos/${owner}/${repo}`);
237319
+ if (repoRead && repoRead.status === 200 && repoRead.body && repoRead.body.default_branch) {
237320
+ resolvedBranch = String(repoRead.body.default_branch);
237321
+ }
237322
+ }
237323
+ const protPath = resolvedBranch ? `repos/${owner}/${repo}/branches/${encodeURIComponent(resolvedBranch)}/protection` : protectionPath;
237324
+ const protectionRead = client && protPath ? await client.get(protPath) : null;
237325
+ const environmentRead = client && envPath ? await client.get(envPath) : null;
237326
+ return {
237327
+ branch: resolvedBranch,
237328
+ repoRead,
237329
+ protectionRead,
237330
+ environmentRead
237331
+ };
237332
+ }
237333
+ module2.exports = {
237334
+ EVIDENCE_SPEC,
237335
+ APP_INSTALLATION,
237336
+ NO_TOKEN_INSTRUCTION,
237337
+ LAYER_IDS,
237338
+ STATUS,
237339
+ CHECK_NAME,
237340
+ GATE_ACTION,
237341
+ resolveGitHubAuth,
237342
+ redactSecrets,
237343
+ createGitHubHttpClient,
237344
+ evaluateLayers,
237345
+ scanLocalWorkflows,
237346
+ queryGitHubEnforcement,
237347
+ enforceAdminsEnabled,
237348
+ strictEnabled,
237349
+ environmentProtected
237350
+ };
237351
+ }
237352
+ });
237353
+
237354
+ // src/commands/enforce-check.js
237355
+ var require_enforce_check = __commonJS({
237356
+ "src/commands/enforce-check.js"(exports2, module2) {
237357
+ "use strict";
237358
+ var chalk = require_source();
237359
+ var {
237360
+ EVIDENCE_SPEC,
237361
+ APP_INSTALLATION,
237362
+ NO_TOKEN_INSTRUCTION,
237363
+ resolveGitHubAuth,
237364
+ createGitHubHttpClient,
237365
+ evaluateLayers,
237366
+ scanLocalWorkflows,
237367
+ queryGitHubEnforcement
237368
+ } = require_github_enforcement();
237369
+ var { isValidRepo } = require_status();
237370
+ var { resolveOwnerRepo } = require_setup_required_check();
237371
+ if (process.env.NO_COLOR) chalk.level = 0;
237372
+ var USAGE = [
237373
+ "Usage: coderifts enforce --check --repo owner/repo [--env <github-environment>] [--branch <name>] [--json]",
237374
+ "",
237375
+ "Query the provider (GitHub) and local workflow files. Report, per layer:",
237376
+ " VERIFIED / NOT_VERIFIED / UNVERIFIABLE(reason).",
237377
+ "inescapable_deploy is true only when every layer is VERIFIED.",
237378
+ "",
237379
+ "Auth: GITHUB_TOKEN (or GH_TOKEN). Never prints token values.",
237380
+ "The CodeRifts GitHub App installation cannot query branch protection",
237381
+ "(missing administration:read)."
237382
+ ].join("\n");
237383
+ function statusColor(status) {
237384
+ if (status === "VERIFIED") return chalk.green(status);
237385
+ if (status === "NOT_VERIFIED") return chalk.yellow(status);
237386
+ return chalk.red(status);
237387
+ }
237388
+ function renderHuman(report) {
237389
+ const lines = [];
237390
+ lines.push(chalk.bold("CodeRifts enforce --check"));
237391
+ lines.push(` repo: ${report.repo}`);
237392
+ lines.push(` branch: ${report.branch || "(unknown)"}`);
237393
+ if (report.environment) lines.push(` env: ${report.environment}`);
237394
+ lines.push(` queried: ${report.queried_at}`);
237395
+ lines.push(` auth: ${report.auth.source} (value not printed)`);
237396
+ lines.push(chalk.dim(` App installation: cannot query protection (${report.auth.app_installation.missing_scope})`));
237397
+ lines.push("");
237398
+ lines.push(chalk.bold(" Layer Status"));
237399
+ for (const l of report.layers) {
237400
+ const id = String(l.id).padEnd(34, " ");
237401
+ lines.push(` ${id}${statusColor(l.status)}`);
237402
+ if (l.status !== "VERIFIED" && l.reason) {
237403
+ lines.push(chalk.dim(` WHY ${l.reason}`));
237404
+ }
237405
+ if (l.endpoint) lines.push(chalk.dim(` via ${l.endpoint}`));
237406
+ }
237407
+ lines.push("");
237408
+ const claim = report.claim && report.claim.inescapable_deploy === true;
237409
+ lines.push(` inescapable_deploy: ${claim ? chalk.green("true") : chalk.yellow("false")}`);
237410
+ lines.push(chalk.dim(` ${report.claim.basis}`));
237411
+ lines.push("");
237412
+ lines.push(chalk.dim(" This is queried evidence, not a host claim."));
237413
+ lines.push(chalk.dim(" --enforce / CODERIFTS_DEPLOY_NO_BYPASS are host claims and do not set this field."));
237414
+ if (!claim) {
237415
+ lines.push("");
237416
+ lines.push(chalk.bold(" ACTION"));
237417
+ const missing = report.layers.filter((l) => l.status !== "VERIFIED");
237418
+ for (const l of missing) {
237419
+ if (l.id === "required_check") {
237420
+ lines.push(chalk.dim(' require "CodeRifts / contract-gate" on default-branch protection'));
237421
+ lines.push(chalk.dim(" (coderifts setup-required-check --apply)"));
237422
+ } else if (l.id === "enforce_admins") {
237423
+ lines.push(chalk.dim(" set enforce_admins: true so administrators cannot bypass"));
237424
+ lines.push(chalk.dim(" (coderifts setup-required-check --enforce-admins --apply)"));
237425
+ } else if (l.id === "strict_status_checks") {
237426
+ lines.push(chalk.dim(" set required_status_checks.strict: true"));
237427
+ } else if (l.id === "environment_protection") {
237428
+ lines.push(chalk.dim(" add a GitHub Environment with protection rules, pass --env <name>"));
237429
+ } else if (l.id === "workflow_contract_gate") {
237430
+ lines.push(chalk.dim(" add .github/workflows/coderifts.yml using coderifts/contract-gate@v0"));
237431
+ lines.push(chalk.dim(" (coderifts init --agents)"));
237432
+ } else if (l.id === "workflow_deploy_gate") {
237433
+ lines.push(chalk.dim(" add a CD step: coderifts deploy-gate --env <env> --artifact <id> --receipt <file>"));
237434
+ }
237435
+ }
237436
+ }
237437
+ return lines.join("\n");
237438
+ }
237439
+ async function runEnforceCheck(options = {}, deps = {}) {
237440
+ const log = deps.log || console.log.bind(console);
237441
+ const errLog = deps.errLog || console.error.bind(console);
237442
+ const cwd = deps.cwd || options.cwd || process.cwd();
237443
+ const now = deps.now || (() => (/* @__PURE__ */ new Date()).toISOString());
237444
+ const env = deps.env || process.env;
237445
+ const repoArg = options.repo;
237446
+ let ownerRepo = null;
237447
+ if (repoArg && isValidRepo(String(repoArg).trim())) {
237448
+ const [owner, repo] = String(repoArg).trim().split("/");
237449
+ ownerRepo = { owner, repo };
237450
+ } else if (!repoArg) {
237451
+ try {
237452
+ ownerRepo = resolveOwnerRepo(cwd, deps.gitImpl);
237453
+ } catch {
237454
+ ownerRepo = null;
237455
+ }
237456
+ }
237457
+ if (!ownerRepo) {
237458
+ errLog(chalk.red("Error: missing repo (pass --repo owner/repo)"));
237459
+ errLog(USAGE);
237460
+ return { exitCode: 1, error: "missing_repo" };
237461
+ }
237462
+ const providerName = String(options.provider || "github").toLowerCase();
237463
+ const queriedAt = now();
237464
+ const environmentName = options.env ? String(options.env).trim() : null;
237465
+ const local = (deps.scanLocalWorkflows || scanLocalWorkflows)(cwd, deps.fs || {});
237466
+ const authPublic = {
237467
+ source: "none",
237468
+ app_installation: { ...APP_INSTALLATION }
237469
+ };
237470
+ if (providerName !== "github") {
237471
+ const reason = `no ${providerName} adapter \u2014 GitHub is the only implemented provider; refusing to stub results`;
237472
+ const evaluated2 = evaluateLayers({
237473
+ protectionRead: null,
237474
+ environmentRead: null,
237475
+ environmentName,
237476
+ local,
237477
+ auth: { ok: false, source: "none", reason }
237478
+ });
237479
+ const report2 = {
237480
+ command: "enforce --check",
237481
+ spec: EVIDENCE_SPEC,
237482
+ queried_at: queriedAt,
237483
+ repo: `${ownerRepo.owner}/${ownerRepo.repo}`,
237484
+ branch: options.branch || null,
237485
+ environment: environmentName,
237486
+ provider: providerName,
237487
+ auth: { ...authPublic, reason },
237488
+ ...evaluated2
237489
+ };
237490
+ if (options.json) log(JSON.stringify(report2, null, 2));
237491
+ else log(renderHuman(report2));
237492
+ return { exitCode: 0, report: report2 };
237493
+ }
237494
+ const auth = (deps.resolveAuth || resolveGitHubAuth)(env);
237495
+ authPublic.source = auth.source;
237496
+ if (auth.reason) authPublic.reason = auth.reason;
237497
+ let branch = options.branch ? String(options.branch).trim() : null;
237498
+ let protectionRead = null;
237499
+ let environmentRead = null;
237500
+ if (auth.ok) {
237501
+ const client = deps.githubClient || createGitHubHttpClient({
237502
+ token: auth.token,
237503
+ fetchImpl: deps.fetchImpl
237504
+ });
237505
+ const queried = await queryGitHubEnforcement({
237506
+ owner: ownerRepo.owner,
237507
+ repo: ownerRepo.repo,
237508
+ branch,
237509
+ environmentName,
237510
+ client
237511
+ });
237512
+ branch = queried.branch || branch;
237513
+ protectionRead = queried.protectionRead;
237514
+ environmentRead = queried.environmentRead;
237515
+ }
237516
+ const evaluated = evaluateLayers({
237517
+ protectionRead,
237518
+ environmentRead,
237519
+ environmentName,
237520
+ local,
237521
+ auth
237522
+ });
237523
+ const report = {
237524
+ command: "enforce --check",
237525
+ spec: EVIDENCE_SPEC,
237526
+ queried_at: queriedAt,
237527
+ repo: `${ownerRepo.owner}/${ownerRepo.repo}`,
237528
+ branch,
237529
+ environment: environmentName,
237530
+ provider: "github",
237531
+ auth: authPublic,
237532
+ ...evaluated
237533
+ };
237534
+ if (options.json) log(JSON.stringify(report, null, 2));
237535
+ else log(renderHuman(report));
237536
+ return { exitCode: 0, report };
237537
+ }
237538
+ module2.exports = {
237539
+ runEnforceCheck,
237540
+ renderHuman,
237541
+ USAGE,
237542
+ NO_TOKEN_INSTRUCTION
237543
+ };
237544
+ }
237545
+ });
237546
+
237002
237547
  // src/commands/enforce.js
237003
237548
  var require_enforce = __commonJS({
237004
237549
  "src/commands/enforce.js"(exports2, module2) {
@@ -237021,14 +237566,19 @@ var require_enforce = __commonJS({
237021
237566
  var USAGE = [
237022
237567
  "Usage: coderifts enforce --repo owner/repo [--apply]",
237023
237568
  " or: coderifts enforce owner/repo [--apply]",
237569
+ " or: coderifts enforce --check --repo owner/repo [--env <name>] [--json]",
237024
237570
  "",
237025
237571
  "Cross-layer orchestrator: reads enforcement-status, then closes gaps by calling existing",
237026
237572
  "setup commands (setup-required-check, hook install, deploy-gate guidance).",
237027
237573
  "",
237574
+ "--check: query GitHub + local workflows and report VERIFIED / NOT_VERIFIED /",
237575
+ "UNVERIFIABLE per layer. inescapable_deploy is true only when every layer is VERIFIED.",
237576
+ "Does not use the CodeRifts API key. Auth: GITHUB_TOKEN. Read-only.",
237577
+ "",
237028
237578
  "DRY-RUN BY DEFAULT \u2014 without --apply, prints what WOULD run and mutates NOTHING.",
237029
237579
  "With --apply, threads apply into each underlying command (their own safety still applies).",
237030
237580
  "",
237031
- "Requires a cloud API key for the status read (coderifts login / CODERIFTS_API_KEY).",
237581
+ "Requires a cloud API key for the (non --check) status read (coderifts login / CODERIFTS_API_KEY).",
237032
237582
  "Merge apply uses your local `gh` credentials, not the CodeRifts API key."
237033
237583
  ].join("\n");
237034
237584
  var AGENT_GUARD_GUIDANCE = [
@@ -237039,8 +237589,10 @@ var require_enforce = __commonJS({
237039
237589
  ].join("\n");
237040
237590
  var DEPLOY_GUIDANCE = [
237041
237591
  "Deploy is declared-only on the server (never server-observed ENFORCING).",
237042
- "Add a CD step that runs: coderifts deploy-gate --env <env> --artifact <id> --receipt <file> --enforce",
237043
- " (fail-closed by default; --enforce attests ENFORCING for inescapable_deploy. CODERIFTS_DEPLOY_ADVISORY=1 to soften.)",
237592
+ "Add a CD step that runs: coderifts deploy-gate --env <env> --artifact <id> --receipt <file>",
237593
+ " (fail-closed by default. --enforce is a host claim, not inescapable_deploy.",
237594
+ " Prove pipeline protection with: coderifts enforce --check --repo owner/repo --env <env>.",
237595
+ " CODERIFTS_DEPLOY_ADVISORY=1 to soften the exit.)",
237044
237596
  "Also set policy.require_source_binding: true in .coderifts.yml for the deploy declaration leg."
237045
237597
  ].join("\n");
237046
237598
  var CONTENT_GUIDANCE = [
@@ -237067,6 +237619,15 @@ var require_enforce = __commonJS({
237067
237619
  }
237068
237620
  }
237069
237621
  async function runEnforce(options = {}, deps = {}) {
237622
+ if (options.check === true) {
237623
+ if (options.apply === true) {
237624
+ const errLog2 = deps.errLog || console.error.bind(console);
237625
+ errLog2(chalk.red("Error: --check is read-only evidence and cannot be combined with --apply"));
237626
+ return { exitCode: 1, error: "check_apply_conflict", outcomes: [] };
237627
+ }
237628
+ const { runEnforceCheck } = require_enforce_check();
237629
+ return runEnforceCheck(options, deps);
237630
+ }
237070
237631
  const apply = options.apply === true;
237071
237632
  const getKey = deps.getApiKey || getApiKey;
237072
237633
  const fetchStatus = deps.cloudGetEnforcementStatus || cloudGetEnforcementStatus;
@@ -240058,12 +240619,13 @@ program.command("status [repo]").description("Show cross-layer enforcement statu
240058
240619
  process.exitCode = result.exitCode;
240059
240620
  }
240060
240621
  });
240061
- program.command("enforce [repo]").description("Close enforcement gaps by chaining setup-required-check / hook / deploy guidance (dry-run default; use --apply to mutate)").option("--repo <owner/repo>", "Repository (owner/repo); also accepted as a positional argument").option("--apply", "Actually run underlying setup commands (default: dry-run only)").option("--json", "Machine-readable JSON result").action(async (repoPositional, options) => {
240622
+ program.command("enforce [repo]").description("Close enforcement gaps (dry-run default), or --check to query provider-native pipeline protection evidence").option("--repo <owner/repo>", "Repository (owner/repo); also accepted as a positional argument").option("--apply", "Actually run underlying setup commands (default: dry-run only)").option("--check", "Query GitHub + local workflows; report VERIFIED / NOT_VERIFIED / UNVERIFIABLE (read-only)").option("--env <name>", "With --check: GitHub Environment name to query for protection rules").option("--branch <name>", "With --check: branch to query (default: repository default branch)").option("--provider <name>", "With --check: provider adapter (github only; others UNVERIFIABLE, never stubbed)", "github").option("--json", "Machine-readable JSON result").action(async (repoPositional, options) => {
240062
240623
  const { runEnforce } = require_enforce();
240063
240624
  const result = await runEnforce({
240064
240625
  ...options,
240065
240626
  repo: options.repo || repoPositional || null,
240066
- apply: !!options.apply
240627
+ apply: !!options.apply,
240628
+ check: !!options.check
240067
240629
  });
240068
240630
  if (result && typeof result.exitCode === "number") {
240069
240631
  process.exitCode = result.exitCode;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coderifts",
3
- "version": "4.6.0",
3
+ "version": "4.7.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",