feat: publish the repository git-state GraphQL contract [IFC-3146]

Merged
OG
2mo ago
+3077-130

Jira: IFC-3146 · Feature: IFC-3101

Targets the feature branch pog-repo-commit-visibility-ifc-3101, not develop. This is PR 2 of the eleven in dev/specs/ifc-3101-repo-commit-visibility/tasks.md; the develop catch-up belongs on the feature branch once the sub-PRs have landed.

What this publishes

The whole read surface for a repository's git state, at once — answering everything Infrahub can answer from the graph, and reporting an honest unavailable state for everything that needs a worker's clone.

  • InfrahubRepositoryCommits — paged, newest-first commit log for the request branch, carrying the branch, the remote branch or tracked ref it resolves, and the commit Infrahub has imported. limit bounded to 1..100, offset to >= 0, and pending_count requested only when selected.
  • InfrahubRepositoryBranchDrift — answers with the repository id and its own column-level unavailable state. Rows need a per-branch tracked commit, which arrives with the per-branch graph query in PR 3.
  • InfrahubReadOnlyRepositoryCheckRefs — submits the on-demand refs check.

Both queries enforce repository view permission imperatively on the concrete kind, because InfrahubGraphQLQueryAnalyzer maps top-level fields to kinds by exact name and cannot see a custom query. The mutation gates on update permission for the read-only kind and refuses a read-write repository.

Why this is the hand-off point. Nothing in the frontend reads a live server schema — it types against the checked-in schema/schema.graphql through gql.tada and graphql-codegen. The moment that file carries the new SDL, the frontend team is unblocked, whether or not any server can yet answer with commits. UNAVAILABLE / NOT_IMPLEMENTED is a state the UI has to handle regardless.

Nothing here writes to the graph: no schema change, no migration, and no write to commit or ref anywhere.

Structure

RepositoryGitStateReader is a protocol with one dependency-free implementation, chosen in a single factory. Swapping it for the worker-backed reader in PR 5 is that factory's body and nothing else — no resolver, test or consumer change. The recording and failing doubles under backend/tests/helpers/ are its other two implementations.

The remote-branch mapping rule moves out of InfrahubRepositoryBase._get_mapped_remote_branch into infrahub.git.branch_mapping as a pure function with all three inputs required, and the method delegates to it with no behaviour change. The API server cannot call a private instance method and must never build a repository object; restating the rule in the resolver would leave two copies to drift on precisely the default-branch case INFP-670 is fixing.

Three WorkflowDefinitions are registered with bodies that log and return — the repository warm-up, the read-only refs-check cycle, and its per-repository run. Their real bodies come with PRs 5 and 8.

Decisions a reviewer would otherwise query

All of these are recorded against their task in tasks.md.

  • config.OVERRIDE.repository_git_state_reader is the test injection seam, checked first in the factory alongside the existing message_bus / cache / workflow entries. It is what lets the component tests inject doubles with no patching.
  • The drift resolver does not call the reader — it short-circuits to its own NOT_IMPLEMENTED column state. T014's text says to delegate; T048 requires the short-circuit, and T048 is right: with no per-branch tracked commit until PR 3, a request built from the request branch's values would carry the wrong commit for every other branch, and BusRepositoryGitStateReader.branch_heads does not exist until PR 10.
  • FailingRepositoryGitStateReader takes the exception to raise as a constructor argument rather than hard-coding WorkerTimeoutError, which arrives with PR 4.
  • The mutation's payload type prints as ReadOnlyRepositoryCheckRefs, following ReadOnlyRepositoryImportLastCommit, rather than the InfrahubReadOnlyRepositoryCheckRefs the contract sketches. The field on Mutation keeps the Infrahub prefix.
  • The mutation submits {"repository_id": ...} for now; the GitReadOnlyRepositoryCheckRefs model and the tracked refs it carries belong to PR 8.
  • The changelog fragment carries the + orphan prefix. Without it towncrier renders the filename as a GitHub issue link, so a bare ifc-3101-….added.md would ship a link to an issue that does not exist.
  • git/state/cache_keys.py defines two TTL constants, not three. The due key's TTL is the configured interval and the in-flight ceiling is the per-repository timeout plus a margin — both derived from values PR 8 introduces.

Governance

The additive GraphQL surface was signed off on 2026-09-04 (recorded in plan.md). The git/branch_mapping.py extraction is an explicit override of the PRD's "without refactoring it" and lands on a file INFP-670 restructures; approved on 2026-09-09 by the owner of both this feature and that epic, with the default_branch property's or registry.default_branch fallback deliberately left in place to stay clear of that epic's P1.

Verification

Green: invoke format, invoke lint (ruff + ty + mypy + yamllint), ruff check . --exclude python_sdk, 2506 backend unit tests, the 27 new component tests, docs.validate, docs.lint, schema.validate-graphqlschema, schema.validate-jsonschema, backend.validate-generated, uv lock --check on both projects, frontend biome ci / knip / betterer ci, and codegen:graphql with a clean diff.

Phase A of quickstart.md was walked against a live API server and infrahubasync worker from this branch. Steps 2, 3 (branch-switch half) and 5 hold; step 4's expectation was wrong and is amended in the same commit — the denial matches querying CoreRepository directly in code, status and the permission named, but not in wording, because these resolvers check one permission while the analyzer batches every kind and denies in the plural. A step 6 was added recording that the paging refusals surface as UNDEFINED_ERROR / 422, since ValidationError has no catalogue entry anywhere in the codebase.

Known-unrelated CI noise

backend/tests/component/git/test_git_repository.py::test_compare_remote_local_new_branches and ::test_get_filtered_remote_branches__no_import_sync_branch_names fail locally on this branch. They are pre-existing: both reproduce unchanged with the pre-change backend/infrahub/git/base.py restored, and the only edit here on that file is a behaviour-preserving delegation off the get_branches_from_remote code path. Not introduced by this PR, and not fixed by it either.