repomatic.github.token module¶
GitHub token validation, and the probes of what REPOMATIC_PAT can do.
The validators run before a command that depends on the GitHub API, so a user
gets a clear error at startup rather than an opaque failure mid-run. The probes
read the permissions and the repository scope of the configured
REPOMATIC_PAT: lint-repo reports what they find, and setup-guide asks the
reader to fix it.
Note
Why REPOMATIC_PAT is needed
GitHub’s GITHUB_TOKEN cannot modify workflow files in .github/. Neither
contents: write, actions: write, nor permissions: write-all grant this
ability. The only way to push changes to workflow YAML files is through a
fine-grained Personal Access Token with the Workflows permission. Without
it, GitHub rejects the push:
! [remote rejected] branch_xxx -> branch_xxx (refusing to allow a GitHub App to create or update workflow
.github/workflows/my_workflow.yamlwithoutworkflowspermission)
Events triggered by GITHUB_TOKEN also start no new workflow runs (see
GitHub docs),
so tag pushes need the PAT to trigger downstream workflows too.
The Settings → Actions → General → Workflow permissions setting has no effect on this limitation: it is a hard security boundary that GitHub enforces whatever the repository settings say.
The permission has to reach the actual git push, not just the gh CLI.
Setting GH_TOKEN in a step’s env only authenticates the gh and
repomatic API calls of that process. A bare git push
(git_ops.force_push_branch, which every pr-sync template goes through)
authenticates with the credentials actions/checkout configured for the job,
which default to github.token whatever GH_TOKEN says. A job whose diff can
touch .github/workflows/ needs
token: ${{ secrets.REPOMATIC_PAT || github.token }} on its own checkout step
too, or the push is rejected as if the PAT had never been set.
Every job falls back to GITHUB_TOKEN when the PAT is unavailable
(secrets.REPOMATIC_PAT || github.token), and then silently fails any
operation that needs the workflows permission or that must trigger a workflow.
Token permission mapping:
Workflows: PRs that touch
.github/workflows/files.Contents: tag pushes, release publishing, PR branch creation.
Pull requests: every PR-creating job.
Dependabot alerts:
fix-vulnerable-depsreads vulnerability alerts.Issues: the setup guide issue.
Administration: reads the Actions settings the setup guide verifies, SHA pinning required and the fork-PR contributor-approval policy. Read-only: repomatic never writes a repository setting.
Metadata: required for every fine-grained token API operation.
- class repomatic.github.token.PatProbe(field, permission, endpoint, success, not_found='')[source]¶
Bases:
NamedTupleOne fine-grained PAT permission probe.
A read-only API call whose
HTTP 403unambiguously identifies the missing fine-grained permission. Rows live inPAT_PERMISSION_PROBES.Create new instance of PatProbe(field, permission, endpoint, success, not_found)
- field: str¶
The key of this probe’s result in
PatPermissionResults.
- not_found: str¶
Message template (with
{repo}) when the probe 404s.Empty for probes whose 404 carries no special meaning: those fall through to the generic failure classification.
- repomatic.github.token.PAT_PERMISSION_PROBES: tuple[PatProbe, ...] = (('administration', 'Administration: Read-only', 'repos/{repo}/actions/permissions', 'Administration: token has access', ''), ('contents', 'Contents: Read and Write', 'repos/{repo}/contents/.github', 'Contents: token has access', ''), ('issues', 'Issues: Read and Write', 'repos/{repo}/issues?per_page=1&state=all', 'Issues: token has access', ''), ('pull_requests', 'Pull requests: Read and Write', 'repos/{repo}/pulls?per_page=1&state=all', 'Pull requests: token has access', ''), ('vulnerability_alerts', 'Dependabot alerts: Read-only', 'repos/{repo}/dependabot/alerts?per_page=1', 'Dependabot alerts: token has access, alerts enabled', 'Vulnerability alerts are not enabled on the repository. Enable them: gh api repos/{repo}/vulnerability-alerts --method PUT'), ('workflows', 'Workflows: Read and Write', 'repos/{repo}/actions/workflows?per_page=1', 'Workflows: token has access', ''))¶
The PAT permission probes, in report order.
- class repomatic.github.token.PatPermissionResults(dict=None, /, **kwargs)[source]¶
Bases:
UserDict[str,CheckResult]The result of each PAT permission probe, keyed by
PatProbe.field.Built by
check_all_pat_permissions(), in the order ofPAT_PERMISSION_PROBES.
- repomatic.github.token.check_all_pat_permissions(repo)[source]¶
Run every PAT permission probe against repo.
The single entry point for PAT permission validation. Both
lint-repoandsetup-guidecall it, so a new row ofPAT_PERMISSION_PROBESreaches every consumer.- Parameters:
repo (
str) – Repository inowner/repoformat.- Return type:
- Returns:
The result of each probe.
- repomatic.github.token.PAT_SCOPE_PROBE = ('administration', 'Administration: Read-only', 'repos/{repo}/actions/permissions', 'Administration: token has access', '')¶
The permission probe
check_pat_repository_scope()sends to other repositories.The endpoint that reports a token’s own repository selection,
GET /installation/repositories, refuses a personal access token. So the check reads what the token is allowed to do. A token gets a 2xx from this endpoint only for a repository it covers withAdministration: Read-only. An answer from another repository then proves the token covers it.No other row of
PAT_PERMISSION_PROBEScan do that. A public repository answers the contents, issues, pull requests and workflows endpoints to any caller. The Dependabot alerts endpoint answers404wherever alerts are off, which says nothing about the token. Read on 2026-10-13: this endpoint answers401to a caller with no token, and403to an account that does not administer the repository.The
permissionsobject thatGET /repos/{repo}returns cannot serve either: it describes the account, not the token. Checked on 2026-10-20: a token limited to one repository gotpush: truethere for another repository of the same owner.
- repomatic.github.token.PAT_SCOPE_PROBE_LIMIT = 10¶
How many other repositories
check_pat_repository_scope()probes.Each probe is one API call, made on every
lint-repoandsetup-guiderun. The check takes the owner’s most recently pushed repositories: a token shared between repositories serves the active ones, and a token with access to all of them gets an answer from the first one probed.
- repomatic.github.token.check_pat_repository_scope(repo)[source]¶
Check that the PAT has no access to another repository of the same owner.
A fine-grained PAT should name this repository alone under Only select repositories, to follow the principle of least privilege. The check sends
PAT_SCOPE_PROBEto the owner’sPAT_SCOPE_PROBE_LIMITmost recently pushed other repositories. The first one that answers fails the check.Each probe splits three ways:
2xx: the token covers that repository too. Fail, and name it.
HTTP 403: the token is refused there. The check passes once a probe was refused and none answered.
Anything else (404, 5xx, network): indeterminate for that repository.
Note
Only a 2xx fails the check, so it never reports a token limited to this repository. A pass is narrower than it reads: it covers the probed repositories, and
/users/{owner}/reposlists the public ones only. A token is also refused wherever its account has no admin role, whatever the token covers.- Parameters:
repo (
str) – Repository inowner/repoformat.- Return type:
- Returns:
A
CheckResult.
- repomatic.github.token.validate_gh_token_env()[source]¶
Check that a GitHub token environment variable is set.
Lookup order:
REPOMATIC_PAT>GH_TOKEN>GITHUB_TOKEN, matchingrun_gh_command.- Raises:
RuntimeError – If no variable is set.
- Return type:
- repomatic.github.token.GITHUB_RATE_LIMIT_URL = 'https://api.github.com/rate_limit'¶
The GitHub API endpoint
validate_gh_api_access()reads.Any authenticated caller may read it, and reading it costs no rate limit.
- repomatic.github.token.validate_gh_api_access()[source]¶
Smoke-test the GitHub API and return the response headers.
Calls
GET https://api.github.com/rate_limitwith the token from environment variables, throughrequest(). The headers are what a caller reads:X-OAuth-Scopesis what tells a classic PAT apart from a fine-grained one.- Return type:
- Returns:
The response headers, keyed by lowercase name.
- Raises:
RuntimeError – If the API returns a 4xx/5xx status, or cannot be reached at all (network error, timeout).
- repomatic.github.token.validate_classic_pat_scope(required_scope)[source]¶
Validate that the GitHub token is a classic PAT with the required scope.
Checks:
A GitHub token environment variable is set.
GitHub API is reachable (smoke-test GET).
Token is a classic PAT (has
X-OAuth-Scopesheader).Token has the required scope.
- Parameters:
required_scope (
str) – The OAuth scope to require (like"notifications").- Return type:
- Returns:
The full list of scopes on the token.
- Raises:
RuntimeError – If any check fails.
- repomatic.github.token.validate_notifications_token()[source]¶
Check the token can read the notifications API, and warn when it can do more.
The unsubscribe command needs a classic PAT holding the
notificationsscope, pervalidate_classic_pat_scope(). A token holding other scopes works too, and gets a warning: least privilege asks for that scope alone.- Raises:
RuntimeError – If the token is missing, unreadable, fine-grained, or lacks the scope.
- Return type:
