Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

All notable changes to `github-delivery` are documented here.

## [1.10.0] - 2026-11-08

### Added

- An explicit, per-repository local CI evidence mode. User configuration schema 2 can select `ciEvidenceMode: local` for one repository with a recorded user waiver, while the default stays remote. The merge gate then requires a user-scoped local receipt bound to the repository, PR, exact head and base SHA, command/suite scope, exit codes, timestamps, runner host, tool versions, and the complete log hash. Missing, foreign, red, stale, pre-head, tampered, or non-ancestor receipts stay blocked; failed remote checks stay visible as observations and never become green. Reviews, authority, draft handling, branch protection, and all other gates are unchanged. New commands: `github-delivery-config --ci-evidence-mode` and `local-ci-receipt`.

### Changed

- Bumped the package version from `1.9.1` to `1.10.0`.

## [1.9.1] - 2026-11-08

### Fixed
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "github-delivery",
"version": "1.9.1",
"version": "1.10.0",
"description": "Guided installer and safety tooling for GitHub Delivery workflows",
"type": "module",
"engines": {
Expand All @@ -22,6 +22,7 @@
"scripts/codex-with-watchdog.mjs",
"scripts/install-codex-watchdog-hooks.mjs",
"scripts/install-skill.mjs",
"scripts/local-ci-receipt.mjs",
"scripts/windows-install-locks.ps1",
"scripts/lib/agent-debug-trace.mjs",
"scripts/lib/agent-trace-launcher.mjs",
Expand All @@ -44,6 +45,7 @@
"scripts/lib/grok-trace-launcher.mjs",
"scripts/lib/install-lock.mjs",
"scripts/lib/installation-backups.mjs",
"scripts/lib/local-ci-evidence.mjs",
"scripts/lib/release-path-identity.mjs",
"scripts/lib/release-self-update.mjs",
"scripts/lib/subprocess-policy.mjs",
Expand Down
23 changes: 23 additions & 0 deletions references/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,29 @@ On Windows, the same stored preference is available in **Control Center > Settin

The config is global for the user's github-delivery installation and lives outside the installed skill directory.

## Local CI evidence mode (explicit opt-in, per repository)

The default required-check source is remote GitHub check evidence. If a repository cannot produce usable remote check runs (for example, an exhausted Actions quota), the user may explicitly select local evidence for that repository only:

```bash
node scripts/github-delivery-config.mjs --ci-evidence-mode local --repository OWNER/REPO --waiver "user authorization reference"
node scripts/github-delivery-config.mjs --ci-evidence-mode remote --repository OWNER/REPO
node scripts/github-delivery-config.mjs --show --repository OWNER/REPO
```

The local mode is stored in user configuration schema 2 and never read from repository files. It records the repository identity, the waiver reference, and the time of the change. Without an entry for the repository, remote checks remain authoritative.

A merge with local mode active requires a matching receipt:

```bash
node scripts/local-ci-receipt.mjs OWNER/REPO PR --log PATH\to\run.log \
--command "npm run ci:local -- --full" --scope "full local CI"
```

The receipt binds the repository, pull request, exact head SHA, base SHA, command and suite scope, exit codes, timestamps, runner host, Node/npm/git versions, and the SHA-256 of the complete log. Receipts live under the user's github-delivery data directory, not in the repository. The merge gate stays blocked when the receipt is missing, belongs to another repository or PR, targets another head, records a non-zero exit, has a tampered log hash, was recorded before the head commit, is older than seven days, or its base is no longer part of the current base history. Remote check results remain visible as observations and advisories; they are not presented as green.

Reviews, authority grants, draft handling, branch protection, conversation resolution, and every other gate keep working unchanged. The mode only substitutes the required-check evidence source for the named repository.

## Windows Authority host

For normal stable installations, use the managed lifecycle:
Expand Down
52 changes: 44 additions & 8 deletions scripts/lib/config-command.mjs
Original file line number Diff line number Diff line change
@@ -1,20 +1,42 @@
import {
resolveCiEvidenceMode,
readUserConfig,
resolveAuthorityMode,
withCiEvidenceRepository,
writeUserConfig,
} from "./user-config.mjs";

export function parseConfigArgs(argv = []) {
const options = { show: false, authorityMode: null };
const options = {
show: false,
authorityMode: null,
ciEvidenceMode: null,
repository: null,
waiver: null,
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--show") options.show = true;
else if (arg === "--authority-mode") {
options.authorityMode = argv[++index];
if (!options.authorityMode) throw new Error("--authority-mode requires a value");
} else if (arg === "--ci-evidence-mode") {
options.ciEvidenceMode = argv[++index];
if (!options.ciEvidenceMode) throw new Error("--ci-evidence-mode requires a value");
} else if (arg === "--repository") {
options.repository = argv[++index];
if (!options.repository) throw new Error("--repository requires a value");
} else if (arg === "--waiver") {
options.waiver = argv[++index];
if (!options.waiver) throw new Error("--waiver requires a value");
} else throw new Error(`unknown argument: ${arg}`);
}
if (!options.show && !options.authorityMode) options.show = true;
if (options.ciEvidenceMode && !options.repository) {
throw new Error("--ci-evidence-mode requires --repository OWNER/REPO");
}
if (!options.show && !options.authorityMode && !options.ciEvidenceMode) {
options.show = true;
}
return options;
}

Expand All @@ -24,18 +46,32 @@ export function runConfigCommand({ argv = [], env = process.env, dependencies =
const options = parseConfigArgs(argv);
const current = read({ env });
let stored = current;
if (options.authorityMode) {
stored = write({
...current.config,
authorityMode: String(options.authorityMode).trim().toLowerCase(),
});
if (options.authorityMode || options.ciEvidenceMode) {
let next = current.config;
if (options.authorityMode) {
next = {
...next,
authorityMode: String(options.authorityMode).trim().toLowerCase(),
};
}
if (options.ciEvidenceMode) {
next = withCiEvidenceRepository(next, {
repository: options.repository,
mode: String(options.ciEvidenceMode).trim().toLowerCase(),
waiver: options.waiver,
});
}
stored = write(next);
}
return {
schemaVersion: 1,
kind: "github-delivery/user-configuration",
path: stored.path,
source: options.authorityMode ? "updated" : current.source,
source: options.authorityMode || options.ciEvidenceMode ? "updated" : current.source,
config: stored.config,
effectiveAuthorityMode: resolveAuthorityMode({ config: stored.config, env }),
effectiveCiEvidenceMode: options.repository
? resolveCiEvidenceMode({ config: stored.config, repo: options.repository }).mode
: null,
};
}
7 changes: 6 additions & 1 deletion scripts/lib/delivery-workflow-controller.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -232,9 +232,14 @@ export function assertPreOpenPublicationEvidence(snapshot, request = null) {
const requestHead = String(request?.head || "");
const separator = requestHead.indexOf(":");
const requestBranch = separator < 0 ? requestHead : requestHead.slice(separator + 1);
const gateHeadRef = String(gate.headRef || "");
// Local-work gates bind headRef to the exact commit SHA; issue-work gates
// bind it to the head branch. Only compare branch names when that is what
// the gate recorded; the SHA identity is already checked above.
const gateHeadRefIsSha = /^[0-9a-f]{40,64}$/i.test(gateHeadRef);
if (
(request?.base && gate.baseRef && String(request.base) !== String(gate.baseRef)) ||
(requestBranch && gate.headRef && requestBranch !== String(gate.headRef)) ||
(requestBranch && gateHeadRef && !gateHeadRefIsSha && requestBranch !== gateHeadRef) ||
(request?.publishedHead && !sameIdentity(request.publishedHead.sha, gate.headSha))
) {
throw new Error("pre_open_evidence_scope_mismatch");
Expand Down
Loading
Loading