# Reusable "bump Harn runtime" workflow

> Every Harn package repo pins the Harn runtime it builds against in a .harn-version file. Keeping that pin current used to mean copying a large bump-harn.yml state machine into...

Website: https://harnlang.com/dev/reusable-bump-harn-runtime.html

This page documents Harn, which is pre-1.0. Language, standard library, and CLI APIs may change. If the intended version is unclear, clarify before using this page.

---

Every Harn package repo pins the Harn runtime it builds against in a
`.harn-version` file. Keeping that pin current used to mean copying a large
`bump-harn.yml` state machine into each repo. Harn now publishes **one**
reusable workflow so a package needs only a small trigger wrapper plus its own
declared refresh and validation commands.

- Workflow: `.github/workflows/bump-harn.yml` (`workflow_call`).
- Orchestration: `scripts/bump-driver/bump_harn_runtime.harn` →
  `std/bump/runtime` (pure state machine) → `std/bump/live` (local filesystem,
  git, command, and polling effects) → the locked `harn-github-connector`
  package (all remote GitHub behavior).
- Workspace boundary: the caller is checked out at `package/`; the exact
  workflow-owner orchestration checkout is its sibling. Refresh, validation, and local git
  effects run from `package/`. The source script's nested package manifest
  activates its locked connector without changing that working directory.
- Receipt schema: `harn-bump-runtime-v1` (printed to stdout; key fields also
  land as step outputs and a step-summary block).
- Package-test compatibility: before any mutation, the target runtime writes a
  compact `package test-inventory` receipt. The workflow retains it as an
  artifact and embeds its exact base64 JSON plus SHA-256 in any generated bump
  pull request, so a repair sees the discovery failure that triggered it.

## Minimal caller workflow

Drop this into the consuming repo. The only repo-specific parts are the trigger
schedule and, when the default lock refresh is insufficient, the
`refresh-command`, `format-command`, and `validate-command`.

```yaml
name: Bump Harn Runtime
on:
  workflow_dispatch:
    inputs:
      version:
        description: "Optional Harn tag (vX.Y.Z). Defaults to latest release."
        required: false
        type: string
  schedule:
    - cron: "43 9 * * *"

permissions:
  contents: write
  pull-requests: write

jobs:
  bump:
    uses: burin-labs/harn/.github/workflows/bump-harn.yml@<pinned-sha>
    with:
      # The fleet registry generates both occurrences from one policy value.
      # The reusable workflow rejects non-SHA refs before checkout.
      orchestration-sha: <pinned-sha>
      version: ${{ inputs.version }}
      # Optional repository-owned materialization. The target tag is inherited
      # as HARN_BUMP_TARGET_TAG. The reusable workflow first applies the target
      # runtime's deterministic capability migrations, then runs this refresh,
      # then applies the target runtime's formatter before validation. All
      # mutations are included in the one signed commit; a non-zero exit blocks
      # it.
      refresh-command: |
        harn install --locked
        ./scripts/regenerate-derived-sources "$HARN_BUMP_TARGET_TAG"
      # Optional repository-owned valid-source boundary. The default is
      # `harn fmt .`; repositories that intentionally keep invalid `.harn`
      # fixtures must point this at their authoritative formatter command.
      # Empty commands fail closed. Formatting always runs after regeneration
      # and before refresh success is recorded.
      format-command: ./scripts/format-harn-sources
      # Optional, when the repository owner commands require Node. The shared
      # workflow installs this exact version rather than trusting runner state.
      # If package.json declares an exact npm, pnpm, or yarn `packageManager`,
      # Corepack activates that exact version before refresh and validation.
      node-version: "22"
      # Repository-owned verification runs after every mutation. A non-zero
      # exit blocks the commit.
      validate-command: |
        set -euo pipefail
        mapfile -t files < <(git ls-files '*.harn')
        if (( ${#files[@]} > 0 )); then
          harn fmt "${files[@]}"
          harn check "${files[@]}"
          harn lint "${files[@]}"
        fi
        [ -d tests ] && harn test tests/ --parallel || true
      # Optional controller handoff. A failed refresh or validation still
      # fails the workflow, but publishes the exact signed mutation as a PR
      # with auto-merge disabled so a separately bounded repair lane has a
      # head lease. Ordinary callers should keep the default false.
      publish-failure-for-repair: false
      # Optional. The shared workflow applies `harn fix --safety
      # behavior-preserving` to your sources before your refresh command, so a
      # bump can normalize its own fallout. Set false to decline that pass and
      # keep the bump limited to the version change plus mandatory compatibility
      # migrations and your own commands. The implicit-any compatibility
      # census, capability migrations, and deterministic formatting are applied
      # either way. Defaults to true.
      apply-behavior-preserving-fixes: true
    secrets:
      app-client-id: ${{ secrets.RELEASE_APP_CLIENT_ID }}
      app-private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
```

Pin `@<pinned-sha>` to a full commit SHA of `burin-labs/harn`, and project that
same registry-owned value into `orchestration-sha`. The reusable workflow
validates exact lowercase 40-hex before checking out its setup action and bump
driver from that commit. The independently resolved release tag selects only
the installed CLI and its embedded `std/bump/*` modules. This lets a current
adapter carry compatibility for an older target without granting callers an
arbitrary orchestration ref.

## Idempotency and concurrency

- Already current: the pin already matches the resolved target → clean no-op,
  zero mutation.
- Old implicit parameters: before caller regeneration or strict validation,
  the target runtime translates each checker-owned omitted annotation to
  explicit `any`. The printed typed census names scanned, changed, pending,
  unresolved, and changed-semantics sites; any nonzero pending or failure count
  stops the bump.
- Not yet published: a target whose release is still finalizing is a clean exit
  (`outcome: not_ready`); the next scheduled run picks it up.
- Refresh or validation failure: the default remains fail-before-publish.
  Controllers that set `publish-failure-for-repair: true` receive a signed
  repair PR only when the failed mutation produced a file delta. The receipt
  preserves `refresh_failed` or `validation_failed`, validation is not run
  after a failed refresh, and the workflow remains failed. This path never
  arms auto-merge; a separate validator must prove and republish the leased
  head before merge can be enabled. When a compatible older target predates
  the typed refresh outcome, the workflow's per-run success sentinel still
  makes validation fail closed.
- Package test discovery: the target runtime inventories selected test files
  before changing the repository. A zero-test file or undeclared empty suite
  is preserved as migration evidence while the normal validation and repair
  policy decides whether a repair pull request may be published. Targets from
  before this command existed record `package_test_inventory_unsupported`;
  they never report an unmeasured suite as zero.
- Stale heads: an open bump PR with auto-merge armed is disarmed only under its
  exact PR-head and base-head leases before refresh begins. The runtime checks
  the checkout's exact base against the remote branch before refresh, after
  validation, and again immediately before arming. The connector derives and
  publishes a GitHub-signed commit only while the measured lease is current.
  Stale actors fail closed.
- Advanced base: a refresh takes long enough that the base branch can move
  under it, and the refreshed content is a pure function of the base content
  and the target release. So a moved base is re-derived, not discarded. Before
  refresh and before publication, the runtime adopts the observed base head
  into the checkout (an authenticated fetch through the provider capability,
  exact cleanup of the discarded refresh paths, then a detached checkout of
  that head) and re-runs apply and validation against it, holding the disarm
  lease. Unrelated untracked files survive, and any unreadable comparison or
  incomplete cleanup aborts the adoption before the checkout moves. Three
  adoptions are allowed per attempt;
  `base_adoptions` in the receipt reports how many were made. After that, or
  when the remote head cannot be read at all, the attempt returns
  `outcome: base_advanced` with a `fresh_base_retry` recovery, and the fleet
  controller's bounded successor run — or a standalone caller's next scheduled
  tick — starts again from a fresh checkout of the declared base.
- Contested refresh output: an adoption is refused when the incoming commits
  changed a path this bump's refresh also authors. Adopting would silently
  decide a contest between two writers of one artifact, so the attempt returns
  `outcome: base_conflict` and names the contested paths for a human. This is
  the only base-race exit a fresh retry cannot clear on its own.
- Base advance immediately before arming: the PR is already published, so the
  attempt stops rather than redoing the refresh. It returns `base_advanced`
  and leaves the PR unarmed; the next run refreshes that PR head and arms it.

## Version availability

The orchestration modules (`std/bump/*`) are embedded in the Harn CLI, so the
workflow runs the state machine under the **target** Harn release. The feature
ships in the release identified in `changelog.d/5299.added.md`; bumping to any
release at or after that version works. (Bumps are always forward to the latest
release, so this holds in practice.)

The driver package declares the runtime floor it needs in
`scripts/bump-driver/harn.toml`. A caller that repins `orchestration-sha` ahead
of its runtime gets that floor as a diagnostic rather than a missing-capability
failure part-way through a bump.

## Security boundary

- **Least privilege, short-lived credentials.** The workflow mints a GitHub App
  installation token scoped to `contents: write` + `pull-requests: write` for
  the run only. The caller passes the App client id and private key as
  `secrets`; no long-lived PAT is used.
- **Signed commits.** The bump commit is created through GitHub's
  `createCommitOnBranch` GraphQL mutation under the App identity, so GitHub
  signs it and an org `required_signatures` ruleset is satisfied. A local
  `git commit` + push would land unsigned and be rejected.
- **Immutable supply chain.** Third-party actions are pinned by full commit SHA.
  The workflow contract, setup action, and driver use one registry-projected
  Harn commit SHA. The installed target release owns embedded runtime modules
  and capability migrations. The nested driver installs with `--locked`,
  so the connector source and content hash are fixed. `setup-harn` verifies the
  downloaded runtime archive against its published SHA-256 before installing.
- **Caller package-manager pin.** When `node-version` is configured, the shared
  workflow activates the exact npm, pnpm, or yarn version declared by the
  caller's `package.json#packageManager`. Undeclared managers remain a Node-only
  setup; unsupported or non-exact declarations fail before caller commands run.
- **One package-contract owner.** The shared workflow invokes Harn's structural
  test-discovery inventory but encodes none of a package's code-generation or
  build/test commands. Repositories expose those owner commands through
  `refresh-command`, `format-command`, and `validate-command`; consumers copy
  no orchestration, release-readiness, signing, branch, or PR machinery.
- **Sandbox posture.** The orchestration runs under `harn run --no-sandbox`
  because it must reach git, the GitHub API through the connector, and the
  caller's refresh and validation commands. It carries no secret beyond the
  scoped installation token, which is passed via the environment and never
  written to the repo.

---

## Read next

- [Release binary-size policy](https://harnlang.com/dev/release-binary-size-policy.md)
- [Merge overrides](https://harnlang.com/dev/merge-overrides.md)
