From b9d077746441e46848f967db07410cf2fd2ee8c1 Mon Sep 17 00:00:00 2001 From: kikashy <35789537+kikashy@users.noreply.github.com> Date: Tue, 20 Oct 2026 11:08:17 -0400 Subject: [PATCH] Adopt the development kit: dev.json, scripts/dev/check.sh, AGENTS.md and CLAUDE.md, CI dev.json lists every check CI ran, in order, as a step with its toolchain and summary pattern, and adds three: the machine-local-path grep, the agent-docs sync check and git diff --check. The steps that checked out and built the pinned Runtimes, the old Runner and the Gateway are one step, scripts/checks/build-pinned.sh, which fetches each pin into .ci/ (ignored by git); the longer test steps are scripts under scripts/checks/ with the comments they had in the workflow. scripts/dev/check.sh and scripts/sync-agent-docs.sh are vendored from the development kit v0.1.0. AGENTS.md and CLAUDE.md carry the shared rules and a section for this repository. The test job is now `check`, still named `Runner ()`, and runs the manifest on Linux and macOS; `Release contents` keeps its name and runs the release steps of the manifest. release.yml still calls ci.yml. Co-Authored-By: Claude Sonnet 5.5 Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com> --- .github/workflows/ci.yml | 195 +++------------------------- .gitignore | 3 + AGENTS.md | 45 +++++++ CLAUDE.md | 45 +++++++ README.md | 9 ++ dev.json | 21 +++ scripts/checks/build-pinned.sh | 62 +++++++++ scripts/checks/chain-tests.sh | 25 ++++ scripts/checks/local-paths.sh | 12 ++ scripts/checks/manual-gate.sh | 48 +++++++ scripts/checks/release-programs.sh | 8 ++ scripts/checks/release-toolchain.sh | 5 + scripts/checks/unit-tests.sh | 11 ++ scripts/dev/check.sh | 51 ++++++++ scripts/sync-agent-docs.sh | 23 ++++ 15 files changed, 384 insertions(+), 179 deletions(-) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 dev.json create mode 100755 scripts/checks/build-pinned.sh create mode 100755 scripts/checks/chain-tests.sh create mode 100755 scripts/checks/local-paths.sh create mode 100755 scripts/checks/manual-gate.sh create mode 100755 scripts/checks/release-programs.sh create mode 100755 scripts/checks/release-toolchain.sh create mode 100755 scripts/checks/unit-tests.sh create mode 100755 scripts/dev/check.sh create mode 100755 scripts/sync-agent-docs.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index edac645..36576a2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,11 @@ name: CI +# The check list is dev.json, run by scripts/dev/check.sh (the development kit's vendored runner); +# the longer steps are scripts under scripts/checks/ with the comments they had here. Two jobs, both +# required by branch protection under their names: `Runner ()` runs every step but the release +# ones on Linux and macOS, and `Release contents` runs the release steps with the exact toolchain +# release.yml names. + on: pull_request: push: @@ -11,7 +17,7 @@ permissions: contents: read jobs: - test: + check: name: Runner (${{ matrix.os }}) runs-on: ${{ matrix.os }} timeout-minutes: 15 @@ -21,170 +27,17 @@ jobs: os: [ubuntu-latest, macos-latest] steps: - uses: actions/checkout@v4 + # First, so that a manifest that does not parse fails before the long steps start. + - name: The manifest parses and its first step runs + run: scripts/dev/check.sh --manifest dev.json --only local-paths - uses: actions/setup-go@v5 with: go-version: '1.26.x' cache: true - - name: Check formatting - run: | - go fmt ./... - git diff --exit-code - - name: Vet - run: go vet ./... - # Real-Runtime tests must run in CI, not silently skip behind an unset - # JPACK_TEST_BIN. Pin the independently versioned evaluator contract. - # A commit is pinned and not a tag, since a commit cannot come to name - # other code. The name after it is the release the commit was taken - # from when the pin was set: whoever moves one moves the other. - # v0.26.0 is the first Runtime release that chains and signs its trail, - # so TestTheRuntimesVerifierReadsTheChainOfRuns, - # TestASignedRunKeepsItsSignature and - # TestVerifyRunChecksARunTheRuntimeSigned run here and do not skip (#35). - # v0.30.0 is the first that refuses to sign with a seed whose public - # key is not JPACK_SIGNING_PUBLIC_KEY, which - # TestASeedReplacedAfterStartSignsNothing needs (#54). v0.31.0 is the - # first that takes a caller's binding (`--binding`, runtime ADR-0050), - # which the binding tests need (#55). A pin moved to a Runtime without - # `jpack audit` or `--binding` would skip them again, and the step after - # the tests fails if any of them does not pass. - - name: Check out tested Runtime - uses: actions/checkout@v4 - with: - repository: Judgment-Pack/judgment-pack-runtime - ref: 09206e743cdf410d7b5ee970f1c9d09ca7c015ca # merged citation session identifier of any string (runtime #291) - path: .ci-runtime - persist-credentials: false - # The build names the pin's release, as the release build does, so a - # test can tell what the Runtime under test answers by its release - # (TestTheRuntimesVerifierReadsTheChainOfRuns). Whoever moves the pin - # moves the version with it. - - name: Build Runtime - working-directory: .ci-runtime - run: go build -trimpath -ldflags "-X github.com/Judgment-Pack/judgment-pack-runtime/internal/result.CLIVersion=0.33.0-dev.09206e7" -o "$RUNNER_TEMP/jpack" ./cmd/jpack - # Test-only old companion: its current-store refusal is part of the - # explicit manual-store preparation contract, not a release dependency. - - name: Check out ownership-era Runner - uses: actions/checkout@v4 - with: - repository: Judgment-Pack/judgment-pack-runner - ref: 1cc6b29885b344716a970cd21fefeaca0a4edaeb - path: .ci-runner-old - persist-credentials: false - - name: Build ownership-era Runner - working-directory: .ci-runner-old - run: go build -buildvcs=false -trimpath -o "$RUNNER_TEMP/jpack-runner-old" ./cmd/jpack-runner - # A Runtime that signs and does not know the expected key, as every - # release from v0.26.0 to v0.29.x: Runner must name the key a replaced - # seed signed under (TestAnEarlierRuntimeSigningWithAReplacedSeedIsNamed, - # #54). It takes no binding either, as every release before v0.31.0: - # Runner must give it none, and verify-run report its record unbound - # (TestARuntimeWithoutTheBindingIsGivenNone, - # TestVerifyRunReportsARecordTheRuntimeDidNotBind, #55). Pinned by - # commit as the tested Runtime is. - - name: Check out an earlier signing Runtime - uses: actions/checkout@v4 - with: - repository: Judgment-Pack/judgment-pack-runtime - ref: 7a2fcfe20cedba5f164246fc544c4920971d2c56 # v0.27.1 - path: .ci-runtime-earlier - persist-credentials: false - - name: Build the earlier Runtime - working-directory: .ci-runtime-earlier - run: go build -trimpath -ldflags "-X github.com/Judgment-Pack/judgment-pack-runtime/internal/result.CLIVersion=0.27.1" -o "$RUNNER_TEMP/jpack-earlier" ./cmd/jpack - - name: Check out durable Gateway contract - uses: actions/checkout@v4 - with: - repository: Judgment-Pack/judgment-pack-gateway - ref: 5b49e1e094b55fdd1deb9f613815c2beebd411f1 # v0.9.1 - path: .ci-gateway - persist-credentials: false - - name: Build Gateway and MCP adapter - run: | - cd .ci-gateway/go - go build -trimpath -o "$RUNNER_TEMP/gateway" . - cd ../adapters - go build -trimpath -o "$RUNNER_TEMP/adapter-mcp" ./cmd/adapter-mcp - - name: Manual admission helper and real Runtime gate tests cannot skip - run: | - out="$(JPACK_TEST_BIN="$RUNNER_TEMP/jpack" \ - JPACK_OLD_RUNNER_TEST_BIN="$RUNNER_TEMP/jpack-runner-old" \ - go test -count=1 -v \ - -run '^(TestManualGovernedRealRuntimeGate|TestManualGovernedActualRuntimeHasNoAuthorityFD|TestManualStorePrepareActualChildRetainsOriginalCustody|TestManualStorePreparationDurableAndNoDowngrade|TestManualStorePreparationSubstitutedStateRefuses|TestManualStoreSQLiteTargetUsesHeldDirectory|TestManualStoreLibraryNeverChangesCWD|TestManualInheritedAuthoritySocketClosesBeforeRuntimeExec|TestManualAuthorityDescriptorObserverDetectsSocket|TestManualStorePreparationPendingAndUnsupported|TestManualExternalDemandCannotBeOmitted|TestManualQueuedRunListsKeepPrivatePreimages)$' \ - ./internal/runner)" || { printf '%s\n' "$out"; exit 1; } - printf '%s\n' "$out" - for name in \ - TestManualGovernedRealRuntimeGate \ - TestManualGovernedRealRuntimeGate/success \ - TestManualGovernedRealRuntimeGate/blocked \ - TestManualGovernedRealRuntimeGate/lost-reply \ - TestManualGovernedRealRuntimeGate/changed-facts \ - TestManualGovernedRealRuntimeGate/changed-config \ - TestManualGovernedRealRuntimeGate/changed-lock \ - TestManualGovernedRealRuntimeGate/changed-signing \ - TestManualGovernedRealRuntimeGate/changed-epoch \ - TestManualGovernedRealRuntimeGate/changed-demand \ - TestManualGovernedRealRuntimeGate/journal-failure \ - TestManualGovernedRealRuntimeGate/start-journal-failure \ - TestManualGovernedRealRuntimeGate/recovered-reserve \ - TestManualStorePrepareActualChildRetainsOriginalCustody \ - TestManualStorePrepareActualChildRetainsOriginalCustody/committed \ - TestManualStorePrepareActualChildRetainsOriginalCustody/refused \ - TestManualStorePrepareActualChildRetainsOriginalCustody/lose-reply \ - TestManualStorePreparationDurableAndNoDowngrade \ - TestManualStorePreparationSubstitutedStateRefuses \ - TestManualStorePreparationSubstitutedStateRefuses/before-adapter \ - TestManualStorePreparationSubstitutedStateRefuses/after-adapter \ - TestManualStoreSQLiteTargetUsesHeldDirectory \ - TestManualStoreLibraryNeverChangesCWD \ - TestManualInheritedAuthoritySocketClosesBeforeRuntimeExec \ - TestManualAuthorityDescriptorObserverDetectsSocket \ - TestManualStorePreparationPendingAndUnsupported \ - TestManualExternalDemandCannotBeOmitted \ - TestManualQueuedRunListsKeepPrivatePreimages; do - printf '%s\n' "$out" | grep -q -- "^[[:space:]]*--- PASS: $name " || { echo "$name did not run and pass"; exit 1; } - done - # This observer identifies the actual Runtime through Linux /proc. - # Every other named helper/gate test is mandatory on both hosts. - if [ "$RUNNER_OS" = Linux ]; then - name=TestManualGovernedActualRuntimeHasNoAuthorityFD - printf '%s\n' "$out" | grep -q -- "^[[:space:]]*--- PASS: $name " || { echo "$name did not run and pass"; exit 1; } - fi - - name: Unit, conformance and cross-process recovery tests - run: | - JPACK_TEST_BIN="$RUNNER_TEMP/jpack" \ - JPACK_EARLIER_TEST_BIN="$RUNNER_TEMP/jpack-earlier" \ - JPACK_GATEWAY_TEST_BIN="$RUNNER_TEMP/gateway" \ - JPACK_MCP_ADAPTER_TEST_BIN="$RUNNER_TEMP/adapter-mcp" \ - JPACK_OLD_RUNNER_TEST_BIN="$RUNNER_TEMP/jpack-runner-old" \ - go test -race ./... - # A skipped test passes `go test`; these must run (#35, #54, #55). - - name: Chain, signature, binding and ownership tests run under the pinned Runtimes - run: | - out="$(JPACK_TEST_BIN="$RUNNER_TEMP/jpack" \ - JPACK_EARLIER_TEST_BIN="$RUNNER_TEMP/jpack-earlier" \ - JPACK_GATEWAY_TEST_BIN="$RUNNER_TEMP/gateway" \ - JPACK_MCP_ADAPTER_TEST_BIN="$RUNNER_TEMP/adapter-mcp" \ - go test -count=1 -v \ - -run '^(TestTheRuntimesVerifierReadsTheChainOfRuns|TestASignedRunKeepsItsSignature|TestVerifyRunChecksARunTheRuntimeSigned|TestASeedReplacedAfterStartSignsNothing|TestAnEarlierRuntimeSigningWithAReplacedSeedIsNamed|TestAnOperationalEvaluationBindsItsPreparation|TestABindingTheRuntimeRefusesFailsTheRun|TestARecordWithoutTheBindingPassedFailsTheRun|TestARuntimeWithoutTheBindingIsGivenNone|TestVerifyRunHoldsTheRecordsBindingToThePreparation|TestVerifyRunReportsARecordTheRuntimeDidNotBind|TestARuntimeThatDoesNotSayItsReleaseEvaluatesNothing|TestNamedDialectRealRuntimeReleaseRunOfflineAndRestart|TestARunWhoseReceiptSessionIsNoTokenIsEvaluatedExportedAndVerified|TestExecutorOwnedServiceRealRuntimeJobsAndChecks|TestExecutorOwnedRealRuntimeSurvivesOwnerDeath)$' \ - ./internal/runner ./cmd/jpack-runner)" || { printf '%s\n' "$out"; exit 1; } - printf '%s\n' "$out" - for name in TestTheRuntimesVerifierReadsTheChainOfRuns TestASignedRunKeepsItsSignature TestVerifyRunChecksARunTheRuntimeSigned TestASeedReplacedAfterStartSignsNothing TestAnEarlierRuntimeSigningWithAReplacedSeedIsNamed TestAnOperationalEvaluationBindsItsPreparation TestABindingTheRuntimeRefusesFailsTheRun TestARecordWithoutTheBindingPassedFailsTheRun TestARuntimeWithoutTheBindingIsGivenNone TestVerifyRunHoldsTheRecordsBindingToThePreparation TestVerifyRunReportsARecordTheRuntimeDidNotBind TestARuntimeThatDoesNotSayItsReleaseEvaluatesNothing TestNamedDialectRealRuntimeReleaseRunOfflineAndRestart TestNamedDialectRealRuntimeReleaseRunOfflineAndRestart/2 TestNamedDialectRealRuntimeReleaseRunOfflineAndRestart/3 TestARunWhoseReceiptSessionIsNoTokenIsEvaluatedExportedAndVerified TestExecutorOwnedServiceRealRuntimeJobsAndChecks; do - printf '%s\n' "$out" | grep -q -- "^[[:space:]]*--- PASS: $name " || { echo "$name did not run and pass"; exit 1; } - done - # The exact real-Runtime lifetime observer uses Linux /proc and an - # isolated subreaper. The cross-platform Service test above is required - # on both CI hosts; Linux must also prove both owner-stop boundaries. - if [ "$RUNNER_OS" = Linux ]; then - for name in TestExecutorOwnedRealRuntimeSurvivesOwnerDeath TestExecutorOwnedRealRuntimeSurvivesOwnerDeath/close TestExecutorOwnedRealRuntimeSurvivesOwnerDeath/kill; do - printf '%s\n' "$out" | grep -q -- "^[[:space:]]*--- PASS: $name " || { echo "$name did not run and pass"; exit 1; } - done - fi - - name: Check API document - run: python3 -m json.tool openapi.json > /dev/null - - name: Build Runner - run: | - go build -trimpath -o "$RUNNER_TEMP/jpack-runner" ./cmd/jpack-runner - go build -trimpath -o "$RUNNER_TEMP/jpack-source-worker" ./cmd/jpack-source-worker + # The pinned Runtimes, the ownership-era Runner and the Gateway are checked out and built by + # the build-pinned step (scripts/checks/build-pinned.sh). + - name: Checks + run: scripts/dev/check.sh --all --skip release-toolchain,third-party-notices,release-archives,release-programs # What a repository release carries (docs/releasing.md), checked on every # commit so a tag is never the first to find a gap. The toolchain is the exact @@ -199,21 +52,5 @@ jobs: with: go-version: '1.26.5' cache: true - - name: Check the toolchain is the one the release workflow names - run: | - named="$(sed -n "s/^ *go-version: *['\"]\{0,1\}\([0-9][0-9.]*\)['\"]\{0,1\} *\$/\1/p" .github/workflows/release.yml)" - [ -n "$named" ] && [ "go$named" = "$(go env GOVERSION)" ] || { echo "release.yml names Go ${named:-nothing}; this job runs $(go env GOVERSION)"; exit 1; } - - name: Check third-party notices against linked modules - run: python3 tools/third_party_notices.py --check - # What the release workflow checks of its archives, run against sound - # archives and against archives spoiled one way at a time: real programs, - # built by this toolchain and archived. - - name: Check the archive check refuses each fault it names - run: python3 tools/test_release_archives.py - # Names in a file compared with directories: an early warning only. The - # release workflow checks the archives themselves. - - name: Check every program is named in the release build - run: | - want="$(ls cmd | sort)" - have="$(sed -n 's/^ binary: //p' .goreleaser.yml | sort)" - [ "$want" = "$have" ] || { echo "cmd and .goreleaser.yml name different programs:"; diff <(echo "$want") <(echo "$have"); exit 1; } + - name: The release steps of dev.json + run: scripts/dev/check.sh --all --only release-toolchain,third-party-notices,release-archives,release-programs diff --git a/.gitignore b/.gitignore index 869949f..682a8d0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ *.sqlite* /dist/ __pycache__/ + +# The pinned Runtimes, Gateway and old Runner the tests run against (scripts/checks/build-pinned.sh). +/.ci/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..dfb1314 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,45 @@ +# Working in this repository + +This file is read by Codex as `AGENTS.md` and by Claude as `CLAUDE.md`; the two are the same text. +Edit `AGENTS.md` and run `scripts/sync-agent-docs.sh` to regenerate `CLAUDE.md`. + +## Before you open a pull request + +- Run `scripts/dev/check.sh` (or `jpdev check` where the kit is installed) once. It runs the + checks listed in `dev.json` in order and prints one summary block. +- Paste that summary block in the pull request body, under a "Checks" heading. +- If a check fails: read its first lines in the block, fix the cause, re-run that one check with + `--only NAME`, then run the whole list once more. Do not loop on the whole list. +- Start no CI watch. Push, report once, and stop; CI owns everything deterministic. + +## Rules + +- Write test vectors and fixtures by hand. Never record them "from a run" of the code under test. +- Keep machine-local paths and personal names out of tracked files, commit messages and pull + request bodies. Refer to people by role or handle where a name is needed. +- Sign off every commit (`git commit -s`). Never force-push; never rewrite published history. +- End a process you started by its process id. Never use a pattern kill (`pkill`, `killall`). +- One pull request per issue or batch. State what changed and why in plain, factual wording. +- A change to a public surface (a schema, a contract, a command's output) says so in the pull + request and changes its vectors and documentation in the same pull request. +- If a safeguard stops you (a permission prompt, a refused command, a failed gate), stop and say + so in your report. Do not work around it. + +## Reports + +Your final report is short: the pull request and its head commit, what changed, the check +summary block, and anything you stopped on or chose not to do. + +## This repository + +- The checks are `dev.json`, run by `scripts/dev/check.sh`; CI runs the same list in the + `Runner (ubuntu-latest)` and `Runner (macos-latest)` jobs, and the four release steps again in + `Release contents` with the exact Go toolchain `release.yml` names. Branch protection requires + those job names. The longer steps are scripts under `scripts/checks/`. +- The real-Runtime tests need the binaries `scripts/checks/build-pinned.sh` builds into `.ci/bin` + from pinned Runtime, Gateway and old-Runner commits (it needs network access to fetch them). A + pin and the release name beside it are moved together. The two test steps fail if a named test + does not run and pass, so a skip is a failure there. +- Runner is a single-owner local Jobs pilot; do not add shared-user permissions, external actions + or agent loops. Do not put real credentials, customer evidence or private operational records in + examples or fixtures. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..dfb1314 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,45 @@ +# Working in this repository + +This file is read by Codex as `AGENTS.md` and by Claude as `CLAUDE.md`; the two are the same text. +Edit `AGENTS.md` and run `scripts/sync-agent-docs.sh` to regenerate `CLAUDE.md`. + +## Before you open a pull request + +- Run `scripts/dev/check.sh` (or `jpdev check` where the kit is installed) once. It runs the + checks listed in `dev.json` in order and prints one summary block. +- Paste that summary block in the pull request body, under a "Checks" heading. +- If a check fails: read its first lines in the block, fix the cause, re-run that one check with + `--only NAME`, then run the whole list once more. Do not loop on the whole list. +- Start no CI watch. Push, report once, and stop; CI owns everything deterministic. + +## Rules + +- Write test vectors and fixtures by hand. Never record them "from a run" of the code under test. +- Keep machine-local paths and personal names out of tracked files, commit messages and pull + request bodies. Refer to people by role or handle where a name is needed. +- Sign off every commit (`git commit -s`). Never force-push; never rewrite published history. +- End a process you started by its process id. Never use a pattern kill (`pkill`, `killall`). +- One pull request per issue or batch. State what changed and why in plain, factual wording. +- A change to a public surface (a schema, a contract, a command's output) says so in the pull + request and changes its vectors and documentation in the same pull request. +- If a safeguard stops you (a permission prompt, a refused command, a failed gate), stop and say + so in your report. Do not work around it. + +## Reports + +Your final report is short: the pull request and its head commit, what changed, the check +summary block, and anything you stopped on or chose not to do. + +## This repository + +- The checks are `dev.json`, run by `scripts/dev/check.sh`; CI runs the same list in the + `Runner (ubuntu-latest)` and `Runner (macos-latest)` jobs, and the four release steps again in + `Release contents` with the exact Go toolchain `release.yml` names. Branch protection requires + those job names. The longer steps are scripts under `scripts/checks/`. +- The real-Runtime tests need the binaries `scripts/checks/build-pinned.sh` builds into `.ci/bin` + from pinned Runtime, Gateway and old-Runner commits (it needs network access to fetch them). A + pin and the release name beside it are moved together. The two test steps fail if a named test + does not run and pass, so a skip is a failure there. +- Runner is a single-owner local Jobs pilot; do not add shared-user permissions, external actions + or agent loops. Do not put real credentials, customer evidence or private operational records in + examples or fixtures. diff --git a/README.md b/README.md index 5ca9af4..3647119 100644 --- a/README.md +++ b/README.md @@ -511,6 +511,15 @@ release it reports, so a Runtime built from source must name its release, as CI' `go build -ldflags "-X github.com/Judgment-Pack/judgment-pack-runtime/internal/result.CLIVersion="`. The fixture is a synthetic Apache-2.0 Runtime specification example, not business policy. +`scripts/dev/check.sh` runs every check CI runs, in order, and prints one summary block of passes, +failures and skips; `--only NAME` re-runs one check. The checks are listed in +[`dev.json`](dev.json), the manifest of the Judgment Pack development kit (v0.1.0), whose runner is +vendored here unchanged; the longer ones are scripts under `scripts/checks/`. The real-Runtime +steps build the pinned Runtimes and Gateway into `.ci/` first and need network access for that. +`AGENTS.md` and `CLAUDE.md` carry the same instructions for agent sessions, shared rules first and +this repository's own after them; CI checks that the two are identical (`scripts/sync-agent-docs.sh` +writes `CLAUDE.md` from `AGENTS.md`). + ## License and contributions This repository is public and licensed under [Apache-2.0](LICENSE). Report bugs or diff --git a/dev.json b/dev.json new file mode 100644 index 0000000..1abc529 --- /dev/null +++ b/dev.json @@ -0,0 +1,21 @@ +{ + "version": 1, + "toolchains": {"go": {}, "python": {}}, + "checks": [ + {"name": "local-paths", "run": "scripts/checks/local-paths.sh", "toolchain": "shell"}, + {"name": "agent-docs", "run": "scripts/sync-agent-docs.sh --check .", "toolchain": "shell", "summary": "^sync-agent-docs:"}, + {"name": "diff-check", "run": "git diff --check \"$(git hash-object -t tree /dev/null)\"", "toolchain": "shell"}, + {"name": "go-fmt", "run": "unformatted=$(gofmt -l $(go list -f '{{.Dir}}' ./...)); if [ -n \"$unformatted\" ]; then echo 'not gofmt-formatted:'; echo \"$unformatted\"; exit 1; fi", "toolchain": "go", "timeout": 600}, + {"name": "go-vet", "run": "go vet ./...", "toolchain": "go", "timeout": 3000}, + {"name": "build-pinned", "run": "scripts/checks/build-pinned.sh", "toolchain": "go", "timeout": 3000}, + {"name": "manual-gate", "run": "scripts/checks/manual-gate.sh", "toolchain": "go", "summary": "^(ok|FAIL|---) ", "timeout": 3000}, + {"name": "unit-tests", "run": "scripts/checks/unit-tests.sh", "toolchain": "go", "summary": "^(ok|FAIL|---) ", "timeout": 3000}, + {"name": "chain-tests", "run": "scripts/checks/chain-tests.sh", "toolchain": "go", "summary": "^(ok|FAIL) ", "timeout": 3000}, + {"name": "openapi", "run": "python3 -m json.tool openapi.json > /dev/null", "toolchain": "python"}, + {"name": "build", "run": "go build -trimpath -o /dev/null ./cmd/jpack-runner && go build -trimpath -o /dev/null ./cmd/jpack-source-worker", "toolchain": "go", "timeout": 3000}, + {"name": "release-toolchain", "run": "scripts/checks/release-toolchain.sh", "toolchain": "go", "timeout": 600}, + {"name": "third-party-notices", "run": "python3 tools/third_party_notices.py --check", "toolchain": "go", "timeout": 3000}, + {"name": "release-archives", "run": "python3 tools/test_release_archives.py", "toolchain": "go", "summary": "^(Ran |OK|FAILED)", "timeout": 3000}, + {"name": "release-programs", "run": "scripts/checks/release-programs.sh", "toolchain": "shell"} + ] +} diff --git a/scripts/checks/build-pinned.sh b/scripts/checks/build-pinned.sh new file mode 100755 index 0000000..d661640 --- /dev/null +++ b/scripts/checks/build-pinned.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# CI steps: check out and build the pinned Runtimes, the ownership-era Runner and the durable +# Gateway contract into .ci/ (ignored by git); the binaries land in .ci/bin. A checkout already at +# its pin is reused. Run from the repository root. +set -e +root=$(git rev-parse --show-toplevel) +bin=$root/.ci/bin +mkdir -p "$bin" + +# fetch_pin REPOSITORY COMMIT DIR: DIR holds REPOSITORY at COMMIT. +fetch_pin() { + [ "$(git -C "$3" rev-parse HEAD 2>/dev/null)" = "$2" ] && return 0 + rm -rf "$3"; mkdir -p "$3" + git -C "$3" init -q + git -C "$3" fetch -q --depth 1 "https://github.com/Judgment-Pack/$1.git" "$2" + git -C "$3" checkout -q FETCH_HEAD +} + +# Real-Runtime tests must run in CI, not silently skip behind an unset +# JPACK_TEST_BIN. Pin the independently versioned evaluator contract. +# A commit is pinned and not a tag, since a commit cannot come to name +# other code. The name after it is the release the commit was taken +# from when the pin was set: whoever moves one moves the other. +# v0.26.0 is the first Runtime release that chains and signs its trail, +# so TestTheRuntimesVerifierReadsTheChainOfRuns, +# TestASignedRunKeepsItsSignature and +# TestVerifyRunChecksARunTheRuntimeSigned run here and do not skip (#35). +# v0.30.0 is the first that refuses to sign with a seed whose public +# key is not JPACK_SIGNING_PUBLIC_KEY, which +# TestASeedReplacedAfterStartSignsNothing needs (#54). v0.31.0 is the +# first that takes a caller's binding (`--binding`, runtime ADR-0050), +# which the binding tests need (#55). A pin moved to a Runtime without +# `jpack audit` or `--binding` would skip them again, and the step after +# the tests fails if any of them does not pass. +# The tested Runtime: merged citation session identifier of any string (runtime #291). +fetch_pin judgment-pack-runtime 09206e743cdf410d7b5ee970f1c9d09ca7c015ca "$root/.ci/runtime" +# The build names the pin's release, as the release build does, so a +# test can tell what the Runtime under test answers by its release +# (TestTheRuntimesVerifierReadsTheChainOfRuns). Whoever moves the pin +# moves the version with it. +(cd "$root/.ci/runtime" && go build -trimpath -ldflags "-X github.com/Judgment-Pack/judgment-pack-runtime/internal/result.CLIVersion=0.33.0-dev.09206e7" -o "$bin/jpack" ./cmd/jpack) + +# Test-only old companion: its current-store refusal is part of the +# explicit manual-store preparation contract, not a release dependency. +fetch_pin judgment-pack-runner 1cc6b29885b344716a970cd21fefeaca0a4edaeb "$root/.ci/runner-old" +(cd "$root/.ci/runner-old" && go build -buildvcs=false -trimpath -o "$bin/jpack-runner-old" ./cmd/jpack-runner) + +# A Runtime that signs and does not know the expected key, as every +# release from v0.26.0 to v0.29.x: Runner must name the key a replaced +# seed signed under (TestAnEarlierRuntimeSigningWithAReplacedSeedIsNamed, +# #54). It takes no binding either, as every release before v0.31.0: +# Runner must give it none, and verify-run report its record unbound +# (TestARuntimeWithoutTheBindingIsGivenNone, +# TestVerifyRunReportsARecordTheRuntimeDidNotBind, #55). Pinned by +# commit as the tested Runtime is. The commit is v0.27.1. +fetch_pin judgment-pack-runtime 7a2fcfe20cedba5f164246fc544c4920971d2c56 "$root/.ci/runtime-earlier" +(cd "$root/.ci/runtime-earlier" && go build -trimpath -ldflags "-X github.com/Judgment-Pack/judgment-pack-runtime/internal/result.CLIVersion=0.27.1" -o "$bin/jpack-earlier" ./cmd/jpack) + +# The durable Gateway contract. The commit is v0.9.1. +fetch_pin judgment-pack-gateway 5b49e1e094b55fdd1deb9f613815c2beebd411f1 "$root/.ci/gateway" +(cd "$root/.ci/gateway/go" && go build -trimpath -o "$bin/gateway" .) +(cd "$root/.ci/gateway/adapters" && go build -trimpath -o "$bin/adapter-mcp" ./cmd/adapter-mcp) diff --git a/scripts/checks/chain-tests.sh b/scripts/checks/chain-tests.sh new file mode 100755 index 0000000..859697e --- /dev/null +++ b/scripts/checks/chain-tests.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# CI step: Chain, signature, binding and ownership tests run under the pinned Runtimes +# A skipped test passes `go test`; these must run (#35, #54, #55). +set -e +root=$(git rev-parse --show-toplevel) +bin=$root/.ci/bin +out="$(JPACK_TEST_BIN="$bin/jpack" \ + JPACK_EARLIER_TEST_BIN="$bin/jpack-earlier" \ + JPACK_GATEWAY_TEST_BIN="$bin/gateway" \ + JPACK_MCP_ADAPTER_TEST_BIN="$bin/adapter-mcp" \ + go test -count=1 -v \ + -run '^(TestTheRuntimesVerifierReadsTheChainOfRuns|TestASignedRunKeepsItsSignature|TestVerifyRunChecksARunTheRuntimeSigned|TestASeedReplacedAfterStartSignsNothing|TestAnEarlierRuntimeSigningWithAReplacedSeedIsNamed|TestAnOperationalEvaluationBindsItsPreparation|TestABindingTheRuntimeRefusesFailsTheRun|TestARecordWithoutTheBindingPassedFailsTheRun|TestARuntimeWithoutTheBindingIsGivenNone|TestVerifyRunHoldsTheRecordsBindingToThePreparation|TestVerifyRunReportsARecordTheRuntimeDidNotBind|TestARuntimeThatDoesNotSayItsReleaseEvaluatesNothing|TestNamedDialectRealRuntimeReleaseRunOfflineAndRestart|TestARunWhoseReceiptSessionIsNoTokenIsEvaluatedExportedAndVerified|TestExecutorOwnedServiceRealRuntimeJobsAndChecks|TestExecutorOwnedRealRuntimeSurvivesOwnerDeath)$' \ + ./internal/runner ./cmd/jpack-runner)" || { printf '%s\n' "$out"; exit 1; } +printf '%s\n' "$out" +for name in TestTheRuntimesVerifierReadsTheChainOfRuns TestASignedRunKeepsItsSignature TestVerifyRunChecksARunTheRuntimeSigned TestASeedReplacedAfterStartSignsNothing TestAnEarlierRuntimeSigningWithAReplacedSeedIsNamed TestAnOperationalEvaluationBindsItsPreparation TestABindingTheRuntimeRefusesFailsTheRun TestARecordWithoutTheBindingPassedFailsTheRun TestARuntimeWithoutTheBindingIsGivenNone TestVerifyRunHoldsTheRecordsBindingToThePreparation TestVerifyRunReportsARecordTheRuntimeDidNotBind TestARuntimeThatDoesNotSayItsReleaseEvaluatesNothing TestNamedDialectRealRuntimeReleaseRunOfflineAndRestart TestNamedDialectRealRuntimeReleaseRunOfflineAndRestart/2 TestNamedDialectRealRuntimeReleaseRunOfflineAndRestart/3 TestARunWhoseReceiptSessionIsNoTokenIsEvaluatedExportedAndVerified TestExecutorOwnedServiceRealRuntimeJobsAndChecks; do + printf '%s\n' "$out" | grep -q -- "^[[:space:]]*--- PASS: $name " || { echo "$name did not run and pass"; exit 1; } +done +# The exact real-Runtime lifetime observer uses Linux /proc and an +# isolated subreaper. The cross-platform Service test above is required +# on both CI hosts; Linux must also prove both owner-stop boundaries. +if [ "$(uname -s)" = Linux ]; then + for name in TestExecutorOwnedRealRuntimeSurvivesOwnerDeath TestExecutorOwnedRealRuntimeSurvivesOwnerDeath/close TestExecutorOwnedRealRuntimeSurvivesOwnerDeath/kill; do + printf '%s\n' "$out" | grep -q -- "^[[:space:]]*--- PASS: $name " || { echo "$name did not run and pass"; exit 1; } + done +fi diff --git a/scripts/checks/local-paths.sh b/scripts/checks/local-paths.sh new file mode 100755 index 0000000..ab4e7b1 --- /dev/null +++ b/scripts/checks/local-paths.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# No machine-local path in tracked text: a /home/ or /tmp/ path of someone's machine. +# Not counted: the files below, where a /tmp path is a synthetic example, test input or a command +# written in a design note before this check existed. A new file is checked. +set -e +if git grep -nE '(^|[^A-Za-z0-9_])/(home|tmp)/[A-Za-z0-9_.-]+' -- . ':!*.png' ':!*.jpg' \ + ':!docs/design/executor-ownership.md' \ + ':!docs/design/manual-admission-store-prepare-shape.md' \ + ':!docs/design/manual-admission-store-prepare-wire.json' \ + ':!internal/runner/cloud_test.go'; then + echo 'a machine-local path is in tracked text'; exit 1 +fi diff --git a/scripts/checks/manual-gate.sh b/scripts/checks/manual-gate.sh new file mode 100755 index 0000000..f89868c --- /dev/null +++ b/scripts/checks/manual-gate.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# CI step: Manual admission helper and real Runtime gate tests cannot skip +set -e +root=$(git rev-parse --show-toplevel) +bin=$root/.ci/bin +out="$(JPACK_TEST_BIN="$bin/jpack" \ + JPACK_OLD_RUNNER_TEST_BIN="$bin/jpack-runner-old" \ + go test -count=1 -v \ + -run '^(TestManualGovernedRealRuntimeGate|TestManualGovernedActualRuntimeHasNoAuthorityFD|TestManualStorePrepareActualChildRetainsOriginalCustody|TestManualStorePreparationDurableAndNoDowngrade|TestManualStorePreparationSubstitutedStateRefuses|TestManualStoreSQLiteTargetUsesHeldDirectory|TestManualStoreLibraryNeverChangesCWD|TestManualInheritedAuthoritySocketClosesBeforeRuntimeExec|TestManualAuthorityDescriptorObserverDetectsSocket|TestManualStorePreparationPendingAndUnsupported|TestManualExternalDemandCannotBeOmitted|TestManualQueuedRunListsKeepPrivatePreimages)$' \ + ./internal/runner)" || { printf '%s\n' "$out"; exit 1; } +printf '%s\n' "$out" +for name in \ + TestManualGovernedRealRuntimeGate \ + TestManualGovernedRealRuntimeGate/success \ + TestManualGovernedRealRuntimeGate/blocked \ + TestManualGovernedRealRuntimeGate/lost-reply \ + TestManualGovernedRealRuntimeGate/changed-facts \ + TestManualGovernedRealRuntimeGate/changed-config \ + TestManualGovernedRealRuntimeGate/changed-lock \ + TestManualGovernedRealRuntimeGate/changed-signing \ + TestManualGovernedRealRuntimeGate/changed-epoch \ + TestManualGovernedRealRuntimeGate/changed-demand \ + TestManualGovernedRealRuntimeGate/journal-failure \ + TestManualGovernedRealRuntimeGate/start-journal-failure \ + TestManualGovernedRealRuntimeGate/recovered-reserve \ + TestManualStorePrepareActualChildRetainsOriginalCustody \ + TestManualStorePrepareActualChildRetainsOriginalCustody/committed \ + TestManualStorePrepareActualChildRetainsOriginalCustody/refused \ + TestManualStorePrepareActualChildRetainsOriginalCustody/lose-reply \ + TestManualStorePreparationDurableAndNoDowngrade \ + TestManualStorePreparationSubstitutedStateRefuses \ + TestManualStorePreparationSubstitutedStateRefuses/before-adapter \ + TestManualStorePreparationSubstitutedStateRefuses/after-adapter \ + TestManualStoreSQLiteTargetUsesHeldDirectory \ + TestManualStoreLibraryNeverChangesCWD \ + TestManualInheritedAuthoritySocketClosesBeforeRuntimeExec \ + TestManualAuthorityDescriptorObserverDetectsSocket \ + TestManualStorePreparationPendingAndUnsupported \ + TestManualExternalDemandCannotBeOmitted \ + TestManualQueuedRunListsKeepPrivatePreimages; do + printf '%s\n' "$out" | grep -q -- "^[[:space:]]*--- PASS: $name " || { echo "$name did not run and pass"; exit 1; } +done +# This observer identifies the actual Runtime through Linux /proc. +# Every other named helper/gate test is mandatory on both hosts. +if [ "$(uname -s)" = Linux ]; then + name=TestManualGovernedActualRuntimeHasNoAuthorityFD + printf '%s\n' "$out" | grep -q -- "^[[:space:]]*--- PASS: $name " || { echo "$name did not run and pass"; exit 1; } +fi diff --git a/scripts/checks/release-programs.sh b/scripts/checks/release-programs.sh new file mode 100755 index 0000000..b15f9a6 --- /dev/null +++ b/scripts/checks/release-programs.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# CI step: Check every program is named in the release build +# Names in a file compared with directories: an early warning only. The +# release workflow checks the archives themselves. +set -e +want="$(ls cmd | sort)" +have="$(sed -n 's/^ binary: //p' .goreleaser.yml | sort)" +[ "$want" = "$have" ] || { echo "cmd and .goreleaser.yml name different programs:"; diff <(echo "$want") <(echo "$have"); exit 1; } diff --git a/scripts/checks/release-toolchain.sh b/scripts/checks/release-toolchain.sh new file mode 100755 index 0000000..a7efa72 --- /dev/null +++ b/scripts/checks/release-toolchain.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +# CI step: Check the toolchain is the one the release workflow names +set -e +named="$(sed -n "s/^ *go-version: *['\"]\{0,1\}\([0-9][0-9.]*\)['\"]\{0,1\} *\$/\1/p" .github/workflows/release.yml)" +[ -n "$named" ] && [ "go$named" = "$(go env GOVERSION)" ] || { echo "release.yml names Go ${named:-nothing}; this job runs $(go env GOVERSION)"; exit 1; } diff --git a/scripts/checks/unit-tests.sh b/scripts/checks/unit-tests.sh new file mode 100755 index 0000000..3045c5d --- /dev/null +++ b/scripts/checks/unit-tests.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# CI step: Unit, conformance and cross-process recovery tests +set -e +root=$(git rev-parse --show-toplevel) +bin=$root/.ci/bin +JPACK_TEST_BIN="$bin/jpack" \ +JPACK_EARLIER_TEST_BIN="$bin/jpack-earlier" \ +JPACK_GATEWAY_TEST_BIN="$bin/gateway" \ +JPACK_MCP_ADAPTER_TEST_BIN="$bin/adapter-mcp" \ +JPACK_OLD_RUNNER_TEST_BIN="$bin/jpack-runner-old" \ +go test -race ./... diff --git a/scripts/dev/check.sh b/scripts/dev/check.sh new file mode 100755 index 0000000..762a3dc --- /dev/null +++ b/scripts/dev/check.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# scripts/dev/check.sh [--setup] [--only a,b] [--skip a,b] [--all] [--manifest FILE] +# Runs the checks of dev.json in order and prints one summary block; exit status = failures. +# Stops at the first failure unless --all. --setup runs the manifest's setup steps instead. +# A step whose requires_env variable is empty is skipped, and said to be; when CI is set that is a +# failure unless the step is marked optional: true. Standard library python3 only. jpdev sets JPDEV_WRAP to add its caps around each step. +root=${JPDEV_ROOT:-$(cd "$(dirname "$0")" && git rev-parse --show-toplevel)} || exit 2 +exec python3 -I - "$root" "$@" <<'PY' +import json, os, re, signal, subprocess, sys, time +root, args = sys.argv[1], sys.argv[2:] +opt = {'only': set(), 'skip': set(), 'all': False, 'setup': False, 'manifest': root + '/dev.json'} +while args: + a = args.pop(0) + if a in ('--only', '--skip'): opt[a[2:]] = set(args.pop(0).split(',')) + elif a == '--manifest': opt['manifest'] = args.pop(0) + elif a in ('--all', '--setup'): opt[a[2:]] = True + else: sys.exit('check.sh: unknown argument ' + a) +m = json.load(open(opt['manifest'], encoding='utf-8')) +steps = m.get('setup', []) if opt['setup'] else m['checks'] + ([dict(name='parity', **m['parity'])] if 'parity' in m else []) +unknown = (opt['only'] | opt['skip']) - set(s['name'] for s in steps) +if unknown: print('check.sh: no step named ' + ', '.join(sorted(unknown)), file=sys.stderr); sys.exit(2) +wrap, logdir, rows, failed = os.environ.get('JPDEV_WRAP'), os.environ.get('JPDEV_LOGDIR'), [], 0 +for c in steps: + n = c['name'] + if (opt['only'] and n not in opt['only']) or n in opt['skip']: continue + if failed and not opt['all']: rows.append((n, 'NOT RUN', '', [])); continue + if c.get('requires_env') and not os.environ.get(c['requires_env']): + # A skip is a failure under CI (CI is set) unless the step says optional: true. + if os.environ.get('CI') and not c.get('optional'): failed += 1; rows.append((n, 'FAIL', 'needs %s, which CI must set (or mark the step optional)' % c['requires_env'], [])) + else: rows.append((n, 'SKIP', 'needs ' + c['requires_env'], [])) + continue + cmd = ['bash', '-c', c['run']] + if wrap: cmd = [wrap, c.get('toolchain', 'shell'), '--'] + cmd + tmo = c.get('timeout', 1200) + t0 = time.time(); print('check: running ' + n, file=sys.stderr, flush=True) + p = subprocess.Popen(cmd, cwd=os.path.join(root, c.get('cwd', '.')), stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, text=True, errors='replace', start_new_session=True, env=dict(os.environ, JPDEV_STEP_TIMEOUT=str(tmo))) + try: out, _ = p.communicate(timeout=None if wrap else tmo) + except subprocess.TimeoutExpired: os.killpg(p.pid, signal.SIGKILL); out = p.communicate()[0] + '\ncheck: stopped for time\n' + lines, took = out.splitlines(), '%ds' % (time.time() - t0) + if logdir: os.makedirs(logdir, exist_ok=True); open('%s/%s.log' % (logdir, n), 'w').write(out) + hit = [l.strip() for l in lines if c.get('summary') and re.search(c['summary'], l)] + note = [' ' + l for l in (hit if len(hit) <= 3 else ['%d lines matched; last: %s' % (len(hit), hit[-1])])] + if p.returncode == 0: rows.append((n, 'PASS', took, note)); continue + failed += 1 + rows.append((n, 'FAIL', '%s, exit %d' % (took, p.returncode), note + [' --'] + [' ' + l for l in lines[:40]] + ([' ... %d more lines' % (len(lines) - 40)] if len(lines) > 40 else []))) +print('== %s summary (%s) ==' % ('setup' if opt['setup'] else 'check', os.path.basename(opt['manifest']))) +for n, s, head, extra in rows: print('\n'.join(['%-8s %-24s %s' % (s, n, head)] + extra)) +print('== %d passed, %d failed, %d skipped, %d not run ==' % tuple(sum(1 for r in rows if r[1] == k) for k in ('PASS', 'FAIL', 'SKIP', 'NOT RUN'))) +sys.exit(min(failed, 255)) +PY diff --git a/scripts/sync-agent-docs.sh b/scripts/sync-agent-docs.sh new file mode 100755 index 0000000..3fdf82d --- /dev/null +++ b/scripts/sync-agent-docs.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# sync-agent-docs.sh [--check] [DIR] +# CLAUDE.md is generated from AGENTS.md (the source) in DIR (default templates/ of the kit; +# a repository that vendors the pair passes its root). --check writes nothing and fails when +# CLAUDE.md is not exactly what the generator would write. +set -eu +check=0 +if [ "${1:-}" = --check ]; then check=1; shift; fi +dir=${1:-$(cd "$(dirname "$0")/.." && pwd)/templates} +src=$dir/AGENTS.md +dst=$dir/CLAUDE.md +[ -f "$src" ] || { echo "sync-agent-docs: $src is missing" >&2; exit 2; } +if [ "$check" -eq 1 ]; then + if [ -f "$dst" ] && cmp -s "$src" "$dst"; then + echo "sync-agent-docs: CLAUDE.md matches AGENTS.md" + else + echo "sync-agent-docs: CLAUDE.md differs from AGENTS.md; run scripts/sync-agent-docs.sh" >&2 + exit 1 + fi +else + cp "$src" "$dst" + echo "sync-agent-docs: wrote $dst" +fi