Pre-release Harn is pre-1.0 — the language, standard library, and CLI may change between releases. See the release notes

Release Harn

Use Harn's GitHub workflows to open a release, certify its files, and publish them. You need permission to dispatch workflows in burin-labs/harn.

Open the release pull request#

Releases are cut from green, the newest main commit whose full CI suite passed. Confirm that green declares an X.Y.Z-dev workspace version and contains unreleased changelog.d/<id>.<category>.md fragments. Fragments that landed on main after green wait for the next release. Then run:

gh workflow run bump-release.yml --repo burin-labs/harn --ref main

The opener takes no version or candidate-build inputs. It strips the declared -dev suffix, folds the fragments, regenerates derived files, and opens Release vX.Y.Z. It publishes one GitHub-signed commit and adds ship so Smart Ship owns delivery through the merge queue. Required checks and review still apply. Repeated openers leave an existing PR's delivery grant unchanged.

The daily schedule runs the same decision. A stable workspace version or no unreleased fragments produces action=none, with a notice explaining why. An unreadable pull-request list fails instead of opening a duplicate.

The signed commit is recorded at release-attempt/vX.Y.Z/<commit> before the pull request opens. An existing release stays frozen at that attempt. Later fragments remain for the next release, including when their changes merge ahead of this release. A push changing changelog.d checks the existing attempt without rebuilding or opening a release.

An existing release/vX.Y.Z branch without a matching attempt fails closed. To replace an unrecorded stale attempt, close its pull request and dispatch the opener explicitly. Inspect the replacement head's required checks and review. Do not reset an attempt ref or dispatch a second version selector.

Repair a failed development bump#

If publication completed but the post-publication development bump failed, repair it through the same opener:

gh workflow run open-development-bump.yml --repo burin-labs/harn --ref main -f published_tag=vX.Y.Z

Omit published_tag to use the latest published stable release. The workflow verifies publication before minting the release App token, derives the next development identity, opens or reuses its pull request, validates its grammar receipt, and enrolls a newly opened PR in Smart Ship. An existing PR retains its owner's delivery grant, including an explicit stop. Resume a stopped PR only after reading its label history and resolving the reason for the stop. A repeated repair after the cutover lands is a no-op. Draft, prerelease, unreadable, and incomplete publication records refuse the repair.

The development monitor reports success only when main declares the expected development version. An open update is pending, not successful convergence; an unreadable, incomplete, or ambiguous PR census fails the measurement.

The five-minute repository-state controller automatically dispatches the same opener once for an owed development version when no open cutover PR exists. It verifies publication and reserves the attempt on the fixed commit in scripts/development_cutover_repair.json before dispatch. Scheduled and manual controller checks share that reservation owner. Existing PRs, including red ones, remain their owner's work.

If reservation or dispatch fails, inspect the controller run and use the manual repair command above. The reservation deliberately prevents another automatic attempt, including after a crash. Missing or incomplete reservation history refuses automatic repair. Do not change the fixed anchor or seed to clear a reservation, and do not infer completed integration from a reserved attempt.

Retire an unpublished candidate with failed source CI#

Read the failed producer and the main-push CI run at its exact source. If both are terminal failures and the version was never published, request retirement through the existing development bump workflow:

gh workflow run open-development-bump.yml --repo burin-labs/harn --ref main \
  -f published_tag=vPREDECESSOR \
  -f retire_source_sha=FULL_SHA -f retire_release_pr=PR_NUMBER \
  -f retire_producer_run=PRODUCER_RUN -f retire_source_ci_run=CI_RUN

Replace the placeholders with the published predecessor and the owning release records. Omit every consumer-retirement input: mixing the two evidence paths is refused. The planner requires complete terminal job censuses with actual failed checks, exact source identity, main ancestry and the merged release PR. It also proves that the version has no tag or release and that neither publication workflow is active, with known positive reads on those paths. The opener repeats those checks after preparation before opening its normal development-version PR. Follow that PR through the merge queue, then use the release opener for the next version. This retires an unpublished identity; it does not publish files from a failed candidate.

Follow certification and publication#

Record the release pull request and the exact commit that lands on main. Follow the runs for that commit in Harn Actions.

  1. build-release-binaries.yml builds the candidate on the version commit's push to main. The release's merge group builds nothing; its Release candidate verdict passes only after the exact-source consumer rehearsal passes at the commit that lands. A merge group can't build the candidate because the trusted macOS smoke host admits only refs/heads/main. Other pushes only warm caches. Daily scheduled runs build signed source candidates at their exact main commit, including commits outside the warm-cache path filter. They reuse a successful candidate only while its manifest, release files, and every target archive remain available and unexpired. These runs do not publish a release.
  2. The candidate run builds, signs, notarizes, and attests the five platform archives. It checks those files with the release audit and smoke tests. The candidate-manifest-<sha> artifact binds the source commit, files, digests, and publication metadata. Keep its exact run ID with the release. Its consumer rehearsal waits for successful archive and manifest jobs, then passes that producer's run ID and attempt with the authenticated observer. The consumer verifies the exact-source archive before installing it and still runs every required product check. Rehearsals without archives keep their source or pull-request CLI path. This installation permission does not replace the terminal consumer verdict or publication checks.
  3. promote-release.yml starts after the successful main push run. For a version commit it waits up to 90 minutes for green to contain it, then finds the successful candidate run at that commit and verifies the manifest, digests, and attestations. It also requires successful full-suite main-push CI at the release commit, including every deferred test family. It waits for an active run; missing or failed proof blocks publication. It creates the tag and GitHub release using those files. Promotion doesn't rebuild them.
  4. The tag starts publish-release.yml, which checks the same full-suite proof before publishing crates from the tag. Promotion also packages the published Linux archives into the container and opens the next patch's development-version pull request.

Before promotion, the candidate's exact source needs a successful macos-nightly.yml run through workflow_dispatch. The candidate run starts it: scripts/ci/dispatch_native_qualification.sh dispatches the main revision of the workflow with the candidate's source_sha. Before executing candidate code, the trusted workflow verifies that this source is a main ancestor and measures both allowed execution and a refused write through process confinement on the owned Mac. Scheduled source candidates don't start it. Arbitrary branch sources are refused before checkout or execution on the restricted runner group.

The common publication gate requires the native workspace tests, CLI smoke and test-case performance steps to have executed successfully in that exact attempt. It reports named pending or failed conditions and refuses absent, partial, skipped or foreign evidence. A run-scoped artifact binds the tested CLI source, workflow revision, repository, run, attempt and phases. A display title alone cannot qualify a candidate. Missing or mismatched receipts refuse publication. Qualification dispatches never publish their older source as a main warm cache. Windows native qualification remains advisory. The platform policy lives in scripts/lib/release_platforms.harn; its JSON projection in scripts/release_audit_contract.json is checked mechanically. Hosted audit receipts carry the downloaded artifact as the macOS platform's qualification field and retain the workflow's head_branch and policy head_sha in its run record. The audit contract checks that same source, run, attempt and phase binding before reusing proof.

Publication is complete only after you verify all of these:

  • The release pull request merged, and the immutable tag selects its certified source.
  • The exact candidate and promotion runs succeeded.
  • The GitHub release has all five archives, SHA256SUMS, and release-assets.json, with digests matching the candidate manifest.
  • The tag's crate publication succeeded, and the versioned container is anonymously pullable.
  • The post-publication development bump reached main or reported a proved no-op because main had already advanced.

Main ancestry and exact-source certification bind a tag to its release source. A tag outside main also needs a trusted SSH signature naming its candidate commit. Corrected stable sources also prove the original version transition in their first-parent history, a verified source signature, and durable publication evidence. The published manifest and signed archive index must bind all five archive digests to that exact source and the owning producer's workflow, main ref, run, and attempt. This read does not depend on temporary build artifacts or retained workflow-run records. The signed index proves provenance; the public release supplies publication authority. Its author and the required manifest, index, and archive uploaders must be the owning release app's GitHub bot identity; additional SDK assets can come from their own publisher.

Read Release assets manifest for the download contract. A visible tag or release page alone doesn't prove complete publication.

Recover a failed run#

Read the failing job before choosing a retry. Keep the commit, run ID, and candidate manifest attached to the release record.

Promotion reports a stuck release on its own. When main's stable version is still unpublished four hours after its version commit, every later promotion run fails and one [Release] vX.Y.Z is unpublished on main issue opens. Close it after the release publishes.

  • Opener failed before publication: fix the cause and dispatch bump-release.yml again. Its admission checks run again.
  • Candidate failed because of infrastructure: rerun the failed jobs in that exact candidate run. A source defect needs a corrected pull request and certification of the resulting commit. For an unpublished stable version, keep that version, build a signed source candidate on corrected main with source_candidate=true, then use manual promotion recovery. Recovery proves the stable version transition in the source's first-parent history and still requires exact-source CI, consumer rehearsal, and candidate certification.
  • Promotion failed: rerun the failed jobs in the exact promotion run. It checks the existing tag's commit and refuses a conflicting tag.
  • Candidate succeeded but no main-push event started promotion: dispatch the existing promoter on main with that certified candidate's run ID and source: gh workflow run promote-release.yml --repo burin-labs/harn --ref main -f candidate_run_id=RUN_ID -f candidate_sha=FULL_SHA. It refuses an unsuccessful or foreign producer and a source not contained in main, then applies the normal version, manifest, digest and attestation checks. If the producer never ran a consumer rehearsal, recovery runs the existing rehearsal workflow at that exact source and requires its measured pass before publication. Missing, failed, cancelled or mismatched results refuse publication. A producer's existing failed rehearsal is not replaced. Recovery publishes the existing files without rebuilding them. When the consumer needs a prepared repair before it can land against the published tag, add -f consumer_pull_request=PR_NUMBER. Recovery selects that open pull request in the registered consumer repository through the existing pairing checks; a fork, closed pull, malformed number, or conflicting source pairing refuses. Consumer observation and publication checks still run. This selection cannot reuse an earlier default-branch rehearsal.
  • The consumer observer reached its deadline, but its exact dispatched child later succeeded: after the completed-child recovery policy is on main, dispatch the normal promoter with the same producer and source plus -f completed_consumer_run_id=CHILD_RUN_ID. The authorization job verifies the original dispatch and deadline, every producer build/smoke/audit result, the child's complete successful census and product receipts, then rechecks both latest attempts. Only the observer deadline and its derived final verdict may have failed. Cancelled, failed, partial, replaced or mismatched children refuse. The shared publisher authenticates the attempt-bound authorization artifact before applying all normal archive and publication checks. The original deadline remains unmeasured; no result is rewritten and no equal-tree source substitution is accepted.
  • Crate publication failed after the tag exists: rerun the failed jobs in the tag's publish-release.yml run. Its publisher resumes remaining crates. If publication policy needed a repair on main, dispatch publish-release.yml with the immutable tag, its expected_source_sha, and the landed expected_policy_sha. Recovery uses that policy without changing the tag's source.
  • Container or development bump failed: rerun those failed promotion jobs.

Don't retag a published version or start a local publisher or watcher as a second release controller. Retired Fleet launchers and candidate-build input tuples aren't recovery entry points for these workflows.

Check downstream convergence#

Promotion's repin job dispatches the registered consumers' own update workflows. For the fleet-owned bump adapters that is one dispatch: the harn-bump-fleet promote-released-orchestration.yml workflow moves the orchestration pin to the release, and each adapter's converged landing starts that consumer's bump. Each consumer opens its own pull request, then follows its checks, review, and merge queue. Record each consumer's terminal state separately from Harn publication. Dispatch success doesn't prove that a consumer updated or that its pull request merged.

Rehearse a release change offline#

Run the fixture rehearsal with an installed Harn binary:

HARN_BIN="$(command -v harn)" bash scripts/release_rehearsal.sh

The rehearsal runs publication staging and checks archive provenance, publication policy, and development cutover. It creates no remote release. CI requires it for release changes and main pushes. It doesn't prove live credentials or replace candidate certification.

For an offline audit or dry run, use:

./scripts/release_gate.sh audit
./scripts/release_gate.sh full --bump patch --dry-run