harn-stdlib 0.8.8

Embedded Harn standard library source catalog
Documentation
import { filter_nil } from "std/collections"
import { merge } from "std/json"
import { wait_for } from "std/monitors"

/**
 * Per-connector configuration applied to every GitHub call. Open shape — any
 * provider-specific knob may live here without breaking back-compat.
 */
type GitHubConnectorConfig = {
  token?: string,
  base_url?: string,
  user_agent?: string,
  retry_attempts?: int,
  retry_backoff_ms?: int,
  rate_limit_delay_ms?: int,
}

/**
 * Common per-call options accepted by every GitHub helper. Open shape so
 * callers can supply provider-specific extras without breaking the contract.
 */
type GitHubCallOptions = {
  token?: string,
  base_url?: string,
  user_agent?: string,
  headers?: dict<string, string>,
  timeout_ms?: int,
  retry_attempts?: int,
}

/** Options for `wait_until_*` helpers — adds a polling cadence to the call options. */
type GitHubWaitOptions = {
  token?: string,
  base_url?: string,
  user_agent?: string,
  headers?: dict<string, string>,
  timeout_ms?: int,
  retry_attempts?: int,
  poll_interval_ms?: int,
  max_wait_ms?: int,
}

var github_connector_config: GitHubConnectorConfig = {}

/** configure overrides the module-level connector config used by every call. */
pub fn configure(config: GitHubConnectorConfig = {}) -> GitHubConnectorConfig {
  github_connector_config = filter_nil(config)
  return github_connector_config
}

/** reset clears the module-level connector config. */
pub fn reset() {
  github_connector_config = {}
}

fn __call(method: string, params: dict = {}) {
  return connector_call("github", method, filter_nil(merge(github_connector_config, params)))
}

/** comment posts a comment on a GitHub issue or PR. */
pub fn comment(issue_url: string, body: string, options: GitHubCallOptions = {}) {
  return __call("comment", merge(options, {issue_url: issue_url, body: body}))
}

/** add_labels adds the listed labels to an issue or PR. */
pub fn add_labels(issue_url: string, labels: list<string>, options: GitHubCallOptions = {}) {
  return __call("add_labels", merge(options, {issue_url: issue_url, labels: labels}))
}

/** request_review asks the listed reviewers to review a pull request. */
pub fn request_review(pr_url: string, reviewers: list<string>, options: GitHubCallOptions = {}) {
  return __call("request_review", merge(options, {pr_url: pr_url, reviewers: reviewers}))
}

/** merge_pr merges a pull request via the API. */
pub fn merge_pr(pr_url: string, options: GitHubCallOptions = {}) {
  return __call("merge_pr", merge(options, {pr_url: pr_url}))
}

/** list_stale_prs returns PRs in `repo` that have been open for at least `days` days. */
pub fn list_stale_prs(repo: string, days: int, options: GitHubCallOptions = {}) {
  return __call("list_stale_prs", merge(options, {repo: repo, days: days}))
}

/** get_pr_diff fetches the unified diff for the pull request. */
pub fn get_pr_diff(pr_url: string, options: GitHubCallOptions = {}) {
  return __call("get_pr_diff", merge(options, {pr_url: pr_url}))
}

/** create_issue opens a new issue in `repo`, optionally with a body and labels. */
pub fn create_issue(
  repo: string,
  title: string,
  body: string? = nil,
  labels: list<string>? = nil,
  options: GitHubCallOptions = {},
) {
  return __call(
    "create_issue",
    filter_nil(merge(options, {repo: repo, title: title, body: body, labels: labels})),
  )
}

/** api_call is the escape hatch for raw GitHub REST calls. */
pub fn api_call(path: string, method: string, body: any = nil, options: GitHubCallOptions = {}) {
  return __call("api_call", filter_nil(merge(options, {path: path, method: method, body: body})))
}

fn __github_event(log_event) {
  return log_event?.payload?.event
}

fn __github_payload(log_event) {
  return __github_event(log_event)?.provider_payload
}

fn __github_repo_matches(payload, repo) {
  let full_name = payload?.raw?.repository?.full_name
  return full_name == nil || full_name == repo
}

/** deployment_status_source. */
pub fn deployment_status_source(repo: string, deployment_id: any, options: GitHubCallOptions = {}) {
  return {
    label: "github.deployment_status:" + repo + "#" + to_string(deployment_id),
    prefers_push: true,
    poll: { _ ->
      let path = "/repos/" + repo + "/deployments/" + to_string(deployment_id) + "/statuses"
      let statuses = api_call(path, "GET", nil, options)
      var latest = nil
      if len(statuses) > 0 {
        latest = statuses[0]
      }
      return {
        repo: repo,
        deployment_id: deployment_id,
        deployment_status: latest,
        state: latest?.state ?? "unknown",
      }
    },
    push_filter: { log_event ->
      let event = __github_event(log_event)
      let payload = __github_payload(log_event)
      return event?.provider == "github"
        && event?.kind == "deployment_status"
        && payload?.deployment?.id == deployment_id
        && __github_repo_matches(payload, repo)
    },
  }
}

/** check_run_source. */
pub fn check_run_source(repo: string, check_run_id: any, options: GitHubCallOptions = {}) {
  return {
    label: "github.check_run:" + repo + "#" + to_string(check_run_id),
    prefers_push: true,
    poll: { _ ->
      let check_run = api_call("/repos/" + repo + "/check-runs/" + to_string(check_run_id), "GET", nil, options)
      return {
        repo: repo,
        check_run_id: check_run_id,
        check_run: check_run,
        status: check_run?.status,
        conclusion: check_run?.conclusion,
      }
    },
    push_filter: { log_event ->
      let event = __github_event(log_event)
      let payload = __github_payload(log_event)
      return event?.provider == "github"
        && event?.kind == "check_run"
        && payload?.check_run?.id == check_run_id
        && __github_repo_matches(payload, repo)
    },
  }
}

/** pull_request_merged_source. */
pub fn pull_request_merged_source(repo: string, number: any, options: GitHubCallOptions = {}) {
  return {
    label: "github.pull_request_merged:" + repo + "#" + to_string(number),
    prefers_push: true,
    poll: { _ ->
      let pull_request = api_call("/repos/" + repo + "/pulls/" + to_string(number), "GET", nil, options)
      return {
        repo: repo,
        number: number,
        pull_request: pull_request,
        merged: pull_request?.merged ?? false,
        state: pull_request?.state,
      }
    },
    push_filter: { log_event ->
      let event = __github_event(log_event)
      let payload = __github_payload(log_event)
      return event?.provider == "github"
        && event?.kind == "pull_request"
        && payload?.pull_request?.number == number
        && __github_repo_matches(payload, repo)
    },
  }
}

/** wait_until_deploy_succeeds. */
pub fn wait_until_deploy_succeeds(repo: string, deployment_id: any, options: GitHubWaitOptions = {}) {
  return wait_for(
    merge(
      options,
      {
        source: deployment_status_source(repo, deployment_id, options),
        condition: { state -> state.state == "success" },
      },
    ),
  )
}

/** wait_until_ci_green. */
pub fn wait_until_ci_green(repo: string, check_run_id: any, options: GitHubWaitOptions = {}) {
  return wait_for(
    merge(
      options,
      {
        source: check_run_source(repo, check_run_id, options),
        condition: { state -> state.status == "completed" && state.conclusion == "success" },
      },
    ),
  )
}

/** wait_until_pr_merged. */
pub fn wait_until_pr_merged(repo: string, number: any, options: GitHubWaitOptions = {}) {
  return wait_for(
    merge(
      options,
      {source: pull_request_merged_source(repo, number, options), condition: { state -> state.merged }},
    ),
  )
}