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 RuntimeError is annotated with the current
githubstatus.com summary so operators
are not sent chasing PAT scopes during an upstream incident.
- 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 ruleclaude.md§ “A cooldown is not a hash” states 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 raisedRuntimeErrorcarries a githubstatus.com annotation when an incident is active.- Parameters:
- Return type:
- Returns:
The stdout output from the command.
- Raises:
RuntimeError – 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_api_json(args, *, strict=False)[source]¶
Run a
ghcommand expected to emit JSON, and parse it.The two ways a JSON-producing
ghcall can fail are indistinguishable to a caller that just wants the payload: the command may not run at all (network, auth, a404on an endpoint the repository has not enabled) or it may return something that is not JSON. Both collapse toNonehere, so a caller reports one “could not read it” outcome instead of two it cannot act on differently.Reserved for calls whose failure is a tolerable outcome, which is what every
repomatic.lint_repocheck wants: a probe that cannot run reports itself as skipped rather than failing the lint. A caller for whom a failed command is fatal while unparsable output stays a soft miss passes strict (ci-statusreads runs this way); one treating every failure as fatal keeps usingrun_gh_command()and handlesRuntimeErroritself.- Parameters:
- Return type:
- Returns:
The parsed JSON payload, or
Nonewhen the command failed (unless strict) or its output did not parse.
- repomatic.github.gh.gh_graphql(query, **variables)[source]¶
Run a one-shot GraphQL query through
gh, and return itsdataenvelope.The paginated sibling of
iter_graphql_nodes(), for the queries that read a handful of fields off a single object rather than walking a connection. The query travels as a raw field, since--fieldreads a value looking like a number or a boolean as one, which would corrupt a query string that happens to start with a digit.- Parameters:
- Return type:
- Returns:
The response’s
dataobject, unwrapped.- Raises:
RuntimeError – When the
ghinvocation fails (seerun_gh_command()).
- repomatic.github.gh.iter_graphql_nodes(query, connection_path, variables=None, *, page_size_var='', page_size=0, max_nodes=None)[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. Strings are passed with-f; ints and bools with-F, so they keep their GraphQL type.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. The last page shrinks to the max_nodes remainder so the budget is never over-fetched.max_nodes (
int|None) – Stop after yielding this many nodes.Nonemeans every node in the connection.
- Yields:
Each node dict, in API order.
- Raises:
RuntimeError – When a
ghinvocation fails (seerun_gh_command()).