repomatic.lint_repo.settings module

Checks of the repository settings the setup guide asks for.

The Actions policies and the immutable releases setting each read one endpoint, through a RepoSettingProbe. The rulesets and the branch protection rules decide how the default branch is protected. The PyPI Trusted Publisher entry, the repository secrets and the REPOMATIC_PAT permissions complete the list.

Note

Most of these endpoints need the Administration: Read-only permission. A token without it gets a 403, which a check reports as skipped, never as a failure.

class repomatic.lint_repo.settings.RepoSettingProbe(label, endpoint, field, pass_message, fail_message, passing=None, failing=frozenset({}), unknown='unknown value {value!r}', unavailable='could not query API')[source]

Bases: object

One repository setting to read, and how its values map to verdicts.

The repository-setting checks share one shape: read one API endpoint through gh, inspect one field, and map its value onto a pass, a failure naming the settings page, or an indeterminate skip. What varies is declared here, and check() runs it. A probe with no passing set reads its field as a boolean toggle and never skips on an unknown value.

label: str

Check name opening every skip message.

endpoint: str

API path under repos/{repo}/ to read.

field: str

Response field carrying the setting.

pass_message: str

Success message; {value} interpolates the field’s value.

fail_message: str

Failure message; {repo} interpolates the repository slug.

passing: frozenset[str] | None = None

Values that pass; None reads the field as a boolean toggle.

failing: frozenset[str] = frozenset({})

Values that fail explicitly; anything in neither set skips.

unknown: str = 'unknown value {value!r}'

Skip reason for a value in neither set; {value} interpolates it.

unavailable: str = 'could not query API'

Skip reason when the endpoint cannot be read.

check(repo)[source]

Read this setting on repo.

Parameters:

repo (str) – Repository in owner/repo format.

Return type:

CheckResult

Returns:

A CheckResult. passed is None when the check could not run.

repomatic.lint_repo.settings.FORK_PR_APPROVAL_POLICY: Final = RepoSettingProbe(label='Fork PR approval policy check', endpoint='actions/permissions/fork-pr-contributor-approval', field='approval_policy', pass_message='Fork PR approval policy: {value}.', fail_message="Fork PR approval policy is 'first_time_contributors_new_to_github', which only catches brand-new GitHub accounts. Set it to 'first_time_contributors' (or stricter) under https://github.com/{repo}/settings/actions to require approval for any first-time contributor.", passing=frozenset({'all_external_contributors', 'first_time_contributors'}), failing=frozenset({'first_time_contributors_new_to_github'}), unknown='unknown policy {value!r}', unavailable='could not query API')

The fork PR approval policy, read by check_fork_pr_approval_policy().

repomatic.lint_repo.settings.SHA_PINNING_REQUIRED: Final = RepoSettingProbe(label='SHA pinning required check', endpoint='actions/permissions', field='sha_pinning_required', pass_message='SHA pinning required: enabled.', fail_message='SHA pinning is not required for GitHub Actions in this repository. Enable it under https://github.com/{repo}/settings/actions (Actions permissions → Require actions to be pinned to a full-length commit SHA) so GitHub rejects any unpinned action reference, not just the ones zizmor happens to catch.', passing=None, failing=frozenset(), unknown='unknown value {value!r}', unavailable='could not query API')

The sha_pinning_required policy, read by check_sha_pinning_required().

repomatic.lint_repo.settings.IMMUTABLE_RELEASES: Final = RepoSettingProbe(label='Immutable releases check', endpoint='immutable-releases', field='enabled', pass_message='Immutable releases are enabled.', fail_message='Immutable releases are not enabled.', passing=None, failing=frozenset(), unknown='unknown value {value!r}', unavailable='could not query API')

The immutable releases setting, read by check_immutable_releases().

repomatic.lint_repo.settings.BRANCH_PROTECTION_RULES_QUERY = '\nquery($owner: String!, $name: String!) {\n  repository(owner: $owner, name: $name) {\n    branchProtectionRules(first: 100) {\n      nodes { pattern }\n    }\n  }\n}\n'

Lists every branch protection rule a repository declares, by branch pattern.

GraphQL because REST cannot answer the question. GET /repos/{repo}/branches/{branch}/protection reads one concrete branch, so finding a rule needs the branch name up front and still misses any pattern targeting a branch that does not exist yet. This field enumerates the rules themselves.

The field also draws the line the check depends on: a ruleset never appears here, and a branch protection rule never appears under rulesets. Verified on 2026-08-27 against a repository holding both at once, which listed the branch protection rule here and the ruleset only there.

repomatic.lint_repo.settings.check_fork_pr_approval_policy(ctx)[source]

Check that fork PR workflows require approval for first-time contributors.

GitHub Actions has a per-repository policy that controls when workflows from fork pull requests must be approved by a maintainer before they run. The three values, from weakest to strongest, are first_time_contributors_new_to_github, first_time_contributors, and all_external_contributors.

The default (first_time_contributors_new_to_github) only catches brand-new GitHub accounts, which is trivial to bypass with a slightly aged account. The minimum acceptable setting is first_time_contributors, which requires approval for any first-time contributor to this repository. This is one of the mitigations recommended in Astral’s open-source security post: see https://astral.sh/blog/open-source-security-at-astral.

Note

The endpoint needs the Administration: Read-only permission. When the REPOMATIC_PAT lacks it, or the API call fails for any other reason, the check reports an indeterminate result rather than a negative one.

Return type:

CheckResult

repomatic.lint_repo.settings.check_sha_pinning_required(ctx)[source]

Check that GitHub Actions must be pinned to a full-length commit SHA.

GitHub has a per-repository policy, sha_pinning_required, that makes the platform itself refuse to run any workflow referencing an action by a mutable tag or branch instead of a commit SHA. repomatic already pins every action it generates and checks unpinned refs with zizmor (check_inline_pins_match_upstream() and the lint-zizmor job), but a zizmor finding can be silenced inline (# zizmor: ignore[...]), so a hand-edited workflow could still slip a mutable tag past review. This repository setting is the platform-enforced backstop.

Note

The endpoint needs the Administration: Read-only permission. When the REPOMATIC_PAT lacks it, or the API call fails for any other reason, the check reports an indeterminate result rather than a negative one.

Return type:

CheckResult

repomatic.lint_repo.settings.check_tag_protection_rules(ctx)[source]

Check that no tag ruleset can block the create-tag workflow job.

A tag ruleset that restricts creation or requires status checks can stop REPOMATIC_PAT (or GITHUB_TOKEN) from pushing release tags. The check warns about any active ruleset that targets tags.

Return type:

CheckResult

repomatic.lint_repo.settings.check_branch_ruleset_on_default(ctx)[source]

Check that at least one active branch ruleset exists.

Reads the same rulesets as check_tag_protection_rules(), and looks for an active ruleset with target == "branch". The presence of any such ruleset is taken as evidence that the default branch is protected (restrict deletions and block force pushes).

Note

This is a heuristic: it does not verify the ruleset targets the default branch specifically, nor that it enables the exact rules the setup guide recommends. A deeper check would read each ruleset’s conditions through GET /repos/{repo}/rulesets/{id}, adding N+1 API calls.

Return type:

CheckResult

Returns:

A CheckResult. passed is None when the rulesets API could not be read, matching check_tag_protection_rules(), which reads the same payload.

repomatic.lint_repo.settings.check_classic_branch_protection(ctx)[source]

Check that no branch protection rule survives beside the rulesets.

Branch protection rules predate rulesets and GitHub still supports both, with no deprecation notice and no removal date. They are not alternatives a repository picks between: both apply at once, and where the two carry the same rule, the stricter version wins. A rule left behind after a migration therefore changes nothing on the day it is left, which is what makes it worth reporting: the branch policy is now split across two settings pages, and an edit to one page does not show on the other.

check_branch_ruleset_on_default() states the other half of the same policy, that a ruleset must exist. Together they say the protection is a ruleset and only a ruleset.

Note

Advisory, not fatal. A leftover rule protects the branch rather than exposing it, so the finding is a cleanup.

Return type:

CheckResult

Returns:

A CheckResult. passed is None when the API could not be read, matching the ruleset checks. Reading the rules needs admin on the repository, so a token without it skips rather than passes.

repomatic.lint_repo.settings.check_immutable_releases(ctx)[source]

Check that immutable releases are enabled for the repository.

Note

The endpoint needs the “Administration: Read-only” permission on fine-grained PATs. When the REPOMATIC_PAT lacks it, the check reports an indeterminate result rather than a negative one.

Return type:

CheckResult

repomatic.lint_repo.settings.check_pypi_trusted_publisher(ctx)[source]

Check that the PyPI Trusted Publisher entry is registered for this repo.

PyPI’s Trusted Publisher settings are owner-only at /manage/project/<name>/settings/publishing/ and not exposed through any public API. The only public surface where the OIDC publisher is observable is the PEP 740 provenance attached to releases uploaded via OIDC: see repomatic.pypi.get_trusted_publishers(). This check probes the latest release’s provenance and looks for a bundle whose repository matches the repository and whose workflow is PYPI_TRUSTED_PUBLISHER_WORKFLOW. A match means the publisher is wired up and a previous release uploaded successfully through it. A mismatch (provenance exists but names a different repo or workflow) is a misconfiguration: typical cause is registering the upstream reusable workflow instead of the downstream caller’s release.yaml, which fails on the first upload after migration. Indeterminate (None) covers two cases that look identical from the outside: no published release yet, and provenance missing because past releases were uploaded via API token. In both cases the setup guide nags until the next OIDC-attested upload appears.

Return type:

CheckResult

repomatic.lint_repo.settings.check_virustotal_secret(ctx)[source]

Report whether the VirusTotal API key is available to release builds.

Return type:

CheckResult

repomatic.lint_repo.settings.check_notifications_secret(ctx)[source]

Report whether the unsubscribe workflow has the token it needs.

Return type:

CheckResult

repomatic.lint_repo.settings.check_pat_permissions(ctx)[source]

Yield one result per PAT permission probe, or a skip note without a PAT.

The probes are the rows of PAT_PERMISSION_PROBES. They need a repository to read, so a PAT with no repository yields nothing.

Return type:

Iterator[CheckResult]

repomatic.lint_repo.settings.check_pat_scope(ctx)[source]

Check that the PAT reaches no other repository of the same owner.

Reads the probe of repomatic.github.token.check_pat_repository_scope(), which the setup guide reads too.

Return type:

CheckResult