Repo-transfer fixtures
Operator-readable guide to the deterministic fixture set used by the
AccountTransfer feature. The fixture seeder, reset, and
failure-injection harness together let an operator stand up — and
reliably tear down — a known dataset against a Vetrix instance (typically
gitvetrix.test) so the transfer engine can be exercised
end-to-end with byte-stable inputs.
For a higher-level operator runbook (production transfer flows, alerting, recovery) see operator runbook.
Overview
The fixtures provide a canonical source/target user pair plus a populated
source repository so the repo_transfers workflow and the downstream
backup, preflight, archive, and round-trip machinery can be exercised
against a known dataset on a non-production Vetrix instance.
There are three pieces:
- Seeder (
scripts/transfer-fixtures/seed.sh+seed.go) — creates the fixture set idempotently and emits a stable manifest digest. - Reset (
scripts/transfer-fixtures/reset.sh) — removes everything the seeder created, in cascade-friendly order, idempotently. - Failure-injection harness (
internal/admin/repo_transfer/inject_*.go) — compile-time gated env-var flags that make the in-process transfer engine simulate each named failure on demand.
The seeder commits to a published count contract (see Seed command) and to a pinned absolute manifest digest (see Expected manifest digest). The round-trip test asserts equality against that pinned digest.
Prerequisites
To run the seeder or the reset against a Vetrix instance you need:
- A reachable Vetrix instance. The dev stack default is
https://api.gitvetrix.test; production targets likehttps://api.gitvetrix.comwork the same way. - A non-admin login to create the fixture entities. The seeder registers
the two fixture users (
transfer_srcandtransfer_dst) on first run, so the only thing required for seeding is an open registration endpoint on the target Vetrix instance. - Admin access is required for the user-row delete phase of the reset
(
DELETE /api/v1/admin/users/<uid>). The reset still runs without it, but the user-delete phase is skipped with a warning — see Reset command.
Environment variables
| Variable | Used by | Notes |
|---|---|---|
API_BASE |
seed + reset | Vetrix API base URL. Default https://api.gitvetrix.test. |
REGISTRY_BASE |
seed + reset | Override the OCI /v2/* base when the registry is split off (e.g. registry.gitvetrix.com). Falls back to $API_BASE. |
CURL_INSECURE |
seed + reset | Defaults to -k for the dev stack's self-signed TLS. Pass "" for trusted certs. |
TRANSFER_PASSWORD |
seed + reset | Password for both fixture users. Development seed fixture default — not a live credential; see below. |
TRANSFER_SRC_USER |
seed + reset | Source-side username. Default transfer_src. |
TRANSFER_DST_USER |
seed + reset | Target-side username. Default transfer_dst. |
TRANSFER_REPO |
seed + reset | Source-owned repository name. Default transfer_fixture_repo. |
TRANSFER_WORKDIR |
seed + reset | Manifest + scratch output dir. Default ./.fixtures. |
TRANSFER_HOOKS_PATH |
seed | Filesystem path to the bare repo's hooks/ dir for --custom-hooks. Unset by default — when unset, hook installation is silently downgraded to "manifest only" (the digest stays stable). |
TRANSFER_ADMIN_TOKEN |
reset | Pre-issued admin bearer token. Required for the user-delete phase. |
TRANSFER_ADMIN_USER |
reset | Admin username. Used when TRANSFER_ADMIN_TOKEN is unset. |
TRANSFER_ADMIN_PASSWORD |
reset | Admin password. Used together with TRANSFER_ADMIN_USER. |
For local development the mydev_postgres container (Docker Compose dev
stack) backs the mydev_forge instance reachable at
https://api.gitvetrix.test; no separate DB connection is needed
because the seeder talks only to the Vetrix HTTP API.
Fixture credential note
TRANSFER_PASSWORD's default is a development seed fixture, not a live
credential — the same fixture password used to log in the a11y gate and
the Playwright dev-stack super-admin (see a11y-gate.md § Environment
overrides
and playwright-stack-target.md §
Credentials). Per
authoring-vetrix-pipelines.md § Variables, interpolation &
secrets,
this doc does not print the literal value; override TRANSFER_PASSWORD
explicitly if your environment needs a different one.
Seed command
Run the seeder against the configured Vetrix instance:
scripts/transfer-fixtures/seed.sh
Useful variants:
# Re-target a non-default Vetrix instance:
API_BASE=https://api.example.com CURL_INSECURE="" \
scripts/transfer-fixtures/seed.sh
# Recompute / re-verify an existing seed without (re-)creating anything:
scripts/transfer-fixtures/seed.sh --self-test-only
# Best-effort install of custom hooks on disk (requires
# $TRANSFER_HOOKS_PATH pointing at a reachable bare-repo hooks/ dir;
# silently downgraded to "manifest only" otherwise):
TRANSFER_HOOKS_PATH=/var/lib/vetrix/repositories/transfer_src/transfer_fixture_repo.git/hooks \
scripts/transfer-fixtures/seed.sh --custom-hooks
Entities created
The seeder commits to the following counts; the self-test phase at the
end of every run asserts each count and aborts non-zero on any miss
(see scripts/transfer-fixtures/README.md for the table mapping each
entity to the backing API endpoint):
| Entity | Count |
|---|---|
| Users | 2 |
| Repository (source-owned) | 1 |
| Issues | 10 |
| Merge requests | 3 |
| Pages | 5 |
| OCI manifests | 5 |
| Pull mirrors | 2 |
| Pipelines | 3 |
| Webhooks | 2 |
| Custom hooks (on disk) | 2 |
Custom hooks are filesystem-resident; the API does not surface a
creation endpoint for them. The seeder records the canonical hook names
in the manifest unconditionally so the digest remains stable. Whether
they were actually installed on disk is captured in the manifest's
installed_custom_hooks boolean.
Webhook secrets and mirror credentials are placeholders sealed by
SECRET_ENC_KEY and MIRROR_ENC_KEY respectively (per the project
encryption contract documented in CLAUDE.md).
The seeder is idempotent: every entity creation is preceded by an existence probe, so a second invocation against the same Vetrix instance is a no-op and produces the same manifest digest.
Expected manifest digest
The seeder writes two manifest files under $TRANSFER_WORKDIR:
| File | Captures | Digest scope |
|---|---|---|
manifest.json |
Full record incl. ephemeral DB UUIDs | Stable within one Vetrix instance across re-runs |
manifest.stable.json |
Deterministic IDs + counts only (DB UUIDs stripped) | Stable across all instances and operators (absolute) |
Both digests are printed by the seeder on its tail lines:
[seed] manifest_digest=sha256:<full-digest, per-instance>
[seed] stable_manifest_digest=sha256:<absolute-digest>
Pinned stable digest
For the canonical fixture-user / fixture-repo names
(transfer_src / transfer_dst / transfer_fixture_repo) the absolute
stable_manifest_digest is:
sha256:16d458b87850b6c2f95d0a6f6eb6ac85a117f1ddd2135e707e59afc6cad445af
This value is locked by seed_test.go::TestStableManifest_DigestPin. It
is also the value the round-trip test asserts equality against —
a transfer round-trip that does not reproduce this digest by the end of
the run is a regression in either the seeder, the transfer engine, or
both, and the test fails.
If the seeder's count contract or the deterministic-ID derivation ever
changes, that pin test fails first and this doc must be updated in the
same commit (along with scripts/transfer-fixtures/README.md).
Per-instance (full) digest
The auto-allocated DB UUIDs (repo_id, source/target user_id) vary
per Vetrix instance — gen_random_uuid() does what it says — so the
full manifest digest is canonical within an instance only.
Run-to-run on the same instance the digest must be identical; that is
the invariant the self-test exercises. Capture it for your
environment with:
scripts/transfer-fixtures/seed.sh # first run
scripts/transfer-fixtures/seed.sh --self-test-only # re-verify
sha256sum .fixtures/manifest.json # or read the seeder's tail line
A second seed.sh invocation against the same instance must print the
same manifest_digest=sha256:… line.
Reset command
reset.sh is the inverse of seed.sh. It removes everything the
seeder created and is safe to re-run on an already-empty fixture set.
# Default (against the dev stack); the user-delete phase is skipped if
# no admin credentials are provided.
scripts/transfer-fixtures/reset.sh
# Recommended for a full teardown — admin token (preferred) or
# admin user/password.
TRANSFER_ADMIN_TOKEN=eyJhbGciOi… \
scripts/transfer-fixtures/reset.sh
# Or via admin login:
TRANSFER_ADMIN_USER=kcoder TRANSFER_ADMIN_PASSWORD='…' \
scripts/transfer-fixtures/reset.sh
# Tear down only the repo + space + workdir, leaving the fixture users
# in place (useful when iterating on the seeder itself):
scripts/transfer-fixtures/reset.sh --keep-users
# Verify the fixture set is already clean without (re-)deleting anything:
scripts/transfer-fixtures/reset.sh --self-test-only
What it removes
The reset is implemented in deliberate cascade-friendly order so a single repo delete does most of the work:
| Phase | API call | Cascades / side effects |
|---|---|---|
| Repository delete | DELETE /api/v1/repos/<src>/<repo> with the confirmation payload |
Cascades issues, MRs, mirrors, webhooks, pipelines, registry packages/blobs/manifests/tags, and the repo_transfers rows whose source_repo_id matches. The repo handler also removes the on-disk bare repository directory (<RepoRoot>/<src>/<repo>.git). |
| Page space delete | DELETE /api/v1/spaces/<slug> |
Cascades pages, page templates, page permissions, page labels. |
| User rows delete | DELETE /api/v1/admin/users/<uid> × 2 |
Removes transfer_src and transfer_dst. Requires admin credentials — see the admin-token note below. |
| Workdir cleanup | rm -rf $TRANSFER_WORKDIR |
Removes manifest output and per-run scratch directories left behind by the seeder. |
Admin-token note
Only the user-row delete phase needs an admin token. The other three phases (repository delete, page-space delete, workdir cleanup) run on the fixture user's own credentials and are unaffected.
If TRANSFER_ADMIN_TOKEN (or TRANSFER_ADMIN_USER +
TRANSFER_ADMIN_PASSWORD) is not provided, the reset prints a warning
on the user-delete phase and continues. The self-test then reports
leftover users as a warning rather than a hard failure — the operator
deferred user-delete on purpose. To later complete the teardown, re-run
reset.sh with admin credentials.
Idempotency
Every removal phase is preceded by an existence probe; missing entities
print an already absent line and continue. A second invocation
against an empty fixture set therefore prints already absent for
every phase, runs the self-test, and exits 0. This mirrors the seeder's
--self-test-only behaviour and lets reset.sh be wired into CI
teardown blocks safely.
Failure-injection flags
The transfer engine exposes a compile-time gated failure-injection
harness that lets a tagged build simulate each named failure on demand
via process env vars. The harness lives in
internal/admin/repo_transfer/inject_test_only.go (compiled in only
under -tags inject_failures); the production stub
internal/admin/repo_transfer/inject_disabled.go (compiled in under
-tags '!inject_failures') implements injectionPoint(name) as a
guaranteed-nil noop. Production binaries dead-strip the flags
entirely — the env-var readers are not present in the binary and
cannot be reached from outside the process, by construction.
The four flags
| Env var | Simulates |
|---|---|
XFER_INJECT_DISK_FULL |
Disk-full condition during a transfer phase. Surfaces as a sentinel error (errInjectedDiskFull). |
XFER_INJECT_PG_DUMP_FAIL |
pg_dump failure during preflight. Surfaces inside the standard *Error envelope with PreflightCodePreflightFailed; preflight short-circuits before the runner is invoked. |
XFER_INJECT_LOCK_TIMEOUT |
Postgres advisory-lock contention timeout. Surfaces as a typed *LockTimeoutError whose Op field the orchestrator routes on. |
XFER_INJECT_CRASH_AFTER_FS_RENAME |
Mid-flight crash after the filesystem rename has succeeded. Surfaces the CrashAfterFSRenameSentinel sentinel; the orchestrator boundary owns the actual crash semantics (log + exit, or panic). |
Truthy values are 1, true, yes, or on (case-insensitive, leading
and trailing whitespace tolerated). Anything else — including unset —
disables the flag.
Build-tag gate
Enable the harness with the inject_failures build tag at compile time:
# Tagged build — the four XFER_INJECT_* env vars are read at init():
go test -tags inject_failures ./internal/admin/repo_transfer/...
go build -tags inject_failures -o vetrix-inject ./cmd/server
# Production build — flags are dead-stripped:
go build ./cmd/server
The flags are consumed at init() time only, so flipping the env
mid-run has no effect; tests opt into a specific failure mode by
setting the env before the test process starts, or by overwriting
the package-level booleans directly via the helper exposed in
inject_test.go for that purpose.
The compile-time gate is auditable: nm / go tool on a deployed
binary either shows the harness symbols or it does not, with no runtime
DB row that could be flipped by accident or by an attacker. Production
deployments must build without -tags inject_failures.
Related documents
- Operator runbook — production-side AccountTransfer operator guide.
scripts/transfer-fixtures/README.md(vetrix backend repo) — implementation-level reference for the seeder and reset. Contains the per-endpoint count table, the deterministic-ID derivation, and the per-phase listing-endpoint contract for the self-test.