diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8e12dec..cbc1946 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -98,3 +98,117 @@ jobs: env: INSTANT_API_REPO: ${{ github.workspace }}/../api run: go test ./... -v -race -count=1 + + # ------------------------------------------------------------------ + # GATING integration job (W2-T1). + # + # WHY: the worker's real-DB integration tests + # (internal/jobs/*_integration_test.go) skip under `-short` AND when + # TEST_DATABASE_URL is unset. The fast `build-and-test` job above runs + # `-race` with NO DB service and the deploy.yml test step runs + # `-short` — so in BOTH the integration tests SKIP and gate NOTHING. + # This job runs them WITHOUT `-short` against a real Postgres so the + # trigger→DB round-trip is actually asserted in CI. It mirrors + # coverage.yml's proven Postgres+Redis service block. Per the two-gate + # rule (root CLAUDE.md rule 15 / project_api_two_test_gates_ci_and_deploy), + # the same job is mirrored in deploy.yml — a worker test-infra change + # must land in BOTH or the deploy wedges while PR CI stays green. + integration: + runs-on: ubuntu-latest + timeout-minutes: 15 + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: instant_dev_test + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + redis: + image: redis:7-alpine + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + env: + # Integration tests read TEST_DATABASE_URL and run the real-DB path + # (they SKIP when it's unset). TEST_REDIS_URL is wired for parity + # with coverage.yml and for the redis-touching jobs as they land. + TEST_DATABASE_URL: postgres://postgres:postgres@localhost:5432/instant_dev_test?sslmode=disable + TEST_REDIS_URL: redis://localhost:6379/15 + steps: + - uses: actions/checkout@v6 + + - name: Checkout proto sibling (replace ../proto) + uses: actions/checkout@v6 + with: + repository: ${{ vars.PROTO_REPO || format('{0}/proto', github.repository_owner) }} + token: ${{ secrets.REPO_ACCESS_TOKEN || secrets.GITHUB_TOKEN }} + path: _proto_ci + - run: mv _proto_ci ../proto + + - name: Checkout common sibling (replace ../common) + uses: actions/checkout@v6 + with: + repository: ${{ vars.COMMON_REPO || format('{0}/common', github.repository_owner) }} + token: ${{ secrets.REPO_ACCESS_TOKEN || secrets.GITHUB_TOKEN }} + path: _common_ci + - run: mv _common_ci ../common + + - name: Checkout api sibling (for migrations + cross-repo tests) + uses: actions/checkout@v6 + with: + repository: ${{ vars.API_REPO || format('{0}/api', github.repository_owner) }} + token: ${{ secrets.REPO_ACCESS_TOKEN || secrets.GITHUB_TOKEN }} + path: _api_ci + fetch-depth: 1 + - run: mv _api_ci ../api + + - uses: actions/setup-go@v6 + with: + go-version: '1.25' + + - name: Apply DB migrations to the test database + # Mirrors coverage.yml: api owns the platform_db schema + # (teams / resources / deployments / pending_propagations / + # audit_log) the integration tests round-trip against; apply those + # first, then any worker-local SQL on top (IF NOT EXISTS-guarded). + env: + PGPASSWORD: postgres + run: | + if [ -d ../api/internal/db/migrations ]; then + for f in $(ls ../api/internal/db/migrations/*.sql | sort); do + echo "→ applying api migration $(basename "$f")" + psql -h localhost -U postgres -d instant_dev_test -f "$f" >/dev/null + done + echo "all api migrations applied to instant_dev_test" + else + echo "::error::no api migrations directory found at ../api/internal/db/migrations — integration tests would skip" + exit 1 + fi + if [ -d sql ]; then + for f in $(ls sql/*.sql 2>/dev/null | sort); do + echo "→ applying worker migration $(basename "$f")" + psql -h localhost -U postgres -d instant_dev_test -f "$f" >/dev/null || echo "::warning::worker migration $(basename "$f") failed — likely overlap with api schema; continuing" + done + fi + + - name: Run real-DB integration tests (NO -short, so they do not skip) + # `-run Integration` scopes the run to the _integration_test.go + # funcs (all carry "Integration" in their name); `-p 1` because + # every package shares the single instant_dev_test DB and default + # parallelism corrupts shared state. Dropping `-short` is what + # makes testhelpers.SetupTestDB's testing.Short() guard pass and + # the real-DB path run instead of skipping. + env: + INSTANT_API_REPO: ${{ github.workspace }}/../api + run: go test ./internal/... -run Integration -count=1 -p 1 -v diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f28423c..26a6225 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -54,8 +54,109 @@ env: K8S_CONTAINER: worker jobs: + # ------------------------------------------------------------------ + # GATING integration job (W2-T1) — two-gate rule mirror of ci.yml. + # + # The deploy.yml test step ("Run unit tests") runs `go test ./... + # -short`, under which every worker real-DB integration test SKIPS + # (testhelpers.SetupTestDB / *_integration_test.go guard on + # testing.Short()). So before this job, a broken integration + # round-trip could merge to master and DEPLOY — the deploy gate never + # ran the integration tests. Per project_api_two_test_gates_ci_and_deploy + # (root CLAUDE.md rule 15), a worker test-infra change must land in + # BOTH ci.yml and deploy.yml or the deploy wedges while PR CI stays + # green. `deploy` now `needs: integration`, so a red integration run + # blocks the build/rollout. + integration: + runs-on: ubuntu-latest + timeout-minutes: 15 + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: instant_dev_test + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + redis: + image: redis:7-alpine + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + env: + TEST_DATABASE_URL: postgres://postgres:postgres@localhost:5432/instant_dev_test?sslmode=disable + TEST_REDIS_URL: redis://localhost:6379/15 + steps: + - uses: actions/checkout@v6 + + - name: Checkout proto sibling (replace ../proto) + uses: actions/checkout@v6 + with: + repository: ${{ vars.PROTO_REPO || format('{0}/proto', github.repository_owner) }} + token: ${{ secrets.REPO_ACCESS_TOKEN || secrets.GITHUB_TOKEN }} + path: _proto_ci + - run: mv _proto_ci ../proto + + - name: Checkout common sibling (replace ../common) + uses: actions/checkout@v6 + with: + repository: ${{ vars.COMMON_REPO || format('{0}/common', github.repository_owner) }} + token: ${{ secrets.REPO_ACCESS_TOKEN || secrets.GITHUB_TOKEN }} + path: _common_ci + - run: mv _common_ci ../common + + - name: Checkout api sibling (for migrations + cross-repo tests) + uses: actions/checkout@v6 + with: + repository: ${{ vars.API_REPO || format('{0}/api', github.repository_owner) }} + token: ${{ secrets.REPO_ACCESS_TOKEN || secrets.GITHUB_TOKEN }} + path: _api_ci + fetch-depth: 1 + - run: mv _api_ci ../api + + - uses: actions/setup-go@v6 + with: + go-version: '1.25' + + - name: Apply DB migrations to the test database + env: + PGPASSWORD: postgres + run: | + if [ -d ../api/internal/db/migrations ]; then + for f in $(ls ../api/internal/db/migrations/*.sql | sort); do + echo "→ applying api migration $(basename "$f")" + psql -h localhost -U postgres -d instant_dev_test -f "$f" >/dev/null + done + echo "all api migrations applied to instant_dev_test" + else + echo "::error::no api migrations directory found at ../api/internal/db/migrations — integration tests would skip" + exit 1 + fi + if [ -d sql ]; then + for f in $(ls sql/*.sql 2>/dev/null | sort); do + echo "→ applying worker migration $(basename "$f")" + psql -h localhost -U postgres -d instant_dev_test -f "$f" >/dev/null || echo "::warning::worker migration $(basename "$f") failed — likely overlap with api schema; continuing" + done + fi + + - name: Run real-DB integration tests (NO -short, so they do not skip) + env: + INSTANT_API_REPO: ${{ github.workspace }}/../api + run: go test ./internal/... -run Integration -count=1 -p 1 -v + deploy: runs-on: ubuntu-latest + needs: integration steps: - name: Checkout worker (this repo) into ./worker uses: actions/checkout@v6 diff --git a/internal/jobs/propagation_runner_integration_test.go b/internal/jobs/propagation_runner_integration_test.go index c2e2d80..339bb9d 100644 --- a/internal/jobs/propagation_runner_integration_test.go +++ b/internal/jobs/propagation_runner_integration_test.go @@ -316,6 +316,19 @@ func TestPropagation_ForUpdateSkipLockedIntegration(t *testing.T) { ctx := context.Background() teamID := uuid.New() propID := uuid.New() + // pending_propagations.team_id carries a real FK to teams(id) in the api + // migration set (ON DELETE CASCADE). Against a fully-migrated DB (the + // non-short integration job's environment) the seed must reference a real + // team row or the INSERT fails the FK. Seed the parent team first; the FK + // cascade tidies the propagation row when the team is deleted in cleanup. + if _, err := db.ExecContext(ctx, + `INSERT INTO teams (id) VALUES ($1)`, teamID, + ); err != nil { + t.Fatalf("seed team: %v", err) + } + t.Cleanup(func() { + _, _ = db.ExecContext(context.Background(), `DELETE FROM teams WHERE id = $1`, teamID) + }) if _, err := db.ExecContext(ctx, ` INSERT INTO pending_propagations (id, kind, team_id, target_tier, payload, attempts, next_attempt_at, created_at)