TEST: validate api-break-allowed escape hatch (do not merge) #10
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: API Compatibility | |
| # This workflow guards the stability of the v1beta1 operator API surface. | |
| # | |
| # A breaking CRD schema change (field removal, type change, required-field | |
| # addition, etc.) fails this check and blocks the PR. If the break is | |
| # intentional — almost exclusively for graduation to v1beta2 — apply the | |
| # `api-break-allowed` label to skip the check. See CONTRIBUTING.md → "API | |
| # Stability" for the full rubric. | |
| on: | |
| pull_request: | |
| paths: | |
| - 'cmd/thv-operator/api/**' | |
| # files/crds is the source of truth — controller-gen emits here, and | |
| # crd-helm-wrapper copies from here into templates/. Any drift in | |
| # templates/ is caught by operator-ci.yml's generate-crds job, so | |
| # watching templates/ would be redundant. values.yaml and the | |
| # crd-helm-wrapper only affect Helm conditionals and annotations the | |
| # checker ignores, so they can't change what we compare. | |
| - 'deploy/charts/operator-crds/files/crds/**' | |
| # Self-exercise the workflow when the workflow itself changes. | |
| - '.github/workflows/api-compat.yml' | |
| permissions: | |
| contents: read | |
| jobs: | |
| crd-schema-check: | |
| name: CRD Schema Compatibility | |
| runs-on: ubuntu-latest | |
| # Skip the check entirely when `api-break-allowed` is applied — a | |
| # required check that is skipped (rather than failed) counts as passing | |
| # for branch protection, so this is the escape hatch for intentional | |
| # breaks. Do not remove the label guard without a replacement path. | |
| if: ${{ !contains(github.event.pull_request.labels.*.name, 'api-break-allowed') }} | |
| # Expected runtime is ~1 minute (checkout + go setup + git fetch tag + | |
| # go install + per-CRD checker loop). 10 minutes is a cheap upper | |
| # bound that protects against a hung go install or git fetch. | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout PR HEAD | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| - name: Set up Go | |
| uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 | |
| with: | |
| go-version: 'stable' | |
| cache: true | |
| - name: Resolve baseline tag | |
| id: baseline | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| # Baseline is the most recent release tag. Tags are immutable, so | |
| # comparing against the tag gives us a stable, released reference | |
| # without needing to render the Helm chart or pull from OCI. | |
| # Falling back to origin/main would silently compare against an | |
| # already-broken baseline once a break lands on main. | |
| LATEST_TAG="$(gh release list --repo "$GITHUB_REPOSITORY" --limit 1 --json tagName --jq '.[0].tagName')" | |
| if [ -z "$LATEST_TAG" ]; then | |
| echo "::error::No releases found for $GITHUB_REPOSITORY; cannot establish an API compatibility baseline." | |
| exit 1 | |
| fi | |
| # Fetch just the tag, shallow — no need to unshallow the repo. | |
| git fetch origin "refs/tags/$LATEST_TAG:refs/tags/$LATEST_TAG" --depth=1 | |
| echo "tag=$LATEST_TAG" >> "$GITHUB_OUTPUT" | |
| - name: Install crd-schema-checker | |
| # SHA-pinned: openshift/crd-schema-checker has no release tags at the | |
| # time of writing, so @latest is the only other option. Pinning makes | |
| # CI deterministic and mitigates supply-chain risk (upstream compromise | |
| # would otherwise execute attacker code on the runner with GITHUB_TOKEN | |
| # in env). Bump via a deliberate PR after verifying the new output | |
| # locally. SHA pinned on 2026-04-21. | |
| run: go install github.com/openshift/crd-schema-checker/cmd/crd-schema-checker@3fee146022bfe6f4adf84998de35d7267b864bef | |
| - name: Check CRD schema compatibility | |
| id: checker | |
| env: | |
| # Route step outputs through env vars so bash quotes them instead | |
| # of the runner substituting them directly into the script body. | |
| # Defense-in-depth against a future edit that routes a | |
| # PR-controlled string through these outputs. | |
| BASELINE_TAG: ${{ steps.baseline.outputs.tag }} | |
| run: | | |
| set -euo pipefail | |
| # NoBools and NoMaps are OpenShift API-style conventions, not | |
| # compat-breaking rules. They fire on fields we legitimately use | |
| # (e.g. embeddingservers.spec.modelCache.enabled) and drown out | |
| # real findings. Re-enable only if upstream clarifies breaking- | |
| # change semantics for them. | |
| DISABLED_VALIDATORS="NoBools,NoMaps" | |
| CRD_DIR="deploy/charts/operator-crds/files/crds" | |
| mkdir -p /tmp/api-compat | |
| : > /tmp/api-compat/output.txt | |
| OVERALL_EXIT=0 | |
| # Detect CRD files removed between baseline and HEAD — a removed | |
| # CRD is a break that the checker can't report (it needs both | |
| # inputs present). Compare the set of filenames directly. | |
| BASELINE_FILES=$(git ls-tree --name-only "$BASELINE_TAG" -- "$CRD_DIR/" | sed "s|$CRD_DIR/||" | sort) | |
| HEAD_FILES=$(ls "$CRD_DIR" | sort) | |
| REMOVED=$(comm -23 <(echo "$BASELINE_FILES") <(echo "$HEAD_FILES") || true) | |
| if [ -n "$REMOVED" ]; then | |
| { | |
| echo "ERROR: CRD files removed from HEAD (present at $BASELINE_TAG):" | |
| echo "$REMOVED" | sed 's/^/ - /' | |
| } | tee -a /tmp/api-compat/output.txt | |
| OVERALL_EXIT=1 | |
| fi | |
| # For each CRD present on HEAD, fetch the baseline version from the | |
| # tag and run the checker. New CRDs (HEAD-only) are additive and | |
| # skipped — note that in the output so reviewers see the full | |
| # inventory. | |
| for crd in "$CRD_DIR"/*.yaml; do | |
| fname=$(basename "$crd") | |
| rel="$CRD_DIR/$fname" | |
| if ! git show "$BASELINE_TAG:$rel" > /tmp/api-compat/baseline.yaml 2>/dev/null; then | |
| echo " (new CRD on HEAD, skipping: $fname)" >> /tmp/api-compat/output.txt | |
| continue | |
| fi | |
| set +e | |
| crd-schema-checker check-manifests \ | |
| --existing-crd-filename /tmp/api-compat/baseline.yaml \ | |
| --new-crd-filename "$crd" \ | |
| --disabled-validators="$DISABLED_VALIDATORS" \ | |
| >> /tmp/api-compat/output.txt 2>&1 | |
| RC=$? | |
| set -e | |
| [ "$RC" -ne 0 ] && OVERALL_EXIT=1 | |
| done | |
| # Surface the combined output in the step log too, not only in the | |
| # summary — some reviewers check the raw log first. | |
| cat /tmp/api-compat/output.txt | |
| if [ "$OVERALL_EXIT" -eq 0 ]; then | |
| STATUS="Compatible" | |
| else | |
| STATUS="Incompatible or Unknown" | |
| fi | |
| { | |
| echo "## API Compatibility — CRD Schema Check" | |
| echo "" | |
| echo "**Baseline**: $BASELINE_TAG" | |
| echo "**Status**: $STATUS" | |
| echo "" | |
| echo "<details><summary>crd-schema-checker output</summary>" | |
| echo "" | |
| echo '```' | |
| cat /tmp/api-compat/output.txt | |
| echo '```' | |
| echo "" | |
| echo "</details>" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| exit "$OVERALL_EXIT" |