Harness Intelligence Wiki
Runbooks

CI Verification And Publication

CI Verification And Publication

Current behavior verification

.github/workflows/behavior-contract.yml runs for trusted same-repository pull requests and every push to main. Root Static Verification is independently visible and runs the canonical entrypoint:

bun scripts/behavior-contract/run-ci-verification.mjs static

Affected Verification validates each changed non-wiki workspace's saved managed Oxlint route and toolchain, then runs Oxlint and Oxfmt on changed files before the affected build, type-check, test, and browser graph. Errors fail; warnings on added lines must stay within that route's saved maxWarnings limit. Warnings on unchanged lines remain baseline debt. Files explicitly excluded by their workspace tool are reported as excluded. Other tool failures remain fatal. This keeps the workspace policy active without Turbo's ^lint or ^check expansion across unrelated legacy debt. Root Static Verification checks the root code scope and managed route. The entrypoint requires TURBO_SCM_BASE and TURBO_SCM_HEAD; pull requests use the prospective merge base SHA, while pushes use the before/head range. Stable Aggregate Check requires both verification jobs to pass. Candidate Evidence remains PR-only and bound to the prospective pull-request tree.

TURBO_SCM_BASE=<base-sha> TURBO_SCM_HEAD=<head-sha> \
  bun scripts/behavior-contract/run-ci-verification.mjs affected

test:ci remains the existing full-tree local verification command. It is not identical to the hosted static and affected entrypoints:

bun run test:ci

The private GitHub repository is currently on the Free plan, which cannot enforce required status checks; branch-protection and ruleset APIs return HTTP 403. Until provider capability changes, merging must treat Stable Aggregate Check as a procedural gate. A successful post-merge main run is defense-in-depth evidence, not retroactive merge protection.

Capability-owned verification

Current verification and signed-cache witness workers use blacksmith-2vcpu-ubuntu-2404 with Bun 1.4.0. Protected npm publication retains its GitHub-hosted 2-vCPU Linux runner and OIDC identity. Automated macOS integration, cache restoration and weekly filesystem jobs are removed; current CI establishes Linux proof. Earlier macOS results below are historical evidence.

The verification wrapper expands generic test into cached capability tasks. CLI tasks partition the test files without overlap into test:source, test:release, and test:built. End-to-end lifecycle coverage lives in apps/cli/src/registry/lifecycle.e2e.test.ts, which runs hi init, update, diff, and check against a locally built Registry (HI_REGISTRY_URL=file://...). Only built composition requires the CLI build. Other testable workspaces use test:ci; parked web has no behavioral test. Its build/type checks remain. Direct bun run --cwd apps/cli test runs the complete local suite; the generic Turbo test facade is uncached and is not selected alongside its capability tasks.

Root test:operators selects seven cheap supported operator files. API test:packaged owns the eighth, which performs one genuine private API build/Postgres/auth runtime proof before independent evidence-tamper refusals. Root cache-policy proof executes once under Static Verification. Both Static and Affected Verification use the existing trusted signed cache; Stable Aggregate remains read-only. Test tasks use one Vitest worker and the wrapper defaults Turbo concurrency to two. There are no shards.

Affected planning uses the real Turbo graph before Chromium setup. Chromium is installed only when test:browser is selected, including selection through dependencies. Base host identity excludes Docker and browser identity; only their consumers hash those capabilities. Failed capability observation prevents unsafe reuse for that capability without invalidating unrelated proof.

Inputs follow actual consumption: unrelated documentation and lint configuration preserve product hashes; consumed prompts, shipped skills, dependency source and runtime capabilities invalidate their consumers. Source-test-only changes preserve unrelated builds. CLI data-tree files and synced skills are inputs of the Registry build, not of the npm package build. Source-identical CLI builds retain the inherited producer provenance of the cached artifact; release authority, classification and publication execute fresh without result caching.

The issue 217 portfolio maps every original and added file to retained proof or an explicit cull. The validation record distinguishes local evidence from hosted timing.

Routine proof and on-demand diagnostics

Installer tests run the real hi init, hi update, hi diff, and hi check paths against a locally built Registry (file://) in temporary repositories. Recovery tests run the production planner/validator against minimal real Git history and tiny valid artifacts. PostgreSQL tests reuse one suite-owned server with separately migrated databases; wiki tests use one Vitest process with real synchronization, navigation and route behavior.

Residual pruning removes duplicate journeys and moves declaration/refusal cases to public planning seams while keeping representative materialization and ownership proof. Keep each changed-input class, but execute a shared recovery sequence once when repeating it adds no distinct outcome. Root policy command-membership assertions share one real Turbo plan. Some policy scenarios still require full application execution to observe their effects; planning proof does not replace those witnesses. See the residual pruning analysis and the portfolio for scenario-specific decisions.

Full historical recovery is an explicit diagnostic, excluded from routine selectors:

bun run --cwd apps/cli test:recovery:historical exact
bun run --cwd apps/cli test:recovery:historical resume

Historical replay requires the historical Git objects and toolchains. It was compile-checked, not fully replayed, during its delivery.

Historical: issue 215 updater-cache witness

Before CLI 6.0.0, hi update validated a temporary Validation Candidate and reused Prepared Installation and Validation Result caches through the signed Turbo artifact provider. CLI 6.0.0 retires that updater and its caches. Trusted runs 35169090936 and 35169496396 remain the historical witness.

Historical pre-repair pull-request verification

.github/workflows/behavior-contract.yml runs only for pull requests. It checks out the prospective pull-request tree, uses the base and head SHAs for Turbo affected selection, then runs one command:

bunx turbo run build lint check check-types test test:browser '//#check:repo' --affected

Turbo owns affected selection and cache identity. Package scripts own deterministic build, lint, check, typecheck, test, and browser work. The root //#check:repo task is limited to repository-wide configuration and workflow policy that no workspace can own. Root commands delegate to Turbo; CI does not invoke a repository suite directly.

The workflow cancels an older run for the same pull request. Stable Aggregate Check always runs after Affected Verification; it is the one merge-facing result even when Turbo finds no applicable package task. A failed affected job fails the aggregate. The check alone is never publication authority.

The workflow supports only same-repository pull requests. Its trusted job receives the signed-cache OIDC capability and required cache inputs. External-fork pull requests are unsupported: GitHub skips the trusted job, the always-run aggregate fails because trusted verification did not succeed, and no Candidate Evidence is retained. The workflow runs under pull_request, never pull_request_target, so contributor code cannot enter a privileged verification path.

Changelog-only publication authority

Candidate Evidence JSON v1 remains a pull-request verification receipt with exactly seven fields: schemaVersion, repository, workflowRunId, pullRequest, headCommit, testedTree, and the successful conclusion. Its artifact name is release-candidate-tree-<testedTree>. Publication does not consume it.

The receipt transports verification identity only. It must never contain package identity, build output, cache data, release classification or intent, credentials, or secrets. Freshness is external artifact metadata, not a receipt field: authority accepts evidence only when its GitHub artifact is no more than 14 days old.

After Affected Verification succeeds, Stable Aggregate Check checks out the prospective merge tree, writes only that receipt, and retains release-candidate-tree-<testedTree> for 14 days. It records the pull-request head SHA separately from the prospective merge tree so authority can prove both GitHub run provenance and the exact tested result.

On each main push, the protected workflow checks out github.sha, resolves its exact commit and tree, and retains a schema-v2 immutable publication input containing only that identity and the repository. It performs no Actions API search and downloads no current or historical Candidate Evidence. Production checks the retained input before checkout, then proves the checkout has the same commit and tree.

The release workflow performs no product-test or installed-tarball replay. Direct commits and merge commits use the same changelog selection rule. Two jobs keep historical repository execution separate from provider credentials.

The workflow uses read authentication only to acquire private durable-anchor tags. release-plan has read-only repository authority and no publication credential; its checkout authentication exists only long enough to snapshot the anchor tags. The job then clones a bare local origin, removes every checkout http.*.extraheader, replaces origin with that local path, and proves both conditions before any install or historical execution. Exact detached historical worktrees therefore see only the credentialless local origin.

release-plan walks the exact first-parent range from the durable provider anchor and installs required historical trees with lifecycle scripts disabled. Release selection uses only changed changelog paths:

Changed pathsRelease kind
neither changelognone
BASELINE_CHANGELOG.mdbaseline
CHANGELOG.mdnpm
both changelogsmixed

Artifact semantic and literal differences remain diagnostics. Selected entries build credential-free artifacts; npm inspection and archive creation use npm pack --json --dry-run --ignore-scripts and npm pack --json --ignore-scripts. Temporary pack archives live outside the checkout and are removed after success or failure. The plan freezes reviewed package, Registry build, Git, changelog, artifact, and recovery tag authority. The resulting plan and artifacts are retained for one day.

Production downloads the immutable publication input and credential-free plan, checks repository and final commit/tree before checkout, binds the checkout to that identity, and provisions Bun defensively before apply. The fixed current-tree adapter revalidates the plan's repository, final commit/tree, exact first-parent order, changelog-selected release-kind/product mapping, static Git package identities, changelog entries, archive paths and bytes, manifests, executable bins, required package files, and frozen recovery tag authority. Production publishes the frozen artifacts without dependency installation, historical worktrees, historical builds, or classification replay. npm publication consumes the reviewed archive with --ignore-scripts.

All release-tag checks use an authenticated exact git ls-remote lookup. Only a successful empty response means absence; command failure, malformed output, or a conflicting direct/peeled tag refuses before mutation. npm registry reads similarly treat only explicit E404 as absence. Registry outages, startup failures, malformed success output, an existing version with different integrity, or a conflicting Git tag fail closed before npm publication. GitHub release metadata is created or repaired to the exact reviewed title and notes, then read back. The retained release dispatcher marks beta-channel releases and version tags containing a prerelease suffix as GitHub prereleases.

Registry publication

A changed BASELINE_CHANGELOG.md selects a Registry Baseline. The release plan builds it with bun run --cwd apps/cli registry:build (scripts/build-registry.mjs) from apps/cli/skills and apps/cli/src/data:

  • --version defaults to today's UTC date plus git rev-parse --short=8 HEAD; --cli-range defaults to the Compatibility: line of the BASELINE_CHANGELOG.md Unreleased section (CLI 6: >=6.0.0 <7.0.0); --out defaults to apps/cli/dist/registry.
  • Output: root registry.json (catalog with latest), <version>/registry.json, and one <version>/<item>.json per Registry Item, in canonical bytes. Each catalog entry carries meta.sha256 of the exact item bytes, for installer cache integrity only.
  • The build validates every item against the shadcn registry-item schema and the CLI model, and refuses an existing version directory, duplicate item names, dangling registryDependencies, and duplicate targets.

Production publishes with bun run --cwd apps/cli registry:publish (scripts/publish-registry.mjs), using the BLOB_READ_WRITE_TOKEN secret of the GitHub Production environment:

  1. Upload every <version>/<item>.json to the public Vercel Blob store harness-intelligence-registry (fra1, linked to Vercel project harness-intelligence-api) with allowOverwrite: false and a one-year cache lifetime. An existing identical item is accepted, so an interrupted publish resumes; a different existing item fails (immutable-conflict).
  2. Write <version>/registry.json. It marks the version complete. If it already exists with identical bytes, the items are verified and nothing is re-uploaded; different bytes fail with version-conflict (a correction is a new Baseline).
  3. Write the root registry.json last (cache max-age 60 s). This moves latest; there is no promotion step. latest never moves back: a root that already names a newer version stays. An identical, fully published version with a correct root is a no-op, so a release re-run converges.
  4. Read back every file through the public Registry URL https://api.harness-intelligence.devpunks.com/r (override HI_REGISTRY_URL) and byte-compare it with the build output, polling while the root catalog cache expires.

The API domain serves the Registry through the apps/api/vercel.json rewrite /r/:path* to the Blob store's public /r/ path. No API code, database row, or credential is involved in reading it; publication needs no API deployment. A run whose changed changelog paths exclude BASELINE_CHANGELOG.md publishes no Registry. Publish by hand only from a clean worktree through the two package scripts (rule HI-REPO-002), then verify the readback.

Retired with CLI 6.0.0: baseline/stable/* GitHub release artifacts, control-plane promotion (/api/baselines, DP_BASELINE_PUBLISH_TOKEN), baseline:publish, and baseline:rollback. There is no rollback; publish a corrected Baseline.

CLI 6.0.0 rollout order. Deploy the API first: it adds the /r rewrite and no longer serves the baseline endpoints. Then apply database migration 0007_drop_baseline_authority, which drops the baseline_release and stable_baseline_promotion tables.

For a private GitHub source repository, npm Trusted Publishing uses OIDC without forced Sigstore provenance because npm rejects private-source provenance with E422. Production exchanges one package-scoped npm token only after validating the exact repository, main ref, protected workflow, Production environment, push event, and npm audience claims. The runner masks that short-lived token and supplies it only to the serialized publication child process for npm publish; publish establishes the stable latest alias. Exact public latest readback is the convergence check. No static npm secret or job-level token persists. If a mixed release fails at npm after the Registry Baseline completes, recovery resumes npm; Registry readback prevents a second Baseline publication.

npm trusted-publish partial-state recovery

Protected release run 35197493506 reached an npm partial state: it published immutable @punks/cli@5.1.1, and npm reported latest=5.1.1; the redundant subsequent npm dist-tag add … latest returned HTTP 401. The run therefore stopped before creating the Git tag or GitHub release. This is evidence of published-package and public-alias state only, not evidence that the release recovered or completed.

Do not rerun the failed Production job with its frozen pre-publish plan: that plan repeats the optional alias mutation that already failed. Instead, recover through a fresh full protected-release replan. After archive-presence reconciliation, the recovery path re-reads provider state. When public readback proves both the frozen archive integrity and that latest already belongs to the frozen version or to an approved superseding version, it skips the optional alias mutation and continues normal Git tag and GitHub release convergence. A different integrity, an absent or unsafe latest owner, unreadable provider state, or any ambiguity remains fail-closed; do not create tags or releases manually to bypass it. This specific partial-state recovery remains incomplete until a fresh protected release run proves the full path.

Readback-only historical recovery is conditional on the target's original changelog classification. An npm-only target must declare exactly one npm product, bind notes from CHANGELOG.md to the target package version, and match the npm package name, version, tag, and integrity; it creates no Baseline intent, artifact, or provider read. A baseline-only or mixed target binds BASELINE_CHANGELOG.md notes and the exact published Baseline identity, verified by provider readback. Every form binds the exact target commit and tree, refuses extra or missing products, builds and publishes nothing, and advances durable authority only after exact provider readback. Historical Baselines published before CLI 6.0.0 as GitHub release artifacts are not in the Registry.

Seed the durable publication anchor

Publication requires an existing exact remote lightweight anchor tag. The repository variable alone is not authority. Seed it once at a reviewed commit whose npm and Registry external state is already fully reconciled:

ANCHOR_COMMIT=0123456789abcdef0123456789abcdef01234567
git fetch origin main --tags
git rev-list --first-parent origin/main | awk -v commit="$ANCHOR_COMMIT" '$0 == commit { found=1 } END { exit !found }'
git tag "release-authority/v1/$ANCHOR_COMMIT" "$ANCHOR_COMMIT"
git push origin "refs/tags/release-authority/v1/$ANCHOR_COMMIT"
gh variable set RELEASE_PROVIDER_ANCHOR_COMMIT --body "$ANCHOR_COMMIT"

Use the full 40-character commit. Confirm the tag target with git ls-remote --tags origin "refs/tags/release-authority/v1/$ANCHOR_COMMIT". If the configured commit has no exact matching remote tag on the reviewed first-parent history, planning refuses with no provider mutation. Normal successful convergence advances later durable tags automatically; do not reseed to skip an incomplete release.

The obsolete release:verify, release:attest, and release:reconcile replay entrypoints are removed. release:publication plan|apply is the supported protected boundary.

Non-pull-request work

main and schedules do not replay the deterministic portfolio. Every external observation is individually named in scripts/external-drift/witnesses.json, including its owning workflow.

  • The former weekly macos-filesystem-capabilities workflow is removed. Historical results do not establish current macOS verification.
  • .github/workflows/release.yml can manually run only npm-trusted-publisher-oidc-exchange. npm Trusted Publishing authorizes one exact workflow identity for @punks/cli, so the non-mutating witness shares the protected release workflow rather than presenting a second workflow identity. Manual dispatch skips release-evidence, release-plan, and production; those jobs require a push to main.

Publication remains in the protected release workflow and binds the immutable repository, commit, and tree input from the main checkout. Changed changelog paths alone select none, baseline, npm, or mixed; publication does not own a product-test replay.

Hosted run 32259801134 proves the repaired route: the witness reported result: observed and npmTokenReceived: true, while release evidence, planning, and Production were skipped. It exchanged authority without publishing. Earlier run 32257389157 received HTTP 201 but rejected the response because the adapter assumed an undocumented expires field; that RED is superseded.

Hosted closeout evidence

  • PR #145 run 32256807550 passed Affected Verification and Stable Aggregate at 55/55 with 49 cached. Its Candidate Evidence is bound to tree e87222547b4b20adbf75a27c43a950e10048d5f5. Run 32256389904 proves superseded-revision cancellation.
  • The macOS witness passed in 32254400362.
  • Release runs 32257146346 and 32258817830 failed closed before mutation and exposed squash-provenance and empty-history defects. PR #146 and #147 validation runs 32258617884 and 32259308638 passed after repair.
  • Exact non-product ownership proof permitted the one-time lightweight tag release-authority/v1/bb4dcb27cedcd7ee11237de13955cab88c41e289. It remains the sole durable authority tag.
  • Protected release 32259491112 passed evidence, plan, and Production from anchor bb4dcb27 to current c37a7d71. The plan contained one none entry with no products; apply completed with firstIncomplete: null. It changed no npm version, baseline, GitHub release, or durable anchor.

External-fork verification is unsupported and has no pending execution gate. The historical fork-lane implementation and its unexecuted hosted proof remain recorded in the completed plan and implementation notes as superseded evidence. Real npm or baseline convergence remains conditional on a separately ready reviewed product release and was correctly not triggered for validation.

Local checks

Use the same Turbo graph locally. The focused publication seams can be checked without external mutation:

bun run --cwd apps/cli test
bun run --cwd apps/cli test -- src/scripts/release-publication.test.ts src/scripts/release-dispatcher.test.ts
bun run --cwd apps/cli check
bun run --cwd apps/cli check-types
TURBO_SCM_BASE=<base-sha> TURBO_SCM_HEAD=<head-sha> \
  bun scripts/behavior-contract/run-ci-verification.mjs affected --dry=json

The dry run shows selected package tasks, root-policy work, and intentional omissions. Repeat an unchanged deterministic task to confirm that Turbo restores its cached result.

Historical local verification

The earlier verification graph used the then-current origin/main merge base and HEAD, the portless API URL, deterministic CI environment values, and disabled remote caching. This command is retained as historical evidence; current capability selection uses the wrapper above:

TURBO_SCM_BASE=$(git merge-base origin/main HEAD) TURBO_SCM_HEAD=$(git rev-parse HEAD) NEXT_PUBLIC_SERVER_URL=https://harness-api.localhost CI=true LANG=C LC_ALL=C NODE_ENV=test TZ=UTC bunx turbo run build lint check check-types test test:browser '//#check:repo' --affected --output-logs=errors-only

The pre-review run completed 55/55 tasks with 0 cached in 58.463 seconds. After all review fixes, the same exact command completed 55/55 across 13 packages with remote caching disabled, 44 tasks restored from the local cache, in 39.796 seconds. Both timings belong to the historical verification graph, not issue 217; neither replaces hosted authority evidence.

On this page