chore: run spanner integration tests against a real test instance (#604) - #629
Conversation
The Spanner emulator only supports one transaction at a time, so concurrent integration tests are flaky. Add a testdb helper that creates a uniquely named database on an existing instance (dropped on teardown) so the suites can target a real, long-lived test instance with isolated per-run databases. Refactor the emulator path to reuse the shared CreateDatabase helper. Refs #604 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire ZITADEL_TEST_SPANNER_INSTANCE into both the repository and API integration suites. When set, they provision a fresh database on the shared instance and drop it after the run. Precedence: instance > URL > emulator container > Postgres. The postgres-only stub keeps that build compiling since the testdb package is spanner_integration-only. Refs #604 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Authenticate to Google Cloud with Workload Identity Federation (keyless) on trusted runs and point the spanner integration step at the shared instance via ZITADEL_TEST_SPANNER_INSTANCE. Fork PRs (no secrets) and unconfigured setups resolve the env empty and fall back to the emulator. Refs #604 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Document ZITADEL_TEST_SPANNER_INSTANCE, ADC authentication, and the override precedence in the Go database integration tests section. Refs #604 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
There was a problem hiding this comment.
Pull request overview
Enables Spanner integration tests to run reliably under concurrency by optionally provisioning a fresh per-run database on a shared real Spanner test instance (instead of relying solely on the single-transaction emulator), with CI and docs updates to support the new mode.
Changes:
- Added a
spanner_integration-taggedtestdbhelper that provisions and later drops a uniquely named database on a configured Spanner instance. - Refactored the emulator setup to reuse the same database-create helper and wired repository integration tests to prefer the shared-instance mode when configured.
- Updated CI to authenticate to GCP via Workload Identity Federation on trusted runs and set the instance env var; documented the new env var and precedence in
CONTRIBUTING.md.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| internal/storage/database/repository/repository_test.go | Adds shared-instance precedence before URL/container/emulator selection for repository integration tests. |
| internal/storage/database/repository/repository_spanner_stub_test.go | Adds stubbed shared-instance hooks for non-spanner builds. |
| internal/storage/database/repository/repository_spanner_setup_test.go | Implements shared-instance provisioning + migration for spanner_integration tests. |
| internal/storage/database/dialect/spanner/testdb/testdb.go | New helper to create/drop per-run databases on a real Spanner instance (ADC/WIF auth). |
| internal/storage/database/dialect/spanner/embedded/start.go | Reuses testdb.CreateDatabase for emulator database creation. |
| internal/storage/database/dbtest/spanner.go | Updates Spanner test connector selection precedence to include shared-instance provisioning. |
| CONTRIBUTING.md | Documents ZITADEL_TEST_SPANNER_INSTANCE and its precedence over URL/emulator. |
| .github/workflows/ci.yml | Adds OIDC permissions, optional GCP auth step, and passes the instance env var into the spanner test step. |
Move the Spanner auth and integration steps to the end of the job. Until the GCP infrastructure and GCP_* secrets are provisioned, the auth step fails on trusted runs by design (a visible signal that the real test instance is not configured yet); running it last keeps that expected failure from skipping the unrelated build, journey, and e2e steps. The trailing artifact-upload steps use always()/failure() and are unaffected. Refs #604 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…test-instance # Conflicts: # .github/workflows/ci.yml # internal/storage/database/dbtest/spanner.go # internal/storage/database/repository/repository_spanner_setup_test.go # internal/storage/database/repository/repository_spanner_stub_test.go # internal/storage/database/repository/repository_test.go # internal/storage/v2/testdb/spanner.go Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
Skip GCP auth unless the WIF secrets and SPANNER_TEST_INSTANCE var are set, and run Spanner integration tests with -parallel 1 -p 1 when no shared instance is configured so the emulator stays reliable for OSS and unconfigured CI. Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
Drop job-level GCP secret env. Always attempt Spanner WIF auth on trusted full runs with continue-on-error; set ZITADEL_TEST_SPANNER_INSTANCE only when auth succeeds, otherwise leave it empty so server:test-spanner uses the serial emulator path. Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
Keep a single server:test-spanner Moon task for local use. Split the CI Spanner suite into two mutually exclusive steps so the job summary shows whether the emulator or the real test instance ran. Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
Goose migrations are embedded via //go:embed sql/*.sql. Without tracking *.sql in serverInputs, Moon could cache-hit test/build after schema-only changes while Go would rebuild if invoked. Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
Inline the serial-emulator Moon script, unexport testdb provisioning helpers, keep uniqueness ahead of any ID clamp, and gate WIF auth on the public SPANNER_TEST_INSTANCE var so misconfigured infra hard-fails instead of silently falling back to the emulator. Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
## Summary <!-- Briefly describe what changed and why. --> - Enables some integration tests to run when spanner. - Use `require.IsType` instead of type-case + `require.True`. ## Validation - `go test ./...` - `go test -tags postgres_integration` - `go test -tags spanner_integration` ## Release notes / changeset - No changeset required ## Notes - blocked by #629 --------- Co-authored-by: adlerhurst <27845747+adlerhurst@users.noreply.github.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
Summary
The Cloud Spanner emulator only supports one transaction at a time, so concurrent
spanner_integrationtests are flaky (Transaction ... aborted due to another transaction getting priority. The emulator only supports one transaction at a time). This lets the integration suites run against a real, long-lived Spanner test instance, giving each run its own isolated database. Fixes #604.internal/storage/database/dialect/spanner/testdbhelper:Provisioncreates a uniquely named database on an existing instance (viaZITADEL_TEST_SPANNER_INSTANCE=projects/<p>/instances/<i>, ADC auth) and drops it on teardown. Database ids areitest_<run-id>_<random>, always including a random suffix so the two test binaries in one CI run never collide.CreateDatabasehelper (emulator kept as the local default).ZITADEL_TEST_SPANNER_INSTANCE>ZITADEL_TEST_SPANNER_URL> emulator container > Postgres.CONTRIBUTING.mddocuments the new env var and precedence.Important
The
Authenticate to Google Cloud (Spanner test instance)step is expected to FAIL on this PR and will keep failing on trusted runs until the GCP infrastructure andGCP_*secrets are provisioned (see prerequisite below). This is intentional: a red check is a visible signal that the real test instance is not yet configured, rather than silently masking it. It runs last so every unrelated step still executes and validates.Validation
gofmt -lon all touched files: clean.go vet -tags spanner_integration ./internal/storage/database/dialect/spanner/... ./internal/storage/database/repository/ ./internal/storage/database/dbtest/ ./internal/api/integration_test/: clean.go vet -tags postgres_integration ./internal/storage/database/repository/(stub path): clean.Release notes / changeset
spanner_integrationbuild tag, CI workflow, and docs).Notes
CI auth choice: Workload Identity Federation (keyless) was chosen. The considered alternative was a service-account JSON key stored as a GitHub secret: simpler to set up, but a long-lived credential to manage and rotate. WIF avoids the standing secret.
One-time infra prerequisite (not in this PR). Before trusted CI actually targets the real instance, ops must provision:
zitadel/nextgen; grant the SAroles/iam.workloadIdentityUser.GCP_WORKLOAD_IDENTITY_PROVIDERandGCP_SERVICE_ACCOUNT, and variableSPANNER_TEST_INSTANCE = projects/<project>/instances/<instance>.Until these exist, the auth step fails by design on trusted runs (see the note above). Because it runs last and the trailing artifact-upload steps use
always()/failure(), no unrelated step is skipped or fails on its account.