harn-stdlib 0.10.53

Embedded Harn standard library source catalog
Documentation
/**
 * std/bump/runtime — the pure, provider-agnostic state machine that bumps a
 * Harn package repository onto a newer pinned Harn runtime.
 *
 * The orchestration logic here performs NO I/O. Every side effect (release
 * lookup, working-tree mutation, validation, branch/PR leases, signed commit
 * creation, PR refresh, auto-merge) is reached through the injected
 * [`BumpAdapter`] capability record. `std/bump/live` owns local filesystem,
 * command, and git effects while a package supplies the remote provider
 * capability; the in-process fixtures inject fakes. That seam is what lets
 * the whole state machine be exercised with `harn test` — no network, no
 * clock, no sleeps.
 *
 * The single entry point is [`run_bump`]. It returns a typed [`BumpReceipt`]
 * that is both the machine-readable receipt the workflow surfaces and the
 * value the fixtures assert against.
 */
import { strip_v } from "std/semver"

/** Everything the caller controls about one bump attempt. */
pub type BumpOptions = {
  tag: string,
  repo: string,
  branch: string,
  base: string,
  enable_auto_merge: bool,
  merge_method: string,
}

/** Normalized readiness verdict for a target release. */
pub type ReleaseReadiness = {
  ready: bool,
  detail: string,
  attempts: int,
  attempt_lines: list<string>,
}

/** Outcome of writing the new pin and regenerating derived sources. */
pub type BumpApplyOutcome = {ok: bool, changed: bool, refreshed: list<string>, detail: string}

/** One declared validation lane (fmt/check/lint/test/package…) and its verdict. */
pub type BumpValidationStep = {name: string, ok: bool, detail: string}

/** Aggregate verdict over the caller's single declared validation entrypoint. */
pub type BumpValidation = {ok: bool, steps: list<BumpValidationStep>, detail: string}

/** The working-tree delta a signed commit would carry. */
pub type BumpChangeSet = {additions: list<string>, deletions: list<string>}

/** Observed state of an already-open bump PR, or nil when none exists. */
pub type BumpPrState = {
  number: int,
  head_oid: string,
  base_ref: string,
  base_oid: string,
  auto_merge_enabled: bool,
  auto_merge_method: string?,
  url: string,
  state: string,
}

/** Inputs to one atomic provider-owned branch + signed-commit publication. */
pub type BumpCommitRequest = {branch: string, base_sha: string, headline: string}

/** Result of atomically publishing the bump branch and signed commit. */
pub type BumpCommitResult = {oid: string, url: string, branch_action: string}

/** Result of a create-or-refresh PR upsert. */
pub type BumpPrUpsert = {number: int, head_oid: string, url: string, created: bool}

/** Result of arming auto-merge. */
pub type BumpAutoMerge = {enabled: bool, state: string, detail: string}

/**
 * The injected capability surface. Each field is a function the orchestrator
 * calls at a well-defined phase; swapping the record swaps live I/O for
 * fixtures without touching a line of the state machine.
 */
pub type BumpAdapter = {
  resolve_target_tag: fn(string) -> string,
  release_ready: fn(string) -> ReleaseReadiness,
  read_current_version: fn() -> string,
  base_sha: fn() -> string,
  apply_version: fn(string) -> BumpApplyOutcome,
  run_validation: fn() -> BumpValidation,
  working_changes: fn() -> BumpChangeSet,
  find_bump_pr: fn() -> BumpPrState?,
  disable_auto_merge: fn(BumpPrState, string) -> bool,
  publish_commit: fn(BumpCommitRequest) -> BumpCommitResult,
  branch_matches_base: fn() -> bool,
  upsert_pr: fn(BumpPrState?, string) -> BumpPrUpsert,
  close_pr: fn(int, string) -> bool,
  enable_auto_merge: fn(int, string) -> BumpAutoMerge,
}

/**
 * Terminal outcome of a bump attempt. Every early-exit is a first-class,
 * idempotent-safe state, not an error.
 */
pub type BumpOutcome = "not_ready" \
  | "already_current" \
  | "refresh_failed" \
  | "no_changes" \
  | "validation_failed" \
  | "closed_noop" \
  | "committed"

/** What happened to the PR head during this attempt. */
pub type BumpPrAction = "none" | "created" | "refreshed" | "closed"

/** The machine-readable receipt every attempt emits. */
pub type BumpReceipt = {
  schema: "harn-bump-runtime-v1",
  outcome: BumpOutcome,
  ok: bool,
  repo: string,
  branch: string,
  target_tag: string,
  previous_version: string,
  ready: bool,
  readiness_detail: string,
  changed: bool,
  refreshed: list<string>,
  refresh_ok: bool,
  refresh_detail: string,
  validation: BumpValidation,
  changeset: BumpChangeSet,
  lease_recovered: bool,
  commit_oid: string?,
  pr_action: BumpPrAction,
  pr_number: int?,
  pr_url: string?,
  auto_merge: BumpAutoMerge?,
  notes: list<string>,
}

fn __bump_default_validation() -> BumpValidation {
  return {ok: true, steps: [], detail: ""}
}

fn __bump_empty_changeset() -> BumpChangeSet {
  return {additions: [], deletions: []}
}

/**
 * Merge caller-supplied options over the canonical defaults. The bump branch,
 * base, and squash auto-merge mirror the shell workflow this replaces.
 *
 * @effects: []
 * @errors: []
 */
pub fn bump_options(overrides: dict = {}) -> BumpOptions {
  const o = overrides ?? {}
  return {
    tag: to_string(o.tag ?? ""),
    repo: to_string(o.repo ?? ""),
    branch: to_string(o.branch ?? "automation/bump-harn-runtime"),
    base: to_string(o.base ?? "main"),
    enable_auto_merge: o.enable_auto_merge ?? true,
    merge_method: to_string(o.merge_method ?? "squash"),
  }
}

/**
 * True when the pinned version already matches the target, module a leading
 * `v`. The pin file and the resolved tag are compared on their bare semver so
 * `v0.10.30` and `0.10.30` are the same pin.
 *
 * @effects: []
 * @errors: []
 */
pub fn bump_is_current(previous: string, target: string) -> bool {
  return strip_v(trim(previous ?? "")) == strip_v(trim(target ?? ""))
    && strip_v(trim(target ?? ""))
    != ""
}

/**
 * True when a changeset carries nothing a commit could include.
 *
 * @effects: []
 * @errors: []
 */
pub fn bump_changeset_empty(changeset: BumpChangeSet) -> bool {
  return len(changeset.additions) == 0 && len(changeset.deletions) == 0
}

fn __bump_receipt_base(options, target, previous, readiness) -> BumpReceipt {
  return {
    schema: "harn-bump-runtime-v1",
    outcome: "not_ready",
    ok: false,
    repo: options.repo,
    branch: options.branch,
    target_tag: target,
    previous_version: previous,
    ready: readiness.ready,
    readiness_detail: readiness.detail,
    changed: false,
    refreshed: [],
    refresh_ok: true,
    refresh_detail: "",
    validation: __bump_default_validation(),
    changeset: __bump_empty_changeset(),
    lease_recovered: false,
    commit_oid: nil,
    pr_action: "none",
    pr_number: nil,
    pr_url: nil,
    auto_merge: nil,
    notes: [],
  }
}

/**
 * Run the full bump state machine against an injected adapter and return its
 * receipt. Pure with respect to this process: all effects flow through
 * `adapter`. The phase order is fixed and every early return leaves the repo
 * in a safe, re-runnable state.
 *
 * @effects: []
 * @errors: []
 */
pub fn run_bump(options: BumpOptions, adapter: BumpAdapter) -> BumpReceipt {
  const target = adapter.resolve_target_tag(options.tag)
  const readiness = adapter.release_ready(target)
  const previous = trim(adapter.read_current_version())
  let receipt = __bump_receipt_base(options, target, previous, readiness)
  // Phase 1 — release readiness gate. A target that is still publishing its
  // assets is not an error; the scheduled rerun will pick it up.
  if !readiness.ready {
    return receipt
      + {
      outcome: "not_ready",
      ok: true,
      notes: ["target release is not fully published yet"],
    }
  }
  // Phase 2 — idempotent no-op. Concurrent/scheduled reruns land here once the
  // pin already matches, so the common steady state does zero mutation.
  if bump_is_current(previous, target) {
    return receipt
      + {outcome: "already_current", ok: true, notes: ["pin already matches ${target}"]}
  }
  // Phase 3 — write the pin and regenerate derived sources (lockfile, codegen).
  const apply = adapter.apply_version(target)
  receipt = receipt
    + {
    changed: apply.changed,
    refreshed: apply.refreshed,
    refresh_ok: apply.ok,
    refresh_detail: apply.detail,
  }
  if !apply.ok {
    return receipt
      + {outcome: "refresh_failed", ok: false, notes: ["refresh failed: ${apply.detail}"]}
  }
  if !apply.changed {
    return receipt
      + {
      outcome: "already_current",
      ok: true,
      notes: ["writing ${target} produced no change"],
    }
  }
  // Phase 4 — the caller's single declared validation entrypoint. A failure
  // stops before any commit so a broken bump never reaches a PR.
  const validation = adapter.run_validation()
  receipt = receipt + {validation: validation}
  if !validation.ok {
    return receipt
      + {
      outcome: "validation_failed",
      ok: false,
      notes: ["validation failed: ${validation.detail}"],
    }
  }
  // Phase 5 — nothing to commit? Treat as a clean no-op rather than an empty PR.
  const changeset = adapter.working_changes()
  receipt = receipt + {changeset: changeset}
  if bump_changeset_empty(changeset) {
    return receipt
      + {outcome: "no_changes", ok: true, notes: ["no file changes after refresh"]}
  }
  // Phase 6 — branch/PR lease reconciliation. Observe base before touching
  // remote state, then disarm a stale PR under exact head + base leases. This
  // prevents a stale actor from disabling or replacing a newer publication.
  const existing = adapter.find_bump_pr()
  const base_sha = trim(adapter.base_sha())
  let notes = []
  let lease_recovered = false
  if existing != nil && existing.auto_merge_enabled {
    if !adapter.disable_auto_merge(existing, base_sha) {
      throw "std/bump/runtime: failed to disable stale auto-merge on #${existing.number}"
    }
    lease_recovered = true
    notes = notes + ["disarmed stale auto-merge on #${existing.number} before refresh"]
  }
  receipt = receipt + {lease_recovered: lease_recovered}
  // Phase 7 — atomically create/reset the branch and publish one signed commit
  // under the observed base lease. The provider adapter owns the worktree
  // encoding and branch mutation as one deep operation.
  const commit = adapter.publish_commit(
    {branch: options.branch, base_sha: base_sha, headline: "chore: bump Harn runtime to ${target}"},
  )
  receipt = receipt + {commit_oid: commit.oid}
  // Phase 8 — if the refreshed branch tree already matches base, the bump is a
  // true no-op: close any stale PR instead of opening an empty one.
  if adapter.branch_matches_base() {
    if existing != nil {
      adapter.close_pr(
        existing.number,
        "Closing: the refreshed Harn bump already matches ${options.base}.",
      )
      return receipt
        + {
        outcome: "closed_noop",
        ok: true,
        pr_action: "closed",
        pr_number: existing.number,
        notes: notes + ["branch tree matches ${options.base}; closed #${existing.number}"],
      }
    }
    return receipt
      + {
      outcome: "no_changes",
      ok: true,
      notes: notes + ["branch tree matches ${options.base}; no PR needed"],
    }
  }
  // Phase 9 — create or refresh the PR.
  const upsert = adapter.upsert_pr(existing, commit.oid)
  const pr_action = if upsert.created {
    "created"
  } else {
    "refreshed"
  }
  receipt = receipt + {pr_action: pr_action, pr_number: upsert.number, pr_url: upsert.url}
  // Phase 10 — arm auto-merge so the merge queue owns the final landing.
  let auto_merge = nil
  if options.enable_auto_merge {
    auto_merge = adapter.enable_auto_merge(upsert.number, upsert.head_oid)
    notes = notes
      + [
      if auto_merge.enabled {
        "auto-merge armed on #${upsert.number}"
      } else {
        "auto-merge not enabled: ${auto_merge.detail}"
      },
    ]
  }
  return receipt + {outcome: "committed", ok: true, auto_merge: auto_merge, notes: notes}
}