harn-stdlib 0.10.30

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` wires the real adapter
 * over `std/connectors/github`, `std/command`, and git; 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 = {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,
  auto_merge_enabled: bool,
  url: string,
  state: string,
}

/** Inputs to a single signed `createCommitOnBranch`-style mutation. */
pub type BumpCommitRequest = {
  branch: string,
  base_sha: string,
  headline: string,
  additions: list<string>,
  deletions: list<string>,
}

/** Result of creating the signed bump commit. */
pub type BumpCommitResult = {oid: string, url: 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(int) -> bool,
  reset_branch: fn(string) -> bool,
  create_signed_commit: fn(BumpCommitRequest) -> BumpCommitResult,
  branch_matches_base: fn() -> bool,
  upsert_pr: fn(BumpPrState?) -> 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" \
  | "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>,
  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: [],
    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}
  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. Disarm a stale PR's auto-merge
  // BEFORE the branch is force-reset so it cannot merge mid-refresh and race a
  // duplicate onto main; then reset the bump branch to the observed base.
  const existing = adapter.find_bump_pr()
  let notes = []
  let lease_recovered = false
  if existing != nil && existing.auto_merge_enabled {
    adapter.disable_auto_merge(existing.number)
    lease_recovered = true
    notes = notes + ["disarmed stale auto-merge on #${existing.number} before refresh"]
  }
  const base_sha = trim(adapter.base_sha())
  adapter.reset_branch(base_sha)
  receipt = receipt + {lease_recovered: lease_recovered}
  // Phase 7 — one signed commit via the App identity (GitHub signs
  // createCommitOnBranch mutations, satisfying required_signatures).
  const commit = adapter.create_signed_commit(
    {
      branch: options.branch,
      base_sha: base_sha,
      headline: "chore: bump Harn runtime to ${target}",
      additions: changeset.additions,
      deletions: changeset.deletions,
    },
  )
  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)
  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}
}