repomatic.github.gh module¶
Generic wrapper for the gh CLI.
Note
Workflow steps must set GH_TOKEN explicitly: GITHUB_TOKEN is a
secret expression in GitHub Actions, not an automatic environment variable.
The standard pattern is GH_TOKEN: ${{ secrets.REPOMATIC_PAT || github.token }}
for steps that prefer a PAT, or GH_TOKEN: ${{ github.token }} otherwise.
As defense-in-depth, run_gh_command() promotes REPOMATIC_PAT to
GH_TOKEN when set, and promotes GITHUB_TOKEN to GH_TOKEN when
GH_TOKEN is absent. A Requires authentication 401 (a GitHub-side
auth incident or a fine-grained PAT scope quirk) is first retried with
the same token after a short back-off, catching transient flaps that
clear on their own. A Bad credentials 401 skips that wait: an expired
or revoked PAT never recovers on a retry. Either then falls back to
GITHUB_TOKEN if available and different. When every retry path is
exhausted, the raised GhCommandError is annotated with the current
githubstatus.com summary so operators
are not sent chasing PAT scopes during an upstream incident.
- exception repomatic.github.gh.GhCommandError(message, *, stdout='', stderr=None)[source]¶
Bases:
RuntimeErrorA
ghcommand that still failed after every retry.The message is the command’s error output, with the githubstatus.com annotation when an incident is active. The attributes keep what a caller branches on, so no caller has to search the message for an HTTP status.
- stdout¶
What the command wrote to its standard output before it failed.
gh api graphqlwrites the partialdatahere when a query fails only in part, seegh_graphql().
- stderr¶
The command’s standard error, without the status annotation.
Defaults to message, which is the error output when no annotation was added to it.
- exception repomatic.github.gh.GhOutputError[source]¶
Bases:
RuntimeErrorA
ghcommand that ran, but printed something other than the JSON asked for.Kept apart from
GhCommandError: a caller may treat a command that could not run as fatal and an unreadable answer as a soft miss.
- repomatic.github.gh.gh_executable() str[source]¶
Resolve the
ghbinary every call in this package shells out to.Prefers the registry-pinned build over whatever
$PATHoffers, which is the rule for any tool repomatic shells out to: the registry pin carries a version, a checksum and a cooldown, while$PATHcarries none of the three and hands each runner image (and each developer laptop) a differentgh.Caution
Falls back to bare
ghon$PATHwhen the registry build cannot be obtained. Unlike a formatter,ghis on the critical path of jobs that have already done real work (a release publish, an issue upsert), so a download failure must not strand them: a hosted runner ships a usablegh, and degrading to it beats failing the job. The fallback is logged, never silent.Memoized: the install-and-verify path runs once per process however many of the ~60 call sites fire.
- Return type:
- repomatic.github.gh.resolve_gh_token()[source]¶
Return the GitHub token from environment variables.
The canonical lookup order for every GitHub API access in the package:
REPOMATIC_PAT>GH_TOKEN>GITHUB_TOKEN. Empty string when no variable is set.- Return type:
- repomatic.github.gh.api_headers()[source]¶
Build GitHub API request headers, authenticated when a token is present.
The one place a direct HTTP call to the GitHub API gets its headers, so every such call agrees on the
Acceptmedia type and the authentication scheme.Beareris the scheme GitHub documents, and it carries both classic and fine-grained tokens.A token raises the rate limit from 60 to at least 1,000 requests/hour, which matters when iterating every tool and action in CI. Resolution follows the canonical
resolve_gh_token()order, so a repo carrying onlyREPOMATIC_PATgets authenticated reads here too, not just through theghCLI.
- repomatic.github.gh.gh_env(token=None)[source]¶
Child environment promoting a token to
GH_TOKEN, orNonefor none.The one spelling of the promotion every
gh-reading subprocess gets (ghitself, andlabelmaker, which reads the same variable): the canonicalresolve_gh_token()winner is injected asGH_TOKEN, a value-preserving no-op whenGH_TOKENitself won the resolution.
- repomatic.github.gh.run_gh_command(args)[source]¶
Run a
ghCLI command and return stdout.Token priority:
REPOMATIC_PAT>GH_TOKEN>GITHUB_TOKEN. TheghCLI does not recognizeREPOMATIC_PAT, so when set it is injected asGH_TOKEN. ARequires authentication401 from the primary token is first retried with the same token after a short bounded back-off (see_TRANSIENT_AUTH_BACKOFF_SECONDS), absorbing transient GitHub auth flaps that resolve on their own; a Bad credentials 401 skips straight past it, since a revoked or expired token cannot clear on a retry. A secondary rate-limit refusal gets the same treatment on its own, longer schedule (see_TRANSIENT_THROTTLE_BACKOFF_SECONDS), since it lifts on its own within the minute. If 401s persist, the command is then retried withGITHUB_TOKENif available and different, letting CI jobs degrade gracefully to the standard Actions token instead of failing outright on a stale PAT. When every retry path is exhausted, the raisedGhCommandErrorcarries a githubstatus.com annotation when an incident is active.- Parameters:
- Return type:
- Returns:
The stdout output from the command.
- Raises:
GhCommandError – If the command fails (after retries and fallback, if attempted).
- repomatic.github.gh.parse_create_output(output, kind)[source]¶
Read the number and URL of a thread out of
gh {kind} createoutput.gh issue createandgh pr createboth print the new thread’s URL last, in the formhttps://github.com/owner/repo/{issues,pull}/123. Read the last line rather than the whole output:ghprepends advisory lines of its own (a deprecation notice, a “Warning: N uncommitted changes” banner), and parsing the joined output turns one of those into an error that reads as a failed creation when the thread was in fact created.
- repomatic.github.gh.gh_json(args, *, soft=False)[source]¶
Run a
ghcommand that prints JSON, and return the parsed value.The one entry point for every
ghcall whose output is JSON: an API read (gh api) as well as a JSON listing (gh release view --json).- Parameters:
- Return type:
- Returns:
The parsed JSON value, or
Noneon a failure when soft is set.- Raises:
GhCommandError – When the command fails and soft is not set.
GhOutputError – When the output is not JSON and soft is not set.
- repomatic.github.gh.gh_api_pages(endpoint, *, key='', soft=False)[source]¶
Read every page of a paginated REST endpoint, and return their items.
gh api --paginate --slurpfollows theLinkheaders and wraps every page in one JSON array, so the pages parse at once rather than line by line.- Parameters:
endpoint (
str) – The REST endpoint, query string included. Sendper_pagein it to keep the page count low.key (
str) – The field holding the items, for an endpoint that wraps them in an object, likeworkflow_runsfor the runs endpoints. Empty for an endpoint that answers with an array.soft (
bool) – Return an empty list when the command fails or prints something that is not JSON, asgh_json()does.
- Return type:
- Returns:
The items of every page, in API order.
- Raises:
GhCommandError – When the command fails and soft is not set.
GhOutputError – When the output is not JSON and soft is not set.
- repomatic.github.gh.gh_api_json(args, *, strict=False)[source]¶
Run a
ghcommand expected to emit JSON, and parse it.A failed command and an unparsable output both read as
None, so a caller reports one “could not read it” outcome instead of two it cannot act on differently. With strict, a failed command raises instead, while an unparsable output stays a soft miss (ci-statusreads runs this way).A thin layer over
gh_json(), which new code calls directly.
- repomatic.github.gh.gh_graphql(query, *, partial=False, **variables)[source]¶
Run a one-shot GraphQL query through
gh, and return itsdataenvelope.The one-shot sibling of
iter_graphql_nodes(), for the queries that read a handful of fields off a single object rather than walking a connection.- Parameters:
query (
str) – The GraphQL query string.partial (
bool) – Accept a partial answer. GitHub answers a query that fails only in part with thedatait could resolve, beside itserrors, andghthen exits non-zero. With partial, thatdatais returned when the failed command printed one: the fields that failed read asnull.variables (
str|int|bool) – Query variables. See_graphql_argsfor how each type travels.
- Return type:
- Returns:
The response’s
dataobject, unwrapped.- Raises:
GhCommandError – When the
ghinvocation fails (seerun_gh_command()), unless partial is set and the output still holds adataobject.GhOutputError – When the output is not JSON.
- repomatic.github.gh.iter_graphql_nodes(query, connection_path, variables=None, *, page_size_var='', page_size=0)[source]¶
Iterate a GraphQL connection’s nodes, following cursor pagination.
The shared
gh api graphqlpagination loop: run the query, walk the response to the connection object, yield each node, then followpageInfo.hasNextPage/endCursoruntil the connection is exhausted. The query must declare a$cursor: Stringvariable and pass it asafter: $cursor, and its connection must selectpageInfo { hasNextPage endCursor }.Null nodes (which GitHub’s
searchconnection can emit) are skipped.- Parameters:
query (
str) – The GraphQL query string.connection_path (
Sequence[str]) – Keys from the response’sdataobject down to the connection (like("search",)or (“user”, “sponsorshipsAsMaintainer”)).variables (
Mapping[str,str|int|bool] |None) – Query variables. See_graphql_argsfor how each type travels.page_size_var (
str) – When set, inject the page size into this query variable on every request (the query then controlsfirst:with it). Leave empty for queries with a hard-coded page size.page_size (
int) – Nodes requested per page; only used with page_size_var.
- Yields:
Each node dict, in API order.
- Raises:
GhCommandError – When a
ghinvocation fails (seerun_gh_command()).GhOutputError – When a page is not JSON.
