diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3d0bb37ca..2cdd06e0c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -187,6 +187,7 @@ jobs: - 'operator/config/**' - 'charts/gameplane/crds/**' - 'charts/gameplane/crd-manifests/**' + - 'hack/check-codegen-drift_test.sh' ci: - '.github/workflows/ci.yaml' - '.github/actions/**' @@ -517,6 +518,32 @@ jobs: - name: test up.sh registry/context bootstrap run: make test-up-registry + # Spec 013 G5: regenerated files (deepcopy, operator/config/{crd,rbac}, chart + # CRDs, go.mod/go.sum) must be committed with the change that needs them. The + # proof step shows the gate goes red on a types edit committed without regenerating. + codegen-drift: + name: codegen drift + needs: [changes] + if: needs.changes.outputs.codegen == 'true' || github.event_name == 'push' + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: ./.github/actions/go-cache + with: + key-suffix: codegen-drift + - uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1 + - name: regenerate deepcopy, CRDs, RBAC and chart CRD copies + run: make generate manifests + - name: go mod tidy across modules + run: make tidy + - name: fail on drift + run: git diff --exit-code + - name: prove the gate fails on a stale types edit + run: ./hack/check-codegen-drift_test.sh + lint: name: lint (${{ matrix.module }}) needs: [changes] @@ -1957,6 +1984,7 @@ jobs: build-images-arm64, capture-sidecar-setcap-proof, docs, + codegen-drift, lint, go, web, @@ -2068,7 +2096,7 @@ jobs: const NEEDS_ORDER = [ 'changes', 'build-images', 'build-images-arm64', - 'capture-sidecar-setcap-proof', 'docs', 'lint', 'go', + 'capture-sidecar-setcap-proof', 'docs', 'codegen-drift', 'lint', 'go', 'web', 'web-e2e-mock', 'helm', 'chart-template', 'workflow-lint', 'go-e2e-unit', 'api-postgres', 'e2e-buckets', 'e2e-go', 'e2e-go-arm64', 'e2e-multicluster', 'e2e-multicluster-arm64', 'e2e-upgrade', 'e2e-upgrade-arm64', @@ -2084,6 +2112,7 @@ jobs: 'build-images-arm64': (n) => n === 'build e2e images (arm64)', 'capture-sidecar-setcap-proof': (n) => n === 'capture-sidecar CAP_NET_RAW survival', docs: (n) => n === 'docs and repository checks', + 'codegen-drift': (n) => n === 'codegen drift', lint: (n) => n.startsWith('lint ('), go: (n) => n.startsWith('go (') && n !== 'go (test/e2e)', web: (n) => n === 'web', diff --git a/go.work.sum b/go.work.sum index a5c49ede0..85adc7e80 100644 --- a/go.work.sum +++ b/go.work.sum @@ -457,6 +457,7 @@ github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjz github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/otlptranslator v0.0.2/go.mod h1:P8AwMgdD7XEr6QRUJ2QWLpiAZTgTE2UYgjlu3svompI= github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM= github.com/prometheus/procfs v0.21.0/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY= diff --git a/hack/check-codegen-drift_test.sh b/hack/check-codegen-drift_test.sh new file mode 100755 index 000000000..e200a884a --- /dev/null +++ b/hack/check-codegen-drift_test.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +# check-codegen-drift_test.sh: Proof that the "codegen drift" CI gate fails on a +# types edit committed without regenerating (spec 013 G5, +# specs/013-expand-test-coverage/contracts/static-gates.md). +# +# The gate is: make generate manifests && make tidy && git diff --exit-code +# run inside a checkout. This script runs it in a throwaway detached worktree: +# +# Case 1 (must FAIL): a probe commit edits +# operator/api/v1alpha1/gameserver_types.go (doc comment + json tag of +# Suspend) without regenerating. The gate must exit non-zero. +# Case 2 (must PASS): the probe commit is dropped; the clean checkout must +# pass the gate. +# +# The main working tree is never modified. The worktree and temp dir are +# removed on exit. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR/.." +REPO_ROOT="$(pwd)" + +TYPES_FILE="operator/api/v1alpha1/gameserver_types.go" +TAG_OLD='json:"suspend,omitempty"' +TAG_NEW='json:"suspendDriftProbe,omitempty"' +DOC_LINE='// DriftProbe: doc comment added by hack/check-codegen-drift_test.sh.' + +tmp="$(mktemp -d)" +wt="$tmp/wt" + +cleanup() { + git -C "$REPO_ROOT" worktree remove --force "$wt" >/dev/null 2>&1 || true + rm -rf "$tmp" +} +trap cleanup EXIT + +# gate DIR: regenerate, tidy, then require a clean tree. +gate() { + local dir="$1" + (cd "$dir" && make generate manifests && make tidy && git diff --exit-code) +} + +git worktree add --detach "$wt" HEAD >/dev/null + +echo "== Case 1: stale types edit committed without regenerating (expect FAIL)" + +# Insert the doc comment above the first `Suspend bool` field and rename its +# json tag, on that same line only. +line_no="$(grep -n -m1 -F " Suspend bool \`$TAG_OLD\`" "$wt/$TYPES_FILE" | cut -d: -f1 || true)" +if [[ -z "$line_no" ]]; then + echo "✗ could not find the Suspend field in $TYPES_FILE" >&2 + exit 1 +fi +awk -v ln="$line_no" -v old="$TAG_OLD" -v new="$TAG_NEW" -v doc=" $DOC_LINE" ' + NR == ln { + print doc + sub(old, new) + } + { print } +' "$wt/$TYPES_FILE" > "$tmp/types.go" +cat "$tmp/types.go" > "$wt/$TYPES_FILE" + +if ! grep -q 'suspendDriftProbe' "$wt/$TYPES_FILE" || ! grep -q 'DriftProbe:' "$wt/$TYPES_FILE"; then + echo "✗ probe edit did not apply to $TYPES_FILE" >&2 + exit 1 +fi + +git -C "$wt" -c user.name=drift-probe -c user.email=drift-probe@invalid \ + commit -q --no-gpg-sign -am "drift probe" + +# Regeneration itself must succeed, so the failure below is the drift diff +# and not a broken toolchain. +(cd "$wt" && make generate manifests && make tidy) +if git -C "$wt" diff --exit-code; then + echo "✗ gate passed on a stale types edit" >&2 + exit 1 +fi +echo "✓ gate failed on a stale types edit" + +echo +echo "== Case 2: clean checkout (expect PASS)" + +git -C "$wt" reset -q --hard HEAD~1 +git -C "$wt" clean -fdq + +if ! gate "$wt"; then + echo "✗ gate failed on a clean checkout" >&2 + exit 1 +fi +echo "✓ gate passed on a clean checkout" diff --git a/specs/013-expand-test-coverage/coverage-gaps.md b/specs/013-expand-test-coverage/coverage-gaps.md index fbf685c0d..75e557132 100644 --- a/specs/013-expand-test-coverage/coverage-gaps.md +++ b/specs/013-expand-test-coverage/coverage-gaps.md @@ -1,7 +1,7 @@ # Coverage Gap Record -**Summary**: 2/24 closed · 0 closed unproven · 2 in progress · 19 open · 1 unit-only by design -**Last updated**: 2026-10-11 +**Summary**: 3/24 closed · 0 closed unproven · 2 in progress · 18 open · 1 unit-only by design +**Last updated**: 2026-10-12 | ID | Category | Gap | Story | FR | Priority | Status | Evidence | Notes | |---|---|---|---|---|---|---|---|---| @@ -9,7 +9,7 @@ | G-02 | static | container image scan gate | US1 | FR-001 | P1 | open | | | | G-03 | static | Dockerfile lint gate | US1 | FR-001 | P1 | open | | | | G-04 | static | dependency-review gate | US1 | FR-001 | P1 | open | | | -| G-05 | static | codegen / tidy drift gate | US1 | FR-001 | P1 | open | | | +| G-05 | static | codegen / tidy drift gate | US1 | FR-001 | P1 | closed | `codegen drift` https://github.com/GameplanePanel/Gameplane/actions/runs/38189397259 | First run on #672 found no drift; the proof shows the job fails on injected drift. | | G-06 | static | submodule pointer freshness gate | US1 | FR-001 | P1 | open | | | | G-07 | e2e-dashboard | live: notification webhook config | US2 | FR-002 | P1 | open | | | | G-08 | e2e-dashboard | live: user create + role assignment | US2 | FR-002 | P1 | open | | | diff --git a/specs/013-expand-test-coverage/tasks.md b/specs/013-expand-test-coverage/tasks.md index 4b8252f39..b0a7d5cf6 100644 --- a/specs/013-expand-test-coverage/tasks.md +++ b/specs/013-expand-test-coverage/tasks.md @@ -105,19 +105,20 @@ description: "Task list for 013-expand-test-coverage" ### G5 codegen drift: PR `ci: codegen drift gate` -- [ ] T012 [P] [US1] Write `hack/check-codegen-drift_test.sh`. In a `git worktree add` temp checkout it: +- [X] T012 [P] [US1] Write `hack/check-codegen-drift_test.sh`. In a `git worktree add` temp checkout it: 1. Appends a doc comment to a field in `operator/api/v1alpha1/gameserver_types.go`, edits the struct tag on that field, runs `make generate manifests && make tidy && git diff --exit-code`, and expects a non-zero exit. 2. Resets the checkout, reruns the same command, and expects zero. It removes the worktree on exit. -- [ ] T013 [US1] Add a `codegen-drift` job named `codegen drift` to `.github/workflows/ci.yaml`. Settings: +- [X] T013 [US1] Add a `codegen-drift` job named `codegen drift` to `.github/workflows/ci.yaml`. Settings: - `needs: [changes]` - `if: needs.changes.outputs.codegen == 'true' || github.event_name == 'push'` - setup: `actions/checkout`, `./.github/actions/go-cache`, `azure/setup-helm` - steps: `make generate manifests`, then `make tidy`, then `git diff --exit-code`, then `./hack/check-codegen-drift_test.sh` Register the job in `report`. Depends on T007 and T012. -- [ ] T014 [US1] If the first CI run of T013 shows drift, commit the regenerated files the job printed: deepcopy, `operator/config/**`, chart CRDs, `go.mod`/`go.sum`. Flip gap row G-05 with the job name and run link. +- [X] T014 [US1] If the first CI run of T013 shows drift, commit the regenerated files the job printed: deepcopy, `operator/config/**`, chart CRDs, `go.mod`/`go.sum`. Flip gap row G-05 with the job name and run link. + *Implementation note (2026-10-12)*: the first run of `codegen drift` (run 38189397259) found no drift, so there was nothing to commit. ### G1 govulncheck: PR `ci: go vulnerability gate`