From 8ca3c3c67b47f6d96f51e88575113f8d75fd875a Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Mon, 5 Oct 2026 05:20:26 +0000 Subject: [PATCH] ci: share the GCP deploy manifest validation with the PR gate The GCP deploy Validate job never runs on pull requests, and the PR-tier Lint (k8s schemas) job validates with -ignore-missing-schemas, so it silently passes schema-less kinds. A GKE CRD in the kustomize base (#2475) therefore passed review and then failed every GCP tenant deploy at Validate. Move the deploy gate's exact validation (pinned kubectl and kubeconform, the overlay plus committed edge manifest render, -strict) into one local composite action. The deploy Validate job calls it for its tenant and the PR gate calls it for every GCP tenant, so a PR is rejected for exactly what a deploy rejects. --- .../actions/validate-gcp-manifests/action.yml | 78 +++++++++++++++++++ .github/quality-path-filters.yaml | 2 + .github/workflows/_gcp-dev.yml | 29 ++----- .github/workflows/_quality.yml | 8 ++ 4 files changed, 93 insertions(+), 24 deletions(-) create mode 100644 .github/actions/validate-gcp-manifests/action.yml diff --git a/.github/actions/validate-gcp-manifests/action.yml b/.github/actions/validate-gcp-manifests/action.yml new file mode 100644 index 000000000..7de6a67c3 --- /dev/null +++ b/.github/actions/validate-gcp-manifests/action.yml @@ -0,0 +1,78 @@ +name: Validate GCP platform manifests +description: >- + Render each GCP tenant's platform manifest stream (kustomize overlay plus the + committed edge manifest) and validate it with kubeconform -strict. This is the + single definition shared by the deploy Validate job (_gcp-dev.yml) and the + PR-tier Lint (k8s schemas) quality job, so a pull request is rejected for + exactly what a deploy would reject. GKE CRDs (BackendConfig, FrontendConfig, + ManagedCertificate) have no schema here by design: they are rendered at deploy + time by scripts/gcp/render_edge_manifest.py, never in the kustomize base. + +inputs: + environments: + description: Space-separated GCP overlay names under platform/k8s/gcp/overlays, or "all". + required: true + +runs: + using: composite + steps: + - uses: azure/setup-kubectl@776406bce94f63e41d621b960d78ee25c8b76ede + with: + version: v1.34.1 + + - name: Install kubeconform + shell: bash + env: + KUBECONFORM_VERSION: v0.7.0 + run: | + set -euo pipefail + # ADR-037-R3: verify the download against the release CHECKSUMS + # manifest before extraction. The archive is saved under the exact name + # the manifest references so `sha256sum -c` matches. + dir="${RUNNER_TEMP}/kubeconform-gcp" + mkdir -p "${dir}" + curl -fsSL -o "${dir}/kubeconform-linux-amd64.tar.gz" \ + "https://github.com/yannh/kubeconform/releases/download/${KUBECONFORM_VERSION}/kubeconform-linux-amd64.tar.gz" + curl -fsSL -o "${dir}/CHECKSUMS" \ + "https://github.com/yannh/kubeconform/releases/download/${KUBECONFORM_VERSION}/CHECKSUMS" + (cd "${dir}" && grep "kubeconform-linux-amd64.tar.gz" CHECKSUMS | sha256sum -c -) + tar -xzf "${dir}/kubeconform-linux-amd64.tar.gz" -C "${dir}" kubeconform + + - name: Render and validate + shell: bash + env: + ENVIRONMENTS: ${{ inputs.environments }} + run: | + set -euo pipefail + overlays_root="platform/k8s/gcp/overlays" + if [ "${ENVIRONMENTS}" = "all" ]; then + environments=$(find "${overlays_root}" -mindepth 2 -maxdepth 2 -name kustomization.yaml \ + -exec dirname {} \; | xargs -n1 basename | sort) + else + environments="${ENVIRONMENTS}" + fi + if [ -z "${environments}" ]; then + echo "::error::No GCP overlays to validate" + exit 1 + fi + + failed=0 + for environment in ${environments}; do + if ! [[ "${environment}" =~ ^[a-z0-9-]+$ ]] || [ ! -f "${overlays_root}/${environment}/kustomization.yaml" ]; then + echo "::error::Unknown GCP overlay '${environment}'" + failed=1 + continue + fi + rendered="${RUNNER_TEMP}/gcp-platform-${environment}.yaml" + ( + kubectl kustomize "${overlays_root}/${environment}" + printf '\n---\n' + cat "${overlays_root}/${environment}/platform-edge.generated.yaml" + ) > "${rendered}" + echo "Validating ${environment}" + if ! "${RUNNER_TEMP}/kubeconform-gcp/kubeconform" -strict -summary "${rendered}"; then + echo "::error::GCP overlay '${environment}' failed strict manifest validation" + failed=1 + fi + done + exit "${failed}" diff --git a/.github/quality-path-filters.yaml b/.github/quality-path-filters.yaml index c338964e3..d8669ed1b 100644 --- a/.github/quality-path-filters.yaml +++ b/.github/quality-path-filters.yaml @@ -44,6 +44,8 @@ schema_version: 1 force_full_matrix: - .github/workflows/** + # Local composite actions are CI control plane called by the workflows. + - .github/actions/** - .github/quality-path-filters.yaml quality_units: diff --git a/.github/workflows/_gcp-dev.yml b/.github/workflows/_gcp-dev.yml index 55c4f4928..9f2f9e346 100644 --- a/.github/workflows/_gcp-dev.yml +++ b/.github/workflows/_gcp-dev.yml @@ -73,9 +73,6 @@ jobs: name: Validate (${{ inputs.environment }}) # Validation does not need deploy-network access; keep PR checks off the deploy runner pool. runs-on: ubuntu-latest - env: - GCP_ENVIRONMENT: ${{ inputs.environment }} - KUBECONFORM_VERSION: v0.7.0 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: @@ -83,33 +80,17 @@ jobs: - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd with: terraform_version: "1.16.1" - - uses: azure/setup-kubectl@776406bce94f63e41d621b960d78ee25c8b76ede - with: - version: v1.34.1 - - name: Install kubeconform - run: | - # ADR-037-R3: verify the download against the release CHECKSUMS - # manifest before extraction. The archive is saved under the exact name - # the manifest references so `sha256sum -c` matches. - curl -fsSL -o /tmp/kubeconform-linux-amd64.tar.gz \ - "https://github.com/yannh/kubeconform/releases/download/${KUBECONFORM_VERSION}/kubeconform-linux-amd64.tar.gz" - curl -fsSL -o /tmp/kubeconform.CHECKSUMS \ - "https://github.com/yannh/kubeconform/releases/download/${KUBECONFORM_VERSION}/CHECKSUMS" - (cd /tmp && grep "kubeconform-linux-amd64.tar.gz" kubeconform.CHECKSUMS | sha256sum -c -) - tar -xzf /tmp/kubeconform-linux-amd64.tar.gz -C /tmp kubeconform - run: terraform fmt -check -recursive working-directory: platform/terraform/gcp - run: terraform init -backend=false working-directory: platform/terraform/gcp/environments/${{ inputs.environment }} - run: terraform validate working-directory: platform/terraform/gcp/environments/${{ inputs.environment }} - - run: | - ( - kubectl kustomize "platform/k8s/gcp/overlays/${GCP_ENVIRONMENT}" - printf '\n---\n' - cat "platform/k8s/gcp/overlays/${GCP_ENVIRONMENT}/platform-edge.generated.yaml" - ) > /tmp/gcp-platform.yaml - - run: /tmp/kubeconform -strict -summary /tmp/gcp-platform.yaml + # Shared with the PR-tier Lint (k8s schemas) job so a PR is rejected for + # exactly what this deploy gate rejects. + - uses: ./.github/actions/validate-gcp-manifests + with: + environments: ${{ inputs.environment }} prepare: # Runs on the requested GCP tenant's same-named runner, provisioned by diff --git a/.github/workflows/_quality.yml b/.github/workflows/_quality.yml index c72455d13..52dbf3f26 100644 --- a/.github/workflows/_quality.yml +++ b/.github/workflows/_quality.yml @@ -1157,6 +1157,14 @@ jobs: -kubernetes-version 1.31.0 \ "${RUNNER_TEMP}/rendered-k8s/"*.yaml + # The roots check above skips schema-less kinds (-ignore-missing-schemas), + # so on its own it passes manifests the GCP deploy gate rejects. Run the + # deploy gate's exact validation (shared action) over every GCP tenant. + - name: Validate GCP tenants exactly as the deploy gate does + uses: ./.github/actions/validate-gcp-manifests + with: + environments: all + # =========================================================================== # Type Checking (blocking quality gate, #564) #