harn-stdlib 0.10.53

Embedded Harn standard library source catalog
Documentation
/**
 * std/bump/live — local effects for the provider-neutral Harn bump runtime.
 *
 * This module owns filesystem, git, command, validation, and readiness polling.
 * Remote repository behavior enters through one typed [`LiveBumpRemote`]
 * capability. Provider packages implement that capability; stdlib never speaks
 * REST, GraphQL, or a provider CLI and therefore cannot drift into a second
 * connector implementation.
 *
 * All orchestration decisions remain in `std/bump/runtime`. Tests can replace
 * the remote capability with a recording fake without a network or subprocess.
 */
import {
  BumpAdapter,
  BumpAutoMerge,
  BumpChangeSet,
  BumpCommitRequest,
  BumpCommitResult,
  BumpPrState,
  BumpPrUpsert,
} from "std/bump/runtime"
import { command_run } from "std/command"
import { poll_until } from "std/poll"
import { is_v_semver, strip_v } from "std/semver"

/** Everything the local adapter needs for mutation, validation, and polling. */
pub type LiveBumpConfig = {
  release_repo: string,
  repo_dir: string,
  version_file: string,
  refresh_command: string,
  validation_command: string,
  readiness_max_attempts: int,
  readiness_interval_ms: int,
  readiness_timeout_ms: int,
}

/** Closed release projection needed by the readiness gate. */
pub type LiveBumpRelease = {found: bool, draft: bool, prerelease: bool, asset_names: list<string>}

/**
 * Provider package boundary for remote bump effects.
 *
 * One constructed instance is already bound to repository, branch, base, and
 * authentication. The stdlib surface therefore exposes behavior rather than
 * provider transport or configuration details.
 */
pub type LiveBumpRemote = {
  latest_release_tag: fn() -> string,
  release_view: fn(string) -> LiveBumpRelease,
  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,
}

/** Release finalization sidecars that prove a Harn release is fully published. */
const REQUIRED_RELEASE_ASSETS = ["SHA256SUMS", "release-assets.json"]

type VersionWriteResult = {ok: bool, detail: string}

type VersionApplyResult = {ok: bool, changed: bool, refreshed: list<string>, detail: string}

/**
 * Fill a partial config with the reusable workflow defaults.
 *
 * @effects: []
 * @errors: []
 */
pub fn live_bump_config(overrides: dict = {}) -> LiveBumpConfig {
  const o = overrides ?? {}
  return {
    release_repo: to_string(o.release_repo ?? "burin-labs/harn"),
    repo_dir: to_string(o.repo_dir ?? "."),
    version_file: to_string(o.version_file ?? ".harn-version"),
    refresh_command: to_string(o.refresh_command ?? "harn install --locked || harn install"),
    validation_command: to_string(o.validation_command ?? ""),
    readiness_max_attempts: to_int(o.readiness_max_attempts ?? 1) ?? 1,
    readiness_interval_ms: to_int(o.readiness_interval_ms ?? 20000) ?? 20000,
    readiness_timeout_ms: to_int(o.readiness_timeout_ms ?? 20000) ?? 20000,
  }
}

fn __git(tools: HarnessTools, repo_dir: string, argv: list<string>) {
  return command_run(tools, ["git", "-C", repo_dir] + argv)
}

fn __shell(tools: HarnessTools, repo_dir: string, command: string) {
  return command_run(tools, {mode: "shell", command: command, cwd: repo_dir})
}

fn __lines(text) -> list<string> {
  const value = trim(to_string(text ?? ""))
  return if value == "" {
    []
  } else {
    split(value, "\n").filter({ line -> trim(line) != "" }).to_list()
  }
}

fn __working_changes(tools: HarnessTools, repo_dir: string) -> BumpChangeSet {
  const tracked_additions = __lines(
    __git(tools, repo_dir, ["diff", "--name-only", "--diff-filter=ACMRTUXB", "HEAD"]).stdout,
  )
  const untracked_additions = __lines(
    __git(tools, repo_dir, ["ls-files", "--others", "--exclude-standard"]).stdout,
  )
  return {
    additions: tracked_additions
      + untracked_additions.filter(
      { name -> !contains(tracked_additions, name) },
    )
      .to_list(),
    deletions: __lines(
      __git(tools, repo_dir, ["diff", "--name-only", "--diff-filter=D", "HEAD"]).stdout,
    ),
  }
}

/** Update the pin through typed filesystem authority, never shell redirection. */
fn __write_version(fs: HarnessFs?, path: string, version: string) -> VersionWriteResult {
  if fs == nil {
    return {
      ok: false,
      detail:
        "std/bump/live: HarnessFs is required to update ${path}; pass harness.fs to live_bump_adapter",
    }
  }
  const result = try {
    fs.write_text(path, "${version}\n")
    nil
  }
  return if is_ok(result) {
    {ok: true, detail: ""}
  } else {
    {ok: false, detail: to_string(unwrap_err(result))}
  }
}

fn __read_version(fs: HarnessFs?, path: string) -> string {
  if fs == nil || !fs.exists(path) {
    return ""
  }
  return trim(fs.read_text(path))
}

fn __apply_version(
  fs: HarnessFs?,
  tools: HarnessTools,
  cfg: LiveBumpConfig,
  target: string,
) -> VersionApplyResult {
  const write = __write_version(fs, cfg.version_file, strip_v(target))
  const refresh = if write.ok {
    __shell(tools, cfg.repo_dir, cfg.refresh_command)
  } else {
    nil
  }
  const refresh_ok = refresh?.success ?? false
  const changes = __working_changes(tools, cfg.repo_dir)
  const refreshed = changes.additions + changes.deletions
  return {
    ok: write.ok && refresh_ok,
    changed: len(refreshed) > 0,
    refreshed: refreshed,
    detail: if !write.ok {
      "version-file write failed: " + write.detail
    } else if refresh_ok {
      "refresh ok"
    } else {
      trim((refresh?.stderr ?? "") + (refresh?.stdout ?? ""))
    },
  }
}

fn __release_finalized(release: LiveBumpRelease) -> bool {
  const missing = REQUIRED_RELEASE_ASSETS.filter({ name -> !contains(release.asset_names, name) })
    .to_list()
  return release.found && !release.draft && !release.prerelease && len(missing) == 0
}

/**
 * Build the local adapter around one package-owned remote capability.
 *
 * @effects: [fs, process, net]
 * @errors: [invalid target tags or delegated local/provider failures]
 */
pub fn live_bump_adapter(
  clock: HarnessClock,
  tools: HarnessTools,
  remote: LiveBumpRemote,
  config: dict = {},
  fs: HarnessFs? = nil,
) -> BumpAdapter {
  const cfg = live_bump_config(config)
  return {
    resolve_target_tag: fn(tag) {
      const requested = trim(tag ?? "")
      const resolved = if requested == "" {
        trim(remote.latest_release_tag())
      } else {
        requested
      }
      if !is_v_semver(resolved) {
        throw "std/bump/live: expected a tag like vX.Y.Z, got '${resolved}'"
      }
      return resolved
    },
    release_ready: fn(target) {
      let attempts = 0
      let lines = []
      const found = poll_until(
        clock,
        { ->
          attempts = attempts + 1
          const view = remote.release_view(target)
          const ready = __release_finalized(view)
          lines = lines + ["attempt ${attempts}: ${target} finalized=${ready}"]
          return if ready {
            view
          } else {
            nil
          }
        },
        {
          max_attempts: cfg.readiness_max_attempts,
          interval_ms: cfg.readiness_interval_ms,
          timeout_ms: cfg.readiness_timeout_ms,
        },
      )
      const ready = found != nil
      return {
        ready: ready,
        detail: if ready {
          "${target} finalized with required assets present"
        } else {
          "${target} is not fully published yet in ${cfg.release_repo}"
        },
        attempts: attempts,
        attempt_lines: lines,
      }
    },
    read_current_version: fn() { return __read_version(fs, cfg.version_file) },
    base_sha: fn() { return trim(__git(tools, cfg.repo_dir, ["rev-parse", "HEAD"]).stdout ?? "") },
    apply_version: fn(target) { return __apply_version(fs, tools, cfg, target) },
    run_validation: fn() {
      if trim(cfg.validation_command) == "" {
        return {ok: true, steps: [], detail: "no validation command declared"}
      }
      const result = __shell(tools, cfg.repo_dir, cfg.validation_command)
      const ok = result.success ?? false
      return {
        ok: ok,
        steps: [{name: "validation", ok: ok, detail: cfg.validation_command}],
        detail: if ok {
          "validation passed"
        } else {
          trim((result.stderr ?? "") + (result.stdout ?? ""))
        },
      }
    },
    working_changes: fn() { return __working_changes(tools, cfg.repo_dir) },
    find_bump_pr: remote.find_bump_pr,
    disable_auto_merge: remote.disable_auto_merge,
    publish_commit: remote.publish_commit,
    branch_matches_base: remote.branch_matches_base,
    upsert_pr: remote.upsert_pr,
    close_pr: remote.close_pr,
    enable_auto_merge: remote.enable_auto_merge,
  }
}