feat(webhook): dispatch unlock commands through the durable inbox #5297
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: "Tests" | |
| on: | |
| push: | |
| tags: | |
| - v* | |
| branches: | |
| - master | |
| - main | |
| pull_request: | |
| # Run in the merge queue so the full suite gates merges. Required for the | |
| # planned PR-smoke / queue-full e2e split. Inert until a repo admin enables | |
| # the merge queue in branch protection. | |
| merge_group: | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| # Detect whether the change touches anything other than *.md / docs/**. | |
| # PRs that touch only docs skip the heavy jobs below; pushes (main/tags) | |
| # always run. Fails open: if detection fails, treat as code change. | |
| changes: | |
| name: "Detect changes" | |
| runs-on: ubuntu-latest | |
| permissions: | |
| pull-requests: read | |
| outputs: | |
| code: ${{ steps.out.outputs.code }} | |
| full: ${{ steps.out.outputs.full }} | |
| steps: | |
| - name: "Detect non-docs changes" | |
| if: github.event_name == 'pull_request' | |
| id: filter | |
| continue-on-error: true | |
| uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # dorny/paths-filter@v3.0.3 | |
| with: | |
| filters: | | |
| code: | |
| - '**' | |
| - '!**/*.md' | |
| - '!docs/**' | |
| - name: "Resolve code-change and coverage flags" | |
| id: out | |
| shell: bash | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| FILTER_OUTCOME: ${{ steps.filter.outcome }} | |
| FILTER_CODE: ${{ steps.filter.outputs.code }} | |
| PR_SMOKE_ENABLED: ${{ vars.PR_SMOKE_E2E_ENABLED }} | |
| run: | | |
| set -euo pipefail | |
| # code: does this change touch anything other than docs? | |
| if [ "$EVENT_NAME" != "pull_request" ]; then | |
| code=true | |
| elif [ "$FILTER_OUTCOME" != "success" ]; then | |
| echo "::warning::paths-filter failed; running CI instead of skipping" | |
| code=true | |
| else | |
| code="$FILTER_CODE" | |
| fi | |
| echo "code=$code" >> "$GITHUB_OUTPUT" | |
| # full: run the complete e2e matrix, or a smoke subset? | |
| # Always full on push/merge_group (these gate a merge). On a PR, run | |
| # the smoke subset ONLY when an admin has opted in via the repo | |
| # variable; any other value fails closed to full PR coverage so the | |
| # gate cannot be weakened by merging this workflow before the merge | |
| # queue is enabled. | |
| if [ "$EVENT_NAME" = "pull_request" ] && [ "$PR_SMOKE_ENABLED" = "true" ]; then | |
| full=false | |
| else | |
| full=true | |
| fi | |
| echo "full=$full" >> "$GITHUB_OUTPUT" | |
| echo "event=$EVENT_NAME code=$code full=$full (pr_smoke_enabled=${PR_SMOKE_ENABLED:-unset})" | |
| build: | |
| name: "Build" | |
| needs: changes | |
| if: needs.changes.outputs.code == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # actions/checkout@v6 | |
| - name: "Configure Go" | |
| uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # actions/setup-go@v6.1 | |
| with: | |
| go-version-file: go.mod | |
| cache: false | |
| - name: "Restore Go cache" | |
| uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # actions/cache/restore@v4 | |
| with: | |
| path: | | |
| ~/go/pkg/mod | |
| ~/.cache/go-build | |
| key: go-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }} | |
| restore-keys: go-${{ runner.os }}-${{ runner.arch }}- | |
| - name: "Build" | |
| run: go build -v ./... | |
| - name: "Vet (all tags)" | |
| run: | | |
| go vet ./... | |
| go vet -tags=integration ./... | |
| go vet -tags=e2e ./e2e/... | |
| - name: "Save Go cache" | |
| uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # actions/cache/save@v4 | |
| with: | |
| path: | | |
| ~/go/pkg/mod | |
| ~/.cache/go-build | |
| key: go-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }} | |
| unit: | |
| name: "Unit Tests" | |
| needs: changes | |
| if: needs.changes.outputs.code == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # actions/checkout@v6 | |
| - name: "Configure Go" | |
| uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # actions/setup-go@v6.1 | |
| with: | |
| go-version-file: go.mod | |
| cache: false | |
| - name: "Restore Go cache" | |
| uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # actions/cache/restore@v4 | |
| with: | |
| path: | | |
| ~/go/pkg/mod | |
| ~/.cache/go-build | |
| key: go-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }} | |
| restore-keys: go-${{ runner.os }}-${{ runner.arch }}- | |
| - name: "Test" | |
| run: make test-unit | |
| integration: | |
| name: "Integration Tests" | |
| needs: changes | |
| if: needs.changes.outputs.code == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # actions/checkout@v6 | |
| - name: "Configure Go" | |
| uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # actions/setup-go@v6.1 | |
| with: | |
| go-version-file: go.mod | |
| cache: false | |
| - name: "Restore Go cache" | |
| uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # actions/cache/restore@v4 | |
| with: | |
| path: | | |
| ~/go/pkg/mod | |
| ~/.cache/go-build | |
| key: go-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }} | |
| restore-keys: go-${{ runner.os }}-${{ runner.arch }}- | |
| # Pre-pull external images with retries so transient registry outages | |
| # surface here as a named, retryable, fail-fast step instead of as many | |
| # parallel testcontainer "context deadline exceeded" test failures. | |
| - name: "Pre-pull external images" | |
| run: | | |
| scripts/ci/prepull-images.sh \ | |
| mysql:8.0 \ | |
| mysql:8.4 \ | |
| postgres:16 \ | |
| localstack/localstack:3.0 \ | |
| testcontainers/ryuk:0.13.0 | |
| - name: "Test" | |
| # Cap package-level parallelism: the integration suite runs many heavy | |
| # MySQL/Spirit package binaries that otherwise thrash CPU and the shared | |
| # MySQL, starving operator claim loops and causing spurious pending-apply | |
| # timeouts. Two lanes keeps wall time within the job budget. | |
| run: go test -tags=integration -timeout=10m -count=1 -p=2 $(go list -tags=integration ./... | grep -v /localscale) | |
| localscale-matrix: | |
| name: "LocalScale Matrix" | |
| needs: changes | |
| if: needs.changes.outputs.code == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 1 | |
| outputs: | |
| shards: ${{ steps.gen.outputs.shards }} | |
| mode: ${{ steps.gen.outputs.mode }} | |
| shard_count: ${{ steps.gen.outputs.shard_count }} | |
| steps: | |
| - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # actions/checkout@v6 | |
| - name: "Generate shard matrix" | |
| id: gen | |
| env: | |
| FULL: ${{ needs.changes.outputs.full }} | |
| PR_NUMBER: ${{ github.event.pull_request.number || 0 }} | |
| run: | | |
| set -euo pipefail | |
| NUM_SHARDS=3 | |
| # Deterministic order so a test always lands in the same bucket. | |
| tests=$(grep -h -o 'func Test[^(]*' pkg/localscale/*_test.go | sed 's/func //' | grep -v '/' | sort) | |
| count=$(printf '%s\n' "$tests" | grep -c . || true) | |
| if [ "$count" -eq 0 ]; then | |
| echo "::error::no localscale tests discovered" | |
| exit 1 | |
| fi | |
| all=$(printf '%s\n' "$tests" | awk -v n="$NUM_SHARDS" ' | |
| { bucket[NR % n] = bucket[NR % n] (bucket[NR % n] ? "|" : "") $0 } | |
| END { for (i=0; i<n; i++) printf "{\"shard\":\"%d\",\"run_filter\":\"%s\"}\n", i+1, bucket[i] } | |
| ' | jq -cs '.') | |
| if [ "$FULL" = "true" ]; then | |
| shards="$all" | |
| mode="full" | |
| else | |
| # Smoke: one bucket, chosen deterministically per PR so reruns are reproducible. | |
| idx=$(( PR_NUMBER % NUM_SHARDS )) | |
| shards=$(printf '%s' "$all" | jq -c "[.[$idx]]") | |
| mode="smoke" | |
| fi | |
| shard_count=$(printf '%s' "$shards" | jq 'length') | |
| echo "shards=$shards" >> "$GITHUB_OUTPUT" | |
| echo "mode=$mode" >> "$GITHUB_OUTPUT" | |
| echo "shard_count=$shard_count" >> "$GITHUB_OUTPUT" | |
| echo "mode=$mode shard_count=$shard_count from $count tests" | |
| localscale: | |
| name: "LocalScale Tests (${{ matrix.shard }})" | |
| needs: [changes, build, localscale-matrix] | |
| if: needs.changes.outputs.code == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: ${{ fromJson(needs.localscale-matrix.outputs.shards) }} | |
| steps: | |
| - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # actions/checkout@v6 | |
| - name: "Configure Go" | |
| uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # actions/setup-go@v6.1 | |
| with: | |
| go-version-file: go.mod | |
| cache: false | |
| - name: "Restore Go cache" | |
| uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # actions/cache/restore@v4 | |
| with: | |
| path: | | |
| ~/go/pkg/mod | |
| ~/.cache/go-build | |
| key: go-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }} | |
| restore-keys: go-${{ runner.os }}-${{ runner.arch }}- | |
| # Pre-pull external images with retries so a transient registry outage | |
| # fails fast in a named, retryable step instead of failing the image | |
| # build or the testcontainer-based tests below. | |
| - name: "Pre-pull external images" | |
| run: | | |
| scripts/ci/prepull-images.sh \ | |
| testcontainers/ryuk:0.13.0 \ | |
| --dockerfile deploy/local/Dockerfile.localscale | |
| - name: "Build LocalScale image" | |
| run: | | |
| CGO_ENABLED=0 GOOS=linux go build -o bin/localscale-linux ./cmd/localscale | |
| docker build -f deploy/local/Dockerfile.localscale -t localscale:latest . | |
| - name: "Compile tests" | |
| run: go test -tags=integration -c -o /dev/null ./pkg/localscale/... | |
| - name: "Test" | |
| run: go test -tags=integration -timeout=8m -count=1 -v -run "${{ matrix.run_filter }}" ./pkg/localscale/... 2>&1 | tee /dev/stderr | tail -50 | |
| e2e-mysql: | |
| name: "E2E MySQL Tests" | |
| needs: changes | |
| if: needs.changes.outputs.code == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # actions/checkout@v6 | |
| - name: "Configure Go" | |
| uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # actions/setup-go@v6.1 | |
| with: | |
| go-version-file: go.mod | |
| cache: false | |
| - name: "Restore Go cache" | |
| uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # actions/cache/restore@v4 | |
| with: | |
| path: | | |
| ~/go/pkg/mod | |
| ~/.cache/go-build | |
| key: go-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }} | |
| restore-keys: go-${{ runner.os }}-${{ runner.arch }}- | |
| # Pre-pull external images with retries so a transient registry outage | |
| # fails fast in a named, retryable step instead of failing the compose | |
| # image builds or container startup mid-job. | |
| - name: "Pre-pull external images" | |
| run: | | |
| scripts/ci/prepull-images.sh \ | |
| mysql:8.0 \ | |
| --dockerfile deploy/local/Dockerfile.dev \ | |
| --dockerfile deploy/local/Dockerfile.localscale | |
| - name: "Test" | |
| run: make test-e2e-mysql test-e2e-grpc | |
| - name: "Capture container logs on failure" | |
| if: failure() | |
| run: | | |
| mkdir -p e2e-logs | |
| # test-e2e-mysql (test-e2e-local) leaves its stack up, so capture it here. | |
| docker compose -f deploy/e2e/docker-compose.yml logs --no-color --timestamps > e2e-logs/mysql-containers.log 2>&1 || true | |
| echo "=== tail of MySQL container logs (full logs in the uploaded e2e-mysql-logs artifact) ===" | |
| tail -n 200 e2e-logs/mysql-containers.log || true | |
| # test-e2e-grpc tears its own stack down, so it captures grpc-containers.log itself. | |
| if [ -f e2e-logs/grpc-containers.log ]; then | |
| echo "=== tail of gRPC container logs ===" | |
| tail -n 200 e2e-logs/grpc-containers.log || true | |
| fi | |
| - name: "Upload container logs on failure" | |
| if: failure() | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # actions/upload-artifact@v4.6.2 | |
| with: | |
| name: e2e-mysql-logs | |
| path: e2e-logs/ | |
| retention-days: 7 | |
| if-no-files-found: ignore | |
| e2e-grpc-multideploy: | |
| name: "E2E gRPC Multi-Deployment Tests (${{ matrix.shard }})" | |
| needs: changes | |
| if: needs.changes.outputs.code == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - shard: ordered | |
| run: TestGRPCMultiDeploy_(SchemaBot_Health|TernHealth_BothDeployments|Plan_FansOut|OrderedCutover|OrderedCutoverObservability|BarrierReleaseBounded)$$ | |
| - shard: failure-policy | |
| run: TestGRPCMultiDeploy_(FailureHaltsRollout|OnFailureContinue)$$ | |
| steps: | |
| - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # actions/checkout@v6 | |
| - name: "Configure Go" | |
| uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # actions/setup-go@v6.1 | |
| with: | |
| go-version-file: go.mod | |
| cache: false | |
| - name: "Restore Go cache" | |
| uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # actions/cache/restore@v4 | |
| with: | |
| path: | | |
| ~/go/pkg/mod | |
| ~/.cache/go-build | |
| key: go-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }} | |
| restore-keys: go-${{ runner.os }}-${{ runner.arch }}- | |
| # Pre-pull external images with retries so a transient registry outage | |
| # fails fast in a named, retryable step instead of failing the compose | |
| # image builds or container startup mid-job. | |
| - name: "Pre-pull external images" | |
| run: | | |
| scripts/ci/prepull-images.sh \ | |
| mysql:8.0 \ | |
| --dockerfile deploy/local/Dockerfile.dev | |
| - name: "Test" | |
| run: make test-e2e-grpc-multideploy E2E_GRPC_MD_RUN='${{ matrix.run }}' | |
| - name: "Show captured container logs on failure" | |
| if: failure() | |
| run: | | |
| # test-e2e-grpc-multideploy tears its own stack down, so it captures | |
| # grpc-multideploy-containers.log itself before teardown; the stack is | |
| # already gone by the time this step runs. | |
| echo "=== tail of container logs (full logs in the uploaded e2e-grpc-md-logs-shard-${{ matrix.shard }} artifact) ===" | |
| tail -n 200 e2e-logs/grpc-multideploy-containers.log 2>/dev/null || echo "no container logs captured (stack failed before tests ran; see the job log above)" | |
| - name: "Upload container logs on failure" | |
| if: failure() | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # actions/upload-artifact@v4.6.2 | |
| with: | |
| name: e2e-grpc-md-logs-shard-${{ matrix.shard }} | |
| path: e2e-logs/ | |
| retention-days: 7 | |
| if-no-files-found: ignore | |
| e2e-vitess-matrix: | |
| name: "E2E Vitess Matrix" | |
| needs: changes | |
| if: needs.changes.outputs.code == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 1 | |
| outputs: | |
| shards: ${{ steps.gen.outputs.shards }} | |
| mode: ${{ steps.gen.outputs.mode }} | |
| shard_count: ${{ steps.gen.outputs.shard_count }} | |
| steps: | |
| - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # actions/checkout@v6 | |
| - name: "Generate shard matrix" | |
| id: gen | |
| env: | |
| FULL: ${{ needs.changes.outputs.full }} | |
| PR_NUMBER: ${{ github.event.pull_request.number || 0 }} | |
| run: | | |
| set -euo pipefail | |
| NUM_SHARDS=3 | |
| # Deterministic order so a test always lands in the same bucket. | |
| tests=$(grep -o 'func TestVitess_[^(]*' e2e/local/vitess_test.go | sed 's/func //' | sort) | |
| count=$(printf '%s\n' "$tests" | grep -c . || true) | |
| if [ "$count" -eq 0 ]; then | |
| echo "::error::no vitess e2e tests discovered" | |
| exit 1 | |
| fi | |
| all=$(printf '%s\n' "$tests" | awk -v n="$NUM_SHARDS" ' | |
| { bucket[NR % n] = bucket[NR % n] (bucket[NR % n] ? "|" : "") $0 } | |
| END { for (i=0; i<n; i++) printf "{\"shard\":\"%d\",\"run_filter\":\"%s\"}\n", i+1, bucket[i] } | |
| ' | jq -cs '.') | |
| if [ "$FULL" = "true" ]; then | |
| shards="$all" | |
| mode="full" | |
| else | |
| # Smoke: one bucket, chosen deterministically per PR so reruns are reproducible. | |
| idx=$(( PR_NUMBER % NUM_SHARDS )) | |
| shards=$(printf '%s' "$all" | jq -c "[.[$idx]]") | |
| mode="smoke" | |
| fi | |
| shard_count=$(printf '%s' "$shards" | jq 'length') | |
| echo "shards=$shards" >> "$GITHUB_OUTPUT" | |
| echo "mode=$mode" >> "$GITHUB_OUTPUT" | |
| echo "shard_count=$shard_count" >> "$GITHUB_OUTPUT" | |
| echo "mode=$mode shard_count=$shard_count from $count tests" | |
| e2e-vitess: | |
| name: "E2E Vitess Tests (${{ matrix.shard }})" | |
| needs: [changes, e2e-vitess-matrix] | |
| if: needs.changes.outputs.code == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: ${{ fromJson(needs.e2e-vitess-matrix.outputs.shards) }} | |
| steps: | |
| - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # actions/checkout@v6 | |
| - name: "Configure Go" | |
| uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # actions/setup-go@v6.1 | |
| with: | |
| go-version-file: go.mod | |
| cache: false | |
| - name: "Restore Go cache" | |
| uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # actions/cache/restore@v4 | |
| with: | |
| path: | | |
| ~/go/pkg/mod | |
| ~/.cache/go-build | |
| key: go-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }} | |
| restore-keys: go-${{ runner.os }}-${{ runner.arch }}- | |
| # Pre-pull external images with retries so a transient registry outage | |
| # fails fast in a named, retryable step instead of failing the compose | |
| # image builds or container startup mid-job. | |
| - name: "Pre-pull external images" | |
| run: | | |
| scripts/ci/prepull-images.sh \ | |
| mysql:8.0 \ | |
| --dockerfile deploy/local/Dockerfile.dev \ | |
| --dockerfile deploy/local/Dockerfile.localscale | |
| - name: "Test" | |
| run: | | |
| echo "Vitess e2e shard ${{ matrix.shard }}: ${{ matrix.run_filter }}" | |
| make test-e2e-local RUN="${{ matrix.run_filter }}" E2E_TEST_TIMEOUT=8m E2E_TEST_FLAGS=-v | |
| - name: "Capture container logs on failure" | |
| if: failure() | |
| run: | | |
| mkdir -p e2e-logs | |
| docker compose -f deploy/e2e/docker-compose.yml logs --no-color --timestamps > e2e-logs/containers.log 2>&1 || true | |
| echo "=== tail of container logs (full logs in the uploaded e2e-vitess-logs-shard-${{ matrix.shard }} artifact) ===" | |
| tail -n 200 e2e-logs/containers.log || true | |
| - name: "Upload container logs on failure" | |
| if: failure() | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # actions/upload-artifact@v4.6.2 | |
| with: | |
| name: e2e-vitess-logs-shard-${{ matrix.shard }} | |
| path: e2e-logs/ | |
| retention-days: 7 | |
| if-no-files-found: ignore | |
| e2e-k8s: | |
| name: "E2E K8s Tests" | |
| needs: changes | |
| if: needs.changes.outputs.code == 'true' && (needs.changes.outputs.full == 'true' || github.event_name != 'pull_request') | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # actions/checkout@v6 | |
| - name: "Configure Go" | |
| uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # actions/setup-go@v6.1 | |
| with: | |
| go-version-file: go.mod | |
| cache: false | |
| - name: "Restore Go cache" | |
| uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # actions/cache/restore@v4 | |
| with: | |
| path: | | |
| ~/go/pkg/mod | |
| ~/.cache/go-build | |
| key: go-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }} | |
| restore-keys: go-${{ runner.os }}-${{ runner.arch }}- | |
| - name: "Start minikube" | |
| uses: medyagh/setup-minikube@d8c0eb871f6f455542491d86a574477bd3894533 # medyagh/setup-minikube@v0.0.18 | |
| with: | |
| driver: docker | |
| cpus: 2 | |
| memory: 2048 | |
| - name: "Build and load image" | |
| run: | | |
| CGO_ENABLED=0 GOOS=linux go build -o bin/schemabot-linux ./pkg/cmd | |
| cp bin/schemabot-linux deploy/local/schemabot-dev | |
| eval $(minikube docker-env) | |
| docker build -t schemabot:test -f deploy/local/Dockerfile.dev deploy/local/ | |
| rm -f deploy/local/schemabot-dev | |
| - name: "Deploy and test" | |
| run: e2e/k8s/e2e-test.sh | |
| - name: "Pod logs on failure" | |
| if: failure() | |
| run: | | |
| echo "=== Control Plane ===" | |
| kubectl logs -n schemabot-e2e -l app.kubernetes.io/instance=control-plane --tail=200 2>/dev/null || true | |
| echo "=== Data Plane ===" | |
| kubectl logs -n schemabot-e2e -l app.kubernetes.io/instance=data-plane --tail=200 2>/dev/null || true | |
| echo "=== MySQL Control Plane ===" | |
| kubectl logs -n schemabot-e2e -l app=mysql-control-plane --tail=50 2>/dev/null || true | |
| echo "=== MySQL Data Plane ===" | |
| kubectl logs -n schemabot-e2e -l app=mysql-data-plane --tail=50 2>/dev/null || true | |
| echo "=== All pods ===" | |
| kubectl get pods -n schemabot-e2e -o wide 2>/dev/null || true | |
| e2e-k8s-etre: | |
| name: "E2E K8s Etre Tests" | |
| needs: changes | |
| if: needs.changes.outputs.code == 'true' && (needs.changes.outputs.full == 'true' || github.event_name != 'pull_request') | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # actions/checkout@v6 | |
| - name: "Configure Go" | |
| uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # actions/setup-go@v6.1 | |
| with: | |
| go-version-file: go.mod | |
| cache: false | |
| - name: "Restore Go cache" | |
| uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # actions/cache/restore@v4 | |
| with: | |
| path: | | |
| ~/go/pkg/mod | |
| ~/.cache/go-build | |
| key: go-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }} | |
| restore-keys: go-${{ runner.os }}-${{ runner.arch }}- | |
| - name: "Start minikube" | |
| uses: medyagh/setup-minikube@d8c0eb871f6f455542491d86a574477bd3894533 # medyagh/setup-minikube@v0.0.18 | |
| with: | |
| driver: docker | |
| cpus: 2 | |
| memory: 4096 | |
| # The script builds both the schemabot and etre images into minikube's | |
| # docker daemon, so no separate build step is needed here. | |
| - name: "Deploy and test" | |
| run: e2e/k8s/e2e-etre-test.sh | |
| - name: "Pod logs on failure" | |
| if: failure() | |
| run: | | |
| echo "=== Control Plane ===" | |
| kubectl logs -n schemabot-e2e-etre -l app.kubernetes.io/instance=control-plane-etre --tail=200 2>/dev/null || true | |
| echo "=== Data Plane ===" | |
| kubectl logs -n schemabot-e2e-etre -l app.kubernetes.io/instance=data-plane-etre --tail=200 2>/dev/null || true | |
| echo "=== Etre ===" | |
| kubectl logs -n schemabot-e2e-etre -l app=etre --tail=100 2>/dev/null || true | |
| echo "=== ministack ===" | |
| kubectl logs -n schemabot-e2e-etre -l app=ministack --tail=100 2>/dev/null || true | |
| echo "=== Seed jobs ===" | |
| kubectl logs -n schemabot-e2e-etre -l job-name=etre-seed --tail=50 2>/dev/null || true | |
| kubectl logs -n schemabot-e2e-etre -l job-name=ministack-seed --tail=50 2>/dev/null || true | |
| echo "=== All pods ===" | |
| kubectl get pods -n schemabot-e2e-etre -o wide 2>/dev/null || true | |
| e2e-k8s-vitess: | |
| name: "E2E K8s Vitess Tests" | |
| needs: changes | |
| if: needs.changes.outputs.code == 'true' && (needs.changes.outputs.full == 'true' || github.event_name != 'pull_request') | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # actions/checkout@v6 | |
| - name: "Configure Go" | |
| uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # actions/setup-go@v6.1 | |
| with: | |
| go-version-file: go.mod | |
| cache: false | |
| - name: "Restore Go cache" | |
| uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # actions/cache/restore@v4 | |
| with: | |
| path: | | |
| ~/go/pkg/mod | |
| ~/.cache/go-build | |
| key: go-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }} | |
| restore-keys: go-${{ runner.os }}-${{ runner.arch }}- | |
| # The runner ships a preinstalled MySQL whose AppArmor profile confines | |
| # /usr/sbin/mysqld to its packaged data dirs. LocalScale runs vtcombo's | |
| # embedded mysqld (the same binary path) against a vttest data dir, so the | |
| # host profile blocks it from reading its own my.cnf and the init aborts. | |
| # Unload the profile on the runner so the embedded mysqld runs unconfined. | |
| - name: "Disable host mysqld AppArmor profile" | |
| run: | | |
| if [ -f /etc/apparmor.d/usr.sbin.mysqld ]; then | |
| sudo apparmor_parser -R /etc/apparmor.d/usr.sbin.mysqld || true | |
| sudo ln -sf /etc/apparmor.d/usr.sbin.mysqld /etc/apparmor.d/disable/ || true | |
| fi | |
| - name: "Start minikube" | |
| uses: medyagh/setup-minikube@d8c0eb871f6f455542491d86a574477bd3894533 # medyagh/setup-minikube@v0.0.18 | |
| with: | |
| driver: docker | |
| cpus: 2 | |
| memory: 6144 | |
| # The script builds the schemabot, etre, and localscale images into | |
| # minikube's docker daemon, so no separate build step is needed here. | |
| - name: "Deploy and test" | |
| # Keep the namespace on exit so the failure log dump below can read pod | |
| # logs; the runner is ephemeral, so there is nothing to clean up after. | |
| env: | |
| KEEP_NAMESPACE: "1" | |
| run: e2e/k8s/e2e-vitess-test.sh | |
| - name: "Pod logs on failure" | |
| if: failure() | |
| run: | | |
| echo "=== Control Plane ===" | |
| kubectl logs -n schemabot-e2e-vitess -l app.kubernetes.io/instance=control-plane-vitess --tail=200 2>/dev/null || true | |
| echo "=== Data Plane ===" | |
| kubectl logs -n schemabot-e2e-vitess -l app.kubernetes.io/instance=data-plane-vitess --tail=200 2>/dev/null || true | |
| echo "=== Etre ===" | |
| kubectl logs -n schemabot-e2e-vitess -l app=etre --tail=100 2>/dev/null || true | |
| echo "=== LocalScale ===" | |
| kubectl logs -n schemabot-e2e-vitess -l app=localscale --tail=200 2>/dev/null || true | |
| echo "=== Seed jobs ===" | |
| kubectl logs -n schemabot-e2e-vitess -l job-name=etre-seed --tail=50 2>/dev/null || true | |
| echo "=== All pods ===" | |
| kubectl get pods -n schemabot-e2e-vitess -o wide 2>/dev/null || true | |
| # Rollup jobs that match the old required check names. | |
| # Remove these after updating branch protection to use the new job names. | |
| # | |
| # On docs-only PRs the upstream jobs are intentionally skipped via the | |
| # `changes` job; the rollups treat that as success. On any other PR a | |
| # skipped or failed upstream is treated as failure. | |
| e2e-rollup: | |
| name: "E2E Tests" | |
| needs: [changes, e2e-vitess-matrix, e2e-mysql, e2e-grpc-multideploy, e2e-vitess] | |
| runs-on: ubuntu-latest | |
| if: always() | |
| steps: | |
| - shell: bash | |
| env: | |
| CHANGES_RESULT: ${{ needs.changes.result }} | |
| CHANGES_CODE: ${{ needs.changes.outputs.code }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| FULL: ${{ needs.changes.outputs.full }} | |
| VITESS_MODE: ${{ needs.e2e-vitess-matrix.outputs.mode }} | |
| VITESS_SHARD_COUNT: ${{ needs.e2e-vitess-matrix.outputs.shard_count }} | |
| E2E_MYSQL_RESULT: ${{ needs.e2e-mysql.result }} | |
| E2E_MULTIDEPLOY_RESULT: ${{ needs.e2e-grpc-multideploy.result }} | |
| E2E_VITESS_RESULT: ${{ needs.e2e-vitess.result }} | |
| run: | | |
| set -euo pipefail | |
| if [ "$CHANGES_RESULT" != "success" ]; then | |
| echo "::error::changes job failed" | |
| exit 1 | |
| fi | |
| if [ "$CHANGES_CODE" != "true" ]; then | |
| echo "Docs-only PR; E2E intentionally skipped." | |
| exit 0 | |
| fi | |
| # Fail closed: a merge-gating event must run the full matrix, and the | |
| # matrix that was actually instantiated must match the claimed mode. | |
| # A passing result on a single smoke shard must never gate a merge. | |
| if [ "$EVENT_NAME" != "pull_request" ] && [ "$FULL" != "true" ]; then | |
| echo "::error::non-PR event ($EVENT_NAME) must run the full E2E matrix" | |
| exit 1 | |
| fi | |
| if [ "$FULL" = "true" ] && [ "$VITESS_SHARD_COUNT" != "3" ]; then | |
| echo "::error::full E2E expected 3 Vitess shards, got mode=$VITESS_MODE count=$VITESS_SHARD_COUNT" | |
| exit 1 | |
| fi | |
| if [ "$FULL" != "true" ] && [ "$VITESS_SHARD_COUNT" != "1" ]; then | |
| echo "::error::smoke E2E expected 1 Vitess shard, got mode=$VITESS_MODE count=$VITESS_SHARD_COUNT" | |
| exit 1 | |
| fi | |
| if [ "$E2E_MYSQL_RESULT" != "success" ]; then | |
| echo "::error::e2e-mysql result=$E2E_MYSQL_RESULT" | |
| exit 1 | |
| fi | |
| if [ "$E2E_MULTIDEPLOY_RESULT" != "success" ]; then | |
| echo "::error::e2e-grpc-multideploy result=$E2E_MULTIDEPLOY_RESULT" | |
| exit 1 | |
| fi | |
| if [ "$E2E_VITESS_RESULT" != "success" ]; then | |
| echo "::error::e2e-vitess result=$E2E_VITESS_RESULT" | |
| exit 1 | |
| fi | |
| localscale-rollup: | |
| name: "LocalScale Tests" | |
| needs: [changes, localscale-matrix, localscale] | |
| runs-on: ubuntu-latest | |
| if: always() | |
| steps: | |
| - shell: bash | |
| env: | |
| CHANGES_RESULT: ${{ needs.changes.result }} | |
| CHANGES_CODE: ${{ needs.changes.outputs.code }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| FULL: ${{ needs.changes.outputs.full }} | |
| LS_MODE: ${{ needs.localscale-matrix.outputs.mode }} | |
| LS_SHARD_COUNT: ${{ needs.localscale-matrix.outputs.shard_count }} | |
| LOCALSCALE_RESULT: ${{ needs.localscale.result }} | |
| run: | | |
| set -euo pipefail | |
| if [ "$CHANGES_RESULT" != "success" ]; then | |
| echo "::error::changes job failed" | |
| exit 1 | |
| fi | |
| if [ "$CHANGES_CODE" != "true" ]; then | |
| echo "Docs-only PR; LocalScale intentionally skipped." | |
| exit 0 | |
| fi | |
| # Fail closed: a merge-gating event must run the full matrix, and the | |
| # matrix that was actually instantiated must match the claimed mode. | |
| if [ "$EVENT_NAME" != "pull_request" ] && [ "$FULL" != "true" ]; then | |
| echo "::error::non-PR event ($EVENT_NAME) must run the full LocalScale matrix" | |
| exit 1 | |
| fi | |
| if [ "$FULL" = "true" ] && [ "$LS_SHARD_COUNT" != "3" ]; then | |
| echo "::error::full LocalScale expected 3 shards, got mode=$LS_MODE count=$LS_SHARD_COUNT" | |
| exit 1 | |
| fi | |
| if [ "$FULL" != "true" ] && [ "$LS_SHARD_COUNT" != "1" ]; then | |
| echo "::error::smoke LocalScale expected 1 shard, got mode=$LS_MODE count=$LS_SHARD_COUNT" | |
| exit 1 | |
| fi | |
| if [ "$LOCALSCALE_RESULT" != "success" ]; then | |
| echo "::error::localscale result=$LOCALSCALE_RESULT" | |
| exit 1 | |
| fi | |
| # Stable rollup for the k8s e2e jobs so a single check name can gate a merge. | |
| # On a PR running the smoke subset these jobs are intentionally skipped and the | |
| # full k8s coverage runs on push / in the merge queue; on any merge-gating | |
| # event all three must succeed. | |
| k8s-rollup: | |
| name: "K8s E2E Tests" | |
| needs: [changes, e2e-k8s, e2e-k8s-etre, e2e-k8s-vitess] | |
| runs-on: ubuntu-latest | |
| if: always() | |
| steps: | |
| - shell: bash | |
| env: | |
| CHANGES_RESULT: ${{ needs.changes.result }} | |
| CHANGES_CODE: ${{ needs.changes.outputs.code }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| FULL: ${{ needs.changes.outputs.full }} | |
| K8S_RESULT: ${{ needs.e2e-k8s.result }} | |
| K8S_ETRE_RESULT: ${{ needs.e2e-k8s-etre.result }} | |
| K8S_VITESS_RESULT: ${{ needs.e2e-k8s-vitess.result }} | |
| run: | | |
| set -euo pipefail | |
| if [ "$CHANGES_RESULT" != "success" ]; then | |
| echo "::error::changes job failed" | |
| exit 1 | |
| fi | |
| if [ "$CHANGES_CODE" != "true" ]; then | |
| echo "Docs-only PR; K8s E2E intentionally skipped." | |
| exit 0 | |
| fi | |
| # On a PR running the smoke subset, k8s is deferred to the merge queue. | |
| if [ "$EVENT_NAME" = "pull_request" ] && [ "$FULL" != "true" ]; then | |
| echo "PR smoke run; full K8s E2E deferred to the merge queue." | |
| exit 0 | |
| fi | |
| for r in "$K8S_RESULT" "$K8S_ETRE_RESULT" "$K8S_VITESS_RESULT"; do | |
| if [ "$r" != "success" ]; then | |
| echo "::error::k8s e2e results: k8s=$K8S_RESULT etre=$K8S_ETRE_RESULT vitess=$K8S_VITESS_RESULT" | |
| exit 1 | |
| fi | |
| done |