Vetrix Docs

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 like https://api.gitvetrix.com work the same way.
  • A non-admin login to create the fixture entities. The seeder registers the two fixture users (transfer_src and transfer_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.

  • 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.