Skip to content
Open
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
94 changes: 94 additions & 0 deletions .github/RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,100 @@ Stop publication, rotate or fix identity, then rerun from the same tag.

## Release Evidence

### Registry-backed closure contract

The `Verify public registry closure` job implements
[#866](https://github.com/git-stunts/git-warp/issues/866) after the registry
publish and GitHub Release jobs. It runs even when a publication job failed,
so a partial publication cannot finish with an unqualified green workflow.

The verifier reads the immutable tag and its reviewed source commit,
then independently observes the exact version in npm and JSR. It checks
npm `gitHead` against that commit and the publishing workflow commit, retains
both registries' integrity values, and records npm provenance availability,
the observed dist-tag, and the GitHub Release identity.

Registry propagation has a finite retry budget and each external command
has a timeout. A visible version with conflicting identity is a failure,
not a reason to retry or publish again. Existing versions can be verified
again without changing tags, dist-tags, registry artifacts, or release notes.
An older immutable release may remain valid after its dist-tag advances;
the receipt must distinguish exact-version visibility from current dist-tag
ownership.

Consumer verification installs the exact public npm version into a fresh
directory outside the checkout. It proves the supported root import, the
private-storage export firewall, installed CLI startup, and the resolved
git-cas and Plumbing versions. Registry signature and attestation checking
must run where npm supports it; a failed check must never become an
unqualified success.

The job leaves a small JSON closure receipt and uploads it even on failure.
Only a receipt whose required checks all succeeded reports `verified`.
The JSR archive is downloaded and hashed against its advertised SHA-512
integrity; the npm installation's lockfile must match npm's advertised
integrity. npm verifies registry signatures and provenance attestations.

The same read-only check can rehearse or re-verify an existing release:

```bash
bash scripts/verify-published-release.sh \
--tag v19.1.0 \
--commit 20a9b7f76d07195de0782f0852102434bdd200d3 \
--run-id 32825544033 \
--dist-tag latest \
--output /tmp/git-warp-release-closure.json
```

It requires Bash, Node/npm with `npm audit signatures` support, Git, `gh`,
`jq`, GNU `timeout`, `curl`, and OpenSSL. GitHub access is read-only. The
checkout must contain the immutable tag; installation occurs outside it.
Add `--require-dist-tag` when verifying a new publication. Historical reruns
record the current dist-tag owner without claiming that the old version is
still latest.

The verifier has a 720-second aggregate work budget. Each command and propagation
delay is capped by the remaining budget, and the consumer inherits only the
remaining time. Commands allow at most five additional seconds to terminate;
the 15-minute job ceiling leaves headroom for setup and receipt upload. The
receipt records the budget limit and whether it was exhausted.

Within that aggregate budget, the default registry policy is six attempts with
ten-second delays and 180-second command limits. Diagnostic overrides are
`GIT_WARP_CLOSURE_ATTEMPTS` (1–10),
`GIT_WARP_CLOSURE_DELAY_SECONDS` (0–30), and
`GIT_WARP_CLOSURE_COMMAND_TIMEOUT_SECONDS` (1–180).
`GIT_WARP_CLOSURE_TOTAL_TIMEOUT_SECONDS` may reduce the aggregate budget (1–720).
Consumer installation and verification commands retain 180-second ceilings,
further capped by their remaining aggregate budget. Signature and attestation
verification precedes execution of the imported package and installed CLI.
Run the adversarial contract suite with
`bats test/bats/release-closure.bats`.

The contract suite is medium-sized: it owns scratch state and controls GitHub
and registry command responses while executing real Node imports, CLI processes,
hashing, and OS timeouts. Its specified oracle is this release contract. The
command fixtures reject unexpected operations; they do not establish npm or
GitHub conformance. A real public-registry rehearsal supplies separate integration
evidence. That deliberately non-hermetic verification controls versions, source
identity, integrity, and a finite propagation budget; it cannot make registry
availability deterministic.

At assertion authoring or material change, run:

```bash
bash scripts/release-closure/calibrate.sh /tmp/git-warp-release-calibration.json
```

Calibration first requires the ordinary suite to pass. It then applies named
contract violations to disposable copies and requires the corresponding test
to fail at its stated assertion. Setup failures, missing selected tests, syntax
errors, and outer watchdog expiries do not count. The JSON receipt retains each
named experiment and failure output. This is selective falsification evidence,
not a mutation score or a proof that all possible bugs are detected. The command
deadline test additionally distinguishes the verifier's own timeout from its
independent outer watchdog.

Record release evidence in the release tracking issue or retrospective:

- tag name and commit SHA;
Expand Down
52 changes: 52 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -439,3 +439,55 @@ jobs:
run: |
echo "Both npm and JSR publish failed."
exit 1

verify_registry_closure:
name: Verify public registry closure
runs-on: ubuntu-latest
timeout-minutes: 15
needs: [verify, publish_npm, publish_jsr, github_release]
if: ${{ always() && needs.verify.result == 'success' }}
permissions:
# Observe the publishing run and tagged source without publication rights.
actions: read
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
ref: ${{ needs.verify.outputs.tag }}
fetch-depth: 0
persist-credentials: false

- name: Setup Node 24
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
with:
node-version: '24'
package-manager-cache: false

- name: Verify tagged public artifacts and a fresh consumer
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ needs.verify.outputs.tag }}
RELEASE_COMMIT: ${{ github.sha }}
RELEASE_RUN_ID: ${{ github.run_id }}
RELEASE_DIST_TAG: ${{ needs.verify.outputs.npm_dist_tag }}
NPM_PUBLISH_STATUS: ${{ needs.publish_npm.outputs.status }}
CLOSURE_RECEIPT: ${{ runner.temp }}/git-warp-release-closure.json
shell: bash
run: |
set -euo pipefail
args=(--tag "$RELEASE_TAG" --commit "$RELEASE_COMMIT"
--run-id "$RELEASE_RUN_ID" --dist-tag "$RELEASE_DIST_TAG"
--output "$CLOSURE_RECEIPT")
if [ "$NPM_PUBLISH_STATUS" = published ]; then
args+=(--require-dist-tag)
fi
bash scripts/verify-published-release.sh "${args[@]}"

- name: Retain release closure receipt
if: ${{ always() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: release-closure-${{ needs.verify.outputs.tag }}
path: ${{ runner.temp }}/git-warp-release-closure.json
if-no-files-found: error
retention-days: 90
28 changes: 21 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions scripts/release-closure/budget.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# Shared elapsed-time budget for the registry driver and independent consumer.

start_budget() {
[[ "$1" =~ ^[1-9][0-9]*$ ]] && [ "$1" -le 720 ] || return 2
[[ "$2" =~ ^[1-9][0-9]*$ ]] && [ "$2" -le 180 ] || return 2
CLOSURE_DEADLINE=$((SECONDS + $1))
CLOSURE_COMMAND_LIMIT="$2"
}

budget_remaining() {
local remaining=$((CLOSURE_DEADLINE - SECONDS))
if [ "$remaining" -le 0 ]; then
echo 'release closure aggregate time budget exhausted' >&2
return 124
fi
printf '%s\n' "$remaining"
}

bounded() {
local limit
limit=$(budget_remaining) || return $?
if [ "$limit" -gt "$CLOSURE_COMMAND_LIMIT" ]; then limit="$CLOSURE_COMMAND_LIMIT"; fi
timeout --kill-after=5s "${limit}s" "$@"
}
140 changes: 140 additions & 0 deletions scripts/release-closure/calibrate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
#!/usr/bin/env bash
# Calibrate release assertions against named contract violations in owned copies.
# Mutation anchors are literal program text; expanding them would execute the
# subject while constructing the experiment, so SC2016 does not apply here.
# shellcheck disable=SC2016
set -euo pipefail

ROOT=$(cd "$(dirname "$0")/../.." && pwd)
OUTPUT="${1:?usage: calibrate.sh <receipt.json>}"
mkdir -p "$(dirname "$OUTPUT")"
OUTPUT=$(cd "$(dirname "$OUTPUT")" && pwd)/$(basename "$OUTPUT")
WORK=$(mktemp -d "${TMPDIR:-/tmp}/git-warp-closure-calibration.XXXXXX")
trap 'rm -rf "$WORK"' EXIT
mkdir -p "$WORK/tree/scripts/release-closure" "$WORK/tree/test/bats/fixtures"
cp "$ROOT/test/bats/release-closure.bats" "$WORK/tree/test/bats/"
cp "$ROOT/test/bats/fixtures/release-closure-command.sh" "$WORK/tree/test/bats/fixtures/"
export LC_ALL=C TZ=UTC
RESULTS="$WORK/results.jsonl"
: > "$RESULTS"
INPUT_DIGEST=$(cat "$ROOT/scripts/verify-published-release.sh" \
"$ROOT/scripts/release-closure/consumer.sh" "$ROOT/scripts/release-closure/calibrate.sh" \
"$ROOT/scripts/release-closure/budget.sh" \
"$ROOT/test/bats/release-closure.bats" "$ROOT/test/bats/fixtures/release-closure-command.sh" |
openssl dgst -sha256 -binary | openssl base64 -A)
NODE_VERSION=$(node --version)
BATS_VERSION=$(bats --version)

reset_subject() {
cp "$ROOT/scripts/verify-published-release.sh" "$WORK/tree/scripts/"
cp "$ROOT/scripts/release-closure/consumer.sh" "$WORK/tree/scripts/release-closure/"
cp "$ROOT/scripts/release-closure/budget.sh" "$WORK/tree/scripts/release-closure/"
}

replace_once() {
local path="$WORK/tree/$1" before="$2" after="$3"
jq -Rsj --arg before "$before" --arg after "$after" '
split($before)|if length==2 then .[0]+$after+.[1]
else error("mutation anchor must occur exactly once") end
' "$path" > "$WORK/replacement"
mv "$WORK/replacement" "$path"
}

record() {
# Logs must be shareable: scratch and checkout paths carry no release evidence.
jq -n --arg mutation "$1" --arg test "$2" --arg verdict "$3" \
--arg root "$ROOT" --arg work "$WORK" --rawfile log "$WORK/run.log" '
{mutation:$mutation,test:$test,verdict:$verdict,
output:($log|split($root)|join("<checkout>")|split($work)|join("<calibration>"))}
' >> "$RESULTS"
jq -s --arg revision "$(git -C "$ROOT" rev-parse HEAD)" \
--arg inputs "$INPUT_DIGEST" --arg bash "$BASH_VERSION" \
--arg node "$NODE_VERSION" --arg bats "$BATS_VERSION" '
{schema:"git-warp/release-calibration@1",revision:$revision,inputDigest:$inputs,
tools:{bash:$bash,node:$node,bats:$bats},
results:.,status:(if all(.[];.verdict=="killed" or .verdict=="baseline-passed")
then "incomplete" else "failed" end)}' "$RESULTS" > "$OUTPUT"
}

reset_subject
if timeout --kill-after=5s 180s bats --formatter tap "$WORK/tree/test/bats/release-closure.bats" > "$WORK/run.log" 2>&1; then
record baseline all baseline-passed
else
record baseline all failed
cat "$WORK/run.log"
exit 1
fi

calibrate() {
local name="$1" test="$2" assertion="$3" path="$4" before="$5" after="$6" code=0
reset_subject
replace_once "$path" "$before" "$after"
bash -n "$WORK/tree/scripts/verify-published-release.sh"
bash -n "$WORK/tree/scripts/release-closure/consumer.sh"
bash -n "$WORK/tree/scripts/release-closure/budget.sh"
timeout --kill-after=5s 30s bats --formatter tap --filter "^$test$" \
"$WORK/tree/test/bats/release-closure.bats" > "$WORK/run.log" 2>&1 || code=$?
# Exit 1 alone could be a setup crash or no selected test. Require the exact
# test and failed assertion, excluding harness failures and external timeouts.
if [ "$code" -eq 1 ] && rg -Fx "not ok 1 $test" "$WORK/run.log" >/dev/null &&
rg -F "$assertion" "$WORK/run.log" >/dev/null; then
record "$name" "$test" killed
echo "killed: $name ($test)"
else
record "$name" "$test" uncalibrated
cat "$WORK/run.log"
echo "calibration failed: $name (BATS exit $code; expected assertion failure exit 1)" >&2
exit 1
fi
}

DRIVER=scripts/verify-published-release.sh
CONSUMER=scripts/release-closure/consumer.sh
BUDGET=scripts/release-closure/budget.sh
NONZERO='assertion: invalid release must exit nonzero'
SUCCESS='[ "$status" -eq 0 ]'
calibrate receipt-claim 'release closure proves public identity and an independent consumer' \
'jq -e' "$CONSUMER" 'registrySignatures:"verified"' 'registrySignatures:"unverified"'
calibrate propagation-budget 'release closure retries delayed visibility in both registries' \
"$SUCCESS" "$DRIVER" 'attempt=$((attempt + 1))' 'attempt=$((attempt + 2))'
calibrate finite-budget 'release closure exhausts a finite visibility budget' \
'npm-attempts' "$DRIVER" 'local attempt=1' 'local attempt=0'
calibrate command-deadline 'release closure terminates a stalled external command' \
'[ "$status" -ne 124 ]' "$BUDGET" \
'timeout --kill-after=5s "${limit}s" "$@"' '"$@"'
calibrate total-deadline 'release closure bounds the total verification time and retains a failed receipt' \
"$NONZERO" "$BUDGET" 'CLOSURE_DEADLINE=$((SECONDS + $1))' 'CLOSURE_DEADLINE=$((SECONDS + 720))'
calibrate consumer-deadline 'release closure includes consumer installation in its aggregate time budget' \
"$NONZERO" "$BUDGET" 'CLOSURE_DEADLINE=$((SECONDS + $1))' 'CLOSURE_DEADLINE=$((SECONDS + 720))'
calibrate public-tag 'release closure rejects a changed public tag' \
"$NONZERO" "$DRIVER" '[ "$(cat "$WORK/remote-commit")" = "$EXPECTED_COMMIT" ]' ':'
calibrate publishing-run 'release closure rejects a publishing run from a different commit' \
"$NONZERO" "$DRIVER" '.head_sha==$commit and .repository' 'true and .repository'
calibrate failed-receipt 'release closure retains a failed receipt after malformed GitHub transport' \
'assertion: failed release must retain a valid failed receipt' "$DRIVER" \
'> "$OUTPUT"' '> "$WORK/discarded-receipt.json"'
calibrate npm-identity 'release closure rejects conflicting npm identity without retrying' \
"$NONZERO" "$DRIVER" "'.gitHead==\$commit'" "'true'"
calibrate provenance 'release closure rejects absent provenance' \
"$NONZERO" "$DRIVER" ".dist.attestations.provenance.predicateType==\"https://slsa.dev/provenance/v1\"" 'true'
calibrate jsr-integrity 'release closure hashes the JSR archive against registry integrity' \
"$NONZERO" "$DRIVER" '[ "$JSR_INTEGRITY" = "$(jq -r .dist.integrity "$WORK/jsr.json")" ]' ':'
calibrate npm-integrity 'release closure rejects npm consumer integrity mismatch' \
"$NONZERO" "$CONSUMER" '.integrity==$integrity' 'true'
calibrate installation-failure 'release closure rejects consumer installation failure' \
'jq -r .consumer.stage' "$CONSUMER" '> "$WORK/install.log" 2>&1' '> "$WORK/install.log" 2>&1 || true'
calibrate root-export 'release closure executes the public import rather than checking metadata' \
"$NONZERO" "$CONSUMER" "if (names.length !== 1 || names[0] !== 'Runtime' || typeof values.Runtime !== 'function')" 'if (false)'
calibrate private-export 'release closure rejects a newly exposed private storage import' \
"$NONZERO" "$CONSUMER" 'if (!denied) throw' 'if (false) throw'
calibrate cli-execution 'release closure executes the installed CLI' \
"$NONZERO" "$CONSUMER" '> "$WORK/cli.log" 2>&1' '> "$WORK/cli.log" 2>&1 || true'
calibrate signature-verification 'release closure rejects failed signature or attestation verification' \
"$NONZERO" "$CONSUMER" '> "$WORK/signatures.log" 2>&1' '> "$WORK/signatures.log" 2>&1 || true'
calibrate dist-tag-ownership 'new publication must own its intended dist-tag' \
"$NONZERO" "$DRIVER" '[ "$REQUIRE_DIST_TAG" -eq 1 ]' '[ "$REQUIRE_DIST_TAG" -eq 2 ]'
calibrate historical-dist-tag 'immutable historical verification records an advanced dist-tag honestly' \
'jq -e' "$DRIVER" 'ownsTag:($owner[0]==$version)' 'ownsTag:true'
jq '.status="verified"' "$OUTPUT" > "$WORK/complete.json"
mv "$WORK/complete.json" "$OUTPUT"
echo 'release calibration: all named violations detected at their target assertions'
Loading
Loading