|
| 1 | +name: API Compatibility |
| 2 | + |
| 3 | +# This workflow guards the stability of the v1beta1 operator API surface. |
| 4 | +# |
| 5 | +# A breaking CRD schema change (field removal, type change, required-field |
| 6 | +# addition, etc.) fails this check and blocks the PR. If the break is |
| 7 | +# intentional — almost exclusively for graduation to v1beta2 — apply the |
| 8 | +# `api-break-allowed` label to skip the check. See CONTRIBUTING.md → "API |
| 9 | +# Stability" for the full rubric. |
| 10 | + |
| 11 | +on: |
| 12 | + pull_request: |
| 13 | + # Include `labeled` and `unlabeled` so applying or removing |
| 14 | + # `api-break-allowed` triggers a fresh workflow run. Without these, |
| 15 | + # re-running the job from the UI uses the original event payload |
| 16 | + # (which still has the old label set) and the skip condition misfires. |
| 17 | + # Re-evaluating on `unlabeled` closes the gap where a user could |
| 18 | + # apply the label, watch the check skip, then remove the label and |
| 19 | + # merge without the check ever running against the current state. |
| 20 | + types: [opened, synchronize, reopened, labeled, unlabeled] |
| 21 | + paths: |
| 22 | + - 'cmd/thv-operator/api/**' |
| 23 | + # files/crds is the source of truth — controller-gen emits here, and |
| 24 | + # crd-helm-wrapper copies from here into templates/. Any drift in |
| 25 | + # templates/ is caught by operator-ci.yml's generate-crds job, so |
| 26 | + # watching templates/ would be redundant. values.yaml and the |
| 27 | + # crd-helm-wrapper only affect Helm conditionals and annotations the |
| 28 | + # checker ignores, so they can't change what we compare. |
| 29 | + - 'deploy/charts/operator-crds/files/crds/**' |
| 30 | + # Self-exercise when either workflow file (real or no-op companion) |
| 31 | + # changes. The companion file reports the same required check on |
| 32 | + # PRs that don't touch the api surface; see api-compat-noop.yml. |
| 33 | + - '.github/workflows/api-compat*.yml' |
| 34 | + |
| 35 | +permissions: |
| 36 | + contents: read |
| 37 | + |
| 38 | +jobs: |
| 39 | + crd-schema-check: |
| 40 | + name: CRD Schema Compatibility |
| 41 | + runs-on: ubuntu-latest |
| 42 | + # Skip the check entirely when `api-break-allowed` is applied — a |
| 43 | + # required check that is skipped (rather than failed) counts as passing |
| 44 | + # for branch protection, so this is the escape hatch for intentional |
| 45 | + # breaks. Do not remove the label guard without a replacement path. |
| 46 | + if: ${{ !contains(github.event.pull_request.labels.*.name, 'api-break-allowed') }} |
| 47 | + # Expected runtime is ~1 minute (checkout + go setup + git fetch tag + |
| 48 | + # go install + per-CRD checker loop). 10 minutes is a cheap upper |
| 49 | + # bound that protects against a hung go install or git fetch. |
| 50 | + timeout-minutes: 10 |
| 51 | + steps: |
| 52 | + - name: Checkout PR HEAD |
| 53 | + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 |
| 54 | + |
| 55 | + - name: Set up Go |
| 56 | + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 |
| 57 | + with: |
| 58 | + go-version: 'stable' |
| 59 | + cache: true |
| 60 | + |
| 61 | + - name: Resolve baseline tag |
| 62 | + id: baseline |
| 63 | + env: |
| 64 | + GH_TOKEN: ${{ github.token }} |
| 65 | + run: | |
| 66 | + set -euo pipefail |
| 67 | +
|
| 68 | + # Baseline is the most recent release tag. Tags are immutable, so |
| 69 | + # comparing against the tag gives us a stable, released reference |
| 70 | + # without needing to render the Helm chart or pull from OCI. |
| 71 | + # Falling back to origin/main would silently compare against an |
| 72 | + # already-broken baseline once a break lands on main. |
| 73 | + LATEST_TAG="$(gh release list --repo "$GITHUB_REPOSITORY" --limit 1 --json tagName --jq '.[0].tagName')" |
| 74 | + if [ -z "$LATEST_TAG" ]; then |
| 75 | + echo "::error::No releases found for $GITHUB_REPOSITORY; cannot establish an API compatibility baseline." |
| 76 | + exit 1 |
| 77 | + fi |
| 78 | +
|
| 79 | + # Fetch just the tag, shallow — no need to unshallow the repo. |
| 80 | + git fetch origin "refs/tags/$LATEST_TAG:refs/tags/$LATEST_TAG" --depth=1 |
| 81 | + echo "tag=$LATEST_TAG" >> "$GITHUB_OUTPUT" |
| 82 | +
|
| 83 | + - name: Install crd-schema-checker |
| 84 | + # SHA-pinned: openshift/crd-schema-checker has no release tags at the |
| 85 | + # time of writing, so @latest is the only other option. Pinning makes |
| 86 | + # CI deterministic and mitigates supply-chain risk (upstream compromise |
| 87 | + # would otherwise execute attacker code on the runner with GITHUB_TOKEN |
| 88 | + # in env). Bump via a deliberate PR after verifying the new output |
| 89 | + # locally. SHA pinned on 2026-04-21. |
| 90 | + run: go install github.com/openshift/crd-schema-checker/cmd/crd-schema-checker@3fee146022bfe6f4adf84998de35d7267b864bef |
| 91 | + |
| 92 | + - name: Check CRD schema compatibility |
| 93 | + id: checker |
| 94 | + env: |
| 95 | + # Route step outputs through env vars so bash quotes them instead |
| 96 | + # of the runner substituting them directly into the script body. |
| 97 | + # Defense-in-depth against a future edit that routes a |
| 98 | + # PR-controlled string through these outputs. |
| 99 | + BASELINE_TAG: ${{ steps.baseline.outputs.tag }} |
| 100 | + run: | |
| 101 | + set -euo pipefail |
| 102 | +
|
| 103 | + # NoBools and NoMaps are OpenShift API-style conventions, not |
| 104 | + # compat-breaking rules. They fire on fields we legitimately use |
| 105 | + # (e.g. embeddingservers.spec.modelCache.enabled) and drown out |
| 106 | + # real findings. Re-enable only if upstream clarifies breaking- |
| 107 | + # change semantics for them. |
| 108 | + DISABLED_VALIDATORS="NoBools,NoMaps" |
| 109 | +
|
| 110 | + CRD_DIR="deploy/charts/operator-crds/files/crds" |
| 111 | + mkdir -p /tmp/api-compat |
| 112 | + : > /tmp/api-compat/output.txt |
| 113 | +
|
| 114 | + OVERALL_EXIT=0 |
| 115 | +
|
| 116 | + # Detect CRD files removed between baseline and HEAD — a removed |
| 117 | + # CRD is a break that the checker can't report (it needs both |
| 118 | + # inputs present). Compare the set of filenames directly. |
| 119 | + BASELINE_FILES=$(git ls-tree --name-only "$BASELINE_TAG" -- "$CRD_DIR/" | sed "s|$CRD_DIR/||" | sort) |
| 120 | + HEAD_FILES=$(ls "$CRD_DIR" | sort) |
| 121 | + REMOVED=$(comm -23 <(echo "$BASELINE_FILES") <(echo "$HEAD_FILES") || true) |
| 122 | + if [ -n "$REMOVED" ]; then |
| 123 | + { |
| 124 | + echo "ERROR: CRD files removed from HEAD (present at $BASELINE_TAG):" |
| 125 | + echo "$REMOVED" | sed 's/^/ - /' |
| 126 | + } | tee -a /tmp/api-compat/output.txt |
| 127 | + OVERALL_EXIT=1 |
| 128 | + fi |
| 129 | +
|
| 130 | + # For each CRD present on HEAD, fetch the baseline version from the |
| 131 | + # tag and run the checker. New CRDs (HEAD-only) are additive and |
| 132 | + # skipped — note that in the output so reviewers see the full |
| 133 | + # inventory. |
| 134 | + for crd in "$CRD_DIR"/*.yaml; do |
| 135 | + fname=$(basename "$crd") |
| 136 | + rel="$CRD_DIR/$fname" |
| 137 | + if ! git show "$BASELINE_TAG:$rel" > /tmp/api-compat/baseline.yaml 2>/dev/null; then |
| 138 | + echo " (new CRD on HEAD, skipping: $fname)" >> /tmp/api-compat/output.txt |
| 139 | + continue |
| 140 | + fi |
| 141 | + set +e |
| 142 | + crd-schema-checker check-manifests \ |
| 143 | + --existing-crd-filename /tmp/api-compat/baseline.yaml \ |
| 144 | + --new-crd-filename "$crd" \ |
| 145 | + --disabled-validators="$DISABLED_VALIDATORS" \ |
| 146 | + >> /tmp/api-compat/output.txt 2>&1 |
| 147 | + RC=$? |
| 148 | + set -e |
| 149 | + [ "$RC" -ne 0 ] && OVERALL_EXIT=1 |
| 150 | + done |
| 151 | +
|
| 152 | + # Surface the combined output in the step log too, not only in the |
| 153 | + # summary — some reviewers check the raw log first. |
| 154 | + cat /tmp/api-compat/output.txt |
| 155 | +
|
| 156 | + if [ "$OVERALL_EXIT" -eq 0 ]; then |
| 157 | + STATUS="Compatible" |
| 158 | + else |
| 159 | + STATUS="Incompatible or Unknown" |
| 160 | + fi |
| 161 | +
|
| 162 | + { |
| 163 | + echo "## API Compatibility — CRD Schema Check" |
| 164 | + echo "" |
| 165 | + echo "**Baseline**: $BASELINE_TAG" |
| 166 | + echo "**Status**: $STATUS" |
| 167 | + echo "" |
| 168 | + echo "<details><summary>crd-schema-checker output</summary>" |
| 169 | + echo "" |
| 170 | + echo '```' |
| 171 | + cat /tmp/api-compat/output.txt |
| 172 | + echo '```' |
| 173 | + echo "" |
| 174 | + echo "</details>" |
| 175 | + } >> "$GITHUB_STEP_SUMMARY" |
| 176 | +
|
| 177 | + exit "$OVERALL_EXIT" |
0 commit comments