Skip to content

feat(webhook): dispatch unlock commands through the durable inbox #5297

feat(webhook): dispatch unlock commands through the durable inbox

feat(webhook): dispatch unlock commands through the durable inbox #5297

Workflow file for this run

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