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,
}
type GitHubRepoRef = {owner: string, name: string, full_name: string}
type GitHubAutoMergeResult = {
ok: bool,
state: string,
already_enabled: bool,
requested_method: string,
merge_method: string?,
pull_number: any,
url: string?,
strategy: string,
error: any,
}
type GitHubClosePrResult = {
ok: bool,
pull_number: any,
state: string,
comment_posted: bool,
pull_request: any,
error: any,
}
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)))
}
/** call dispatches one GitHub connector outbound method. Prefer typed helpers when one exists. */
pub fn call(method: string, args: dict = {}, options: GitHubCallOptions = {}) {
return __call(method, merge(options, args ?? {}))
}
fn __github_strip_git_suffix(value) {
let text = trim(to_string(value ?? ""))
if ends_with(text, ".git") {
return substring(text, 0, len(text) - len(".git"))
}
return text
}
fn __github_slug_match(pattern, text) {
let captures = regex_captures(pattern, text)
if len(captures) == 0 {
return nil
}
let groups = captures[0].groups
if len(groups) < 2 {
return nil
}
let owner = trim(to_string(groups[0]))
let repo = __github_strip_git_suffix(groups[1])
if owner == "" || repo == "" || contains(owner, "/") || contains(repo, "/") {
return nil
}
return owner + "/" + repo
}
/** github_slug_from_remote returns `owner/repo` for common GitHub SSH and HTTPS remote URLs. */
pub fn github_slug_from_remote(url: string) -> string? {
let text = trim(to_string(url ?? ""))
if text == "" {
return nil
}
let url_slug = __github_slug_match(
"^(?:git\\+)?(?:https?|ssh|git)://(?:[^@/]+@)?github\\.com(?::[0-9]+)?/([^/]+)/([^/#?]+)(?:[?#].*)?$",
text,
)
if url_slug != nil {
return url_slug
}
return __github_slug_match("^(?:[^@/]+@)?github\\.com:([^/]+)/([^/#?]+)(?:[?#].*)?$", text)
}
/** github_repo normalizes an `owner/repo` slug, GitHub remote URL, or owner+repo pair. */
pub fn github_repo(repo: any, name: string? = nil) -> GitHubRepoRef? {
if name != nil {
let owner = trim(to_string(repo ?? ""))
let repo_name = __github_strip_git_suffix(name)
if owner == "" || repo_name == "" {
return nil
}
return {owner: owner, name: repo_name, full_name: owner + "/" + repo_name}
}
if type_of(repo) == "dict" {
let full_name = repo?.full_name
if full_name != nil {
return github_repo(full_name)
}
let owner = repo?.owner ?? repo?.owner_login
let repo_name = repo?.name ?? repo?.repo
if owner != nil && repo_name != nil {
return github_repo(owner, repo_name)
}
return nil
}
let raw = trim(to_string(repo ?? ""))
let slug = if contains(raw, "github.com") || contains(raw, "://") {
github_slug_from_remote(raw)
} else {
raw
}
if slug == nil || !contains(slug, "/") {
return nil
}
let parts = split(slug, "/")
if len(parts) != 2 {
return nil
}
return github_repo(parts[0], parts[1])
}
fn __github_repo_or_throw(repo, name = nil) {
let parsed = github_repo(repo, name)
if parsed == nil {
throw "std/connectors/github: expected repo as owner/repo, GitHub remote URL, or owner + repo"
}
return parsed
}
/** 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})))
}
/** workflow_dispatch triggers a workflow_dispatch workflow without shelling out to `gh`. */
pub fn workflow_dispatch(
repo: any,
workflow_id: any,
ref: string = "main",
inputs: dict? = nil,
options: GitHubCallOptions = {},
) {
let r = __github_repo_or_throw(repo)
return call(
"github.actions.workflow_dispatch",
{repo: r.full_name, workflow_id: workflow_id, ref: ref, inputs: inputs ?? {}},
options,
)
}
/**
* workflow_runs lists recent workflow runs, optionally scoped by workflow_id/event/status filters.
*/
pub fn workflow_runs(repo: any, options: GitHubCallOptions = {}) {
let r = __github_repo_or_throw(repo)
return call("github.actions.runs", {repo: r.full_name}, options)
}
/** workflow_run fetches one Actions workflow run by id. */
pub fn workflow_run(repo: any, run_id: any, options: GitHubCallOptions = {}) {
let r = __github_repo_or_throw(repo)
return call("github.actions.run", {repo: r.full_name, run_id: run_id}, options)
}
/** Package-compatible owner/repo alias for workflow_dispatch. */
pub fn actions_workflow_dispatch(
owner: string,
repo: string,
workflow_id: any,
ref: string = "main",
inputs: dict? = nil,
options: GitHubCallOptions = {},
) {
return workflow_dispatch(owner + "/" + repo, workflow_id, ref, inputs, options)
}
/** Package-compatible owner/repo alias for workflow_runs. */
pub fn actions_workflow_runs(owner: string, repo: string, options: GitHubCallOptions = {}) {
return workflow_runs(owner + "/" + repo, options)
}
/** Package-compatible owner/repo alias for workflow_run. */
pub fn actions_workflow_run(
owner: string,
repo: string,
run_id: any,
options: GitHubCallOptions = {},
) {
return workflow_run(owner + "/" + repo, run_id, options)
}
/** read_file_at_ref reads a repository file as decoded text at an optional ref. */
pub fn read_file_at_ref(
repo: any,
path: string,
ref: string? = nil,
options: GitHubCallOptions = {},
) {
let r = __github_repo_or_throw(repo)
return call("repos.get_text", filter_nil({owner: r.owner, repo: r.name, path: path, ref: ref}), options)
}
/** Package-compatible owner/repo alias for read_file_at_ref. */
pub fn repos_get_text(
owner: string,
repo: string,
path: string,
ref: string? = nil,
options: GitHubCallOptions = {},
) {
return read_file_at_ref(owner + "/" + repo, path, ref, options)
}
/** latest_release returns a stable latest-release envelope with asset_names. */
pub fn latest_release(repo: any, options: GitHubCallOptions = {}) {
let r = __github_repo_or_throw(repo)
return call("github.release.latest", {repo: r.full_name}, options)
}
/**
* release_assets returns a stable asset envelope for a release id, or latest release when omitted.
*/
pub fn release_assets(repo: any, release_id: any = nil, options: GitHubCallOptions = {}) {
let r = __github_repo_or_throw(repo)
return call("github.release.assets", filter_nil({repo: r.full_name, release_id: release_id}), options)
}
/** repos_get_latest_release fetches the raw GitHub latest-release payload. */
pub fn repos_get_latest_release(owner: string, repo: string, options: GitHubCallOptions = {}) {
return call("repos.get_latest_release", {owner: owner, repo: repo}, options)
}
/** repos_list_release_assets fetches the raw GitHub release assets payload. */
pub fn repos_list_release_assets(
owner: string,
repo: string,
release_id: any,
options: GitHubCallOptions = {},
) {
return call("repos.list_release_assets", {owner: owner, repo: repo, release_id: release_id}, options)
}
/** Package-compatible owner/repo alias for latest_release. */
pub fn github_latest_release(owner: string, repo: string, options: GitHubCallOptions = {}) {
return latest_release(owner + "/" + repo, options)
}
/** Package-compatible owner/repo alias for release_assets. */
pub fn github_release_assets(
owner: string,
repo: string,
release_id: any = nil,
options: GitHubCallOptions = {},
) {
return release_assets(owner + "/" + repo, release_id, options)
}
/** enable_auto_merge enables GitHub auto-merge and normalizes the result shape. */
pub fn enable_auto_merge(repo: any, pull_number: any, options: GitHubCallOptions = {}) -> GitHubAutoMergeResult {
let r = __github_repo_or_throw(repo)
let requested_method = uppercase(to_string(options?.method ?? options?.merge_method ?? "merge"))
let result = try {
call(
"github.pr.enable_auto_merge",
{repo: r.full_name, pull_number: pull_number, method: requested_method},
options,
)
}
if is_err(result) {
return __github_auto_merge_failed(requested_method, pull_number, unwrap_err(result))
}
return __github_auto_merge_success(unwrap(result), requested_method, pull_number)
}
/** Package-compatible owner/repo alias for enable_auto_merge. */
pub fn pulls_enable_auto_merge(
owner: string,
repo: string,
number: any,
options: GitHubCallOptions = {},
) {
return enable_auto_merge(owner + "/" + repo, number, options)
}
/** Package-compatible owner/repo alias for enable_auto_merge. */
pub fn github_ensure_auto_merge(
owner: string,
repo: string,
pull_number: any,
options: GitHubCallOptions = {},
) {
return enable_auto_merge(owner + "/" + repo, pull_number, options)
}
/**
* close_pr closes a pull request through the issues endpoint, optionally after posting a comment.
*/
pub fn close_pr(
repo: any,
pull_number: any,
comment: string? = nil,
options: GitHubCallOptions = {},
) -> GitHubClosePrResult {
let r = __github_repo_or_throw(repo)
var comment_posted = false
if comment != nil && trim(to_string(comment)) != "" {
let posted = try {
call(
"issues.create_comment",
{owner: r.owner, repo: r.name, issue_number: pull_number, body: to_string(comment)},
options,
)
}
if is_err(posted) {
return __github_close_pr_envelope(false, pull_number, false, nil, unwrap_err(posted))
}
comment_posted = true
}
let updated = try {
call(
"issues.update",
{owner: r.owner, repo: r.name, issue_number: pull_number, state: "closed"},
options,
)
}
if is_err(updated) {
return __github_close_pr_envelope(false, pull_number, comment_posted, nil, unwrap_err(updated))
}
return __github_close_pr_envelope(true, pull_number, comment_posted, unwrap(updated), nil)
}
/** Package-compatible owner/repo alias for close_pr. */
pub fn github_close_pr(
owner: string,
repo: string,
pull_number: any,
comment: string? = nil,
options: GitHubCallOptions = {},
) {
return close_pr(owner + "/" + repo, pull_number, comment, options)
}
fn __github_auto_merge_failed(requested_method, pull_number, error) -> GitHubAutoMergeResult {
return {
ok: false,
state: "failed",
already_enabled: false,
requested_method: requested_method,
merge_method: nil,
pull_number: pull_number,
url: nil,
strategy: "graphql_auto_merge",
error: error,
}
}
fn __github_auto_merge_success(result, requested_method, pull_number) -> GitHubAutoMergeResult {
let state = result?.state ?? "enabled"
let merge_method = result?.requested_method ?? result?.auto_merge?.merge_method
?? result?.pull_request?.autoMergeRequest?.mergeMethod
?? requested_method
return {
ok: true,
state: state,
already_enabled: state == "already_enabled",
requested_method: requested_method,
merge_method: to_string(merge_method),
pull_number: pull_number,
url: result?.pull_request?.html_url ?? result?.pull_request?.url,
strategy: "graphql_auto_merge",
error: nil,
}
}
fn __github_close_pr_envelope(ok, pull_number, comment_posted, pull_request, error) -> GitHubClosePrResult {
return {
ok: ok,
pull_number: pull_number,
state: if ok {
"closed"
} else {
"open"
},
comment_posted: comment_posted,
pull_request: pull_request,
error: error,
}
}
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 }},
),
)
}