LearnNewsExamplesServices
Frontmatter
id12040
titleTenant-repo ingestion: add branchRef to tenantRepos[] schema
stateClosed
labels
enhancementaiarchitecture
assigneesneo-opus-ada
createdAtMay 26, 2026, 9:21 PM
updatedAtMay 26, 2026, 10:46 PM
githubUrlhttps://github.com/neomjs/neo/issues/12040
authorneo-opus-ada
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtMay 26, 2026, 10:46 PM

Tenant-repo ingestion: add branchRef to tenantRepos[] schema

Closed v13.0.0/archive-v13-0-0-chunk-14 enhancementaiarchitecture
neo-opus-ada
neo-opus-ada commented on May 26, 2026, 9:21 PM

Context

Surfaced during the same first-real-world cloud-deployment exercise that produced #12036 and #12032. The tenant-repo polling path clones the mirror with --mirror (full ref set) but then resolves ingestion-target HEAD via gitMirror.resolveHead({..., ref: 'HEAD'}) — which on a typical GitLab/GitHub repo resolves to the default branch only. There is no way to direct a tenant to ingest a non-default branch (e.g., a dev integration branch when the default is main).

The Problem

For deployments where the canonical product-source-of-truth branch differs from the repo's default branch (e.g., trunk-based teams using dev as integration line + main as release-tag-only), the current tenant-ingestion path can only ingest the default branch. Empirically, in the R3 smoke, the operator's tenant repo had main (older legacy state) as default and dev as the active development line — ingestion picked up the stale main content.

The repo-side workaround is "change the default branch on the GitLab/GitHub remote" — but that's an upstream operational change with broader implications (CI, PR target branches, deploy hooks). The substrate-side fix is to let tenantRepos[] declare which branch each tenant ingests from.

The Architectural Reality

  • tenantRepos[] entry shape currently: {tenantId, repoSlug, cloneUrl, credentialRef, mirrorRoot?, cadenceMs?}. No branchRef.
  • GitMirror.cloneIfMissing uses git clone --mirror <url> <path> — fetches all refs (good — no branch-restriction at clone time).
  • GitMirror.resolveHead signature: ({mirrorRoot, tenantId, repoSlug, ref = 'HEAD'}). The ref parameter exists but the upstream call chain in TenantRepoIngestEnvelopeBuilder always passes 'HEAD' (or a sha) — no plumbing for a per-tenant branch override.
  • TenantRepoIngestEnvelopeBuilder resolveRevision is the chokepoint where ref flows into gitMirror.resolveHead. New branchRef from tenant config would land here.

The Fix

Schema + plumbing:

  1. Schema: add optional branchRef field to tenant-repo entry. normalizeTenantRepoEntry (TenantRepoAccessContract.mjs:195-218) preserves via ...entry spread already; just declare in JSDoc + validate (must be string, non-empty if present).
  2. Plumbing: in TenantRepoSyncService (line 419-434), pass branchRef: repo.branchRef || 'HEAD' through to envelopeBuilder. The envelope builder then passes it to gitMirror.resolveHead({..., ref: branchRef}).
  3. Default: if absent, continue to use 'HEAD' (default branch). Backward-compatible.

Contract Ledger

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
tenantRepos[].branchRef (new optional string field) ai/services/knowledge-base/helpers/TenantRepoAccessContract.mjs normalizeTenantRepoEntry Resolved ref name (e.g., 'dev') passed downstream to gitMirror.resolveHead({ref: branchRef}) Absent → 'HEAD' (default branch) Cloud-deployment guide tenant-repo config section Unit test for normalize-with-branchRef + integration test ingesting non-default branch

Decision Record impact

aligned-with ADR 0014 — extends tenant-ingestion schema within existing topology.

Acceptance Criteria

  • normalizeTenantRepoEntry accepts optional branchRef (string, non-empty if present) and preserves it on the normalized entry.
  • TenantRepoSyncService passes branchRef (default 'HEAD') through to the envelope builder.
  • TenantRepoIngestEnvelopeBuilder uses the passed branchRef when resolving newHead revision instead of hardcoded 'HEAD'.
  • When branchRef is omitted, behavior is identical to current ('HEAD' = default branch).
  • Invalid branchRef (non-string, empty) errors at config-load with KB_TENANT_REPO_ENTRY_INVALID.
  • Unit test: tenant entry with branchRef: 'dev' resolves to the dev branch's HEAD.
  • Cloud-deployment guide updated to document the new field with a worked example.

Out of Scope

  • Multi-branch ingestion per tenant — single-branch-per-tenant covers the immediate need. If a future deployment needs to ingest multiple branches from one repo, that's a separate ticket (likely branchRefs: string[] extension).
  • Tag/sha-pinned ingestion — branchRef accepts any git ref (branch, tag, sha) since it flows through git rev-parse. Document with examples.
  • Branch-deletion handling — if the configured branchRef is later deleted on the remote, the next fetch surfaces an error; downstream consumers handle via existing KB_GITMIRROR_REF_NOT_FOUND path.

Avoided Traps

  • Coupling to --single-branch clone: the current git clone --mirror fetches all refs. Sticking with mirror clone + per-ingest ref selection means tenant operators can switch branchRef without re-cloning.
  • Hardcoding the default to dev: keeping the default as 'HEAD' (whatever the remote's default-branch is) preserves backward compat and matches operator expectation that omitted config = "do what git defaults do."

Related

  • Sibling: #12036 (3 substrate gaps from R3), #12032 (credentialRef file: scheme)
  • Parent epic: #11731 (Server-side tenant-repo ingestion)
  • Empirical anchor: first cloud-deployment tenant-ingestion attempt, 2026-05-26 — ingested default-branch content when operator's active integration line was a different branch
tobiu referenced in commit 9e6ef1a - "feat(kb): add branchRef to tenantRepos[] schema (#12040) (#12045) on May 26, 2026, 10:46 PM
tobiu closed this issue on May 26, 2026, 10:46 PM