diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml index 8f5143407..0d7328c83 100644 --- a/.github/workflows/CI.yaml +++ b/.github/workflows/CI.yaml @@ -304,6 +304,23 @@ jobs: - name: Ensure clean modgraph run: git diff --minimal --exit-code + validate-module-refs: + runs-on: ubuntu-latest + name: validate-module-refs + env: + BASE_BRANCH: ${{ github.base_ref || github.event.repository.default_branch || 'main' }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Fetch base branch + run: git fetch --no-tags origin "${BASE_BRANCH}:refs/remotes/origin/${BASE_BRANCH}" + + - name: Validate intra-repo module pins are reachable from the default branch + run: ./script/validate-intra-module-refs.sh "origin/${BASE_BRANCH}" + checks: runs-on: ubuntu-latest needs: @@ -314,6 +331,7 @@ jobs: - tidy-and-generate - integration-tests - modgraph + - validate-module-refs if: always() steps: - name: All tests ok diff --git a/Makefile b/Makefile index 09b98e243..c2212e6c3 100644 --- a/Makefile +++ b/Makefile @@ -36,6 +36,10 @@ generate: protoc mockery gomods ## Execute all go:generate commands (including p update-common-capabilities: ## Update chain_capabilities/common in aptos/evm/solana. Usage: make update-common-capabilities REF= ./script/update-common-capabilities.sh $(REF) +.PHONY: validate-module-refs +validate-module-refs: ## Verify intra-repo module pins are reachable from the default branch. Usage: make validate-module-refs [BASE_REF=origin/main] + ./script/validate-intra-module-refs.sh $(BASE_REF) + .PHONY: help help: ## Display this help screen. @grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' diff --git a/script/validate-intra-module-refs.sh b/script/validate-intra-module-refs.sh new file mode 100755 index 000000000..19f61a90d --- /dev/null +++ b/script/validate-intra-module-refs.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +# +# Validate that every intra-repo module pin resolves to a commit reachable +# from the default branch. +# +# This repo is a multi-module monorepo: modules require sibling modules +# (e.g. chain_capabilities/evm -> libs, -> chain_capabilities/common) by +# pseudo-version (vX.Y.Z--<12-hex-sha>). There are no `replace` +# directives wiring these locally, so the pinned SHA must be reachable from +# the default branch, or external consumers (and fresh builds here) break +# with "unknown revision". +# +# This guards against pinning to a non-default-branch commit (a PR/feature +# commit), which is ephemeral: once the branch is gone, the SHA is GC'd and +# every consumer fails. See `make update-common-capabilities`, which should +# only ever be pointed at a commit already on the default branch. +# +# Usage: validate-intra-module-refs.sh [base-ref] +# base-ref defaults to origin/main. +set -euo pipefail + +BASE_REF="${1:-origin/main}" +MODULE_PREFIX="github.com/smartcontractkit/capabilities" +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$REPO_ROOT" + +if ! git rev-parse --verify --quiet "$BASE_REF" >/dev/null; then + echo "error: base ref '$BASE_REF' not found. Fetch it first (git fetch origin main)." >&2 + exit 2 +fi + +fail=0 +seen_file="$(mktemp)" +trap 'rm -f "$seen_file"' EXIT + +# All go.mod files except vendored ones. +while IFS= read -r gomod; do + # Lines that pin an intra-repo module to a pseudo-version. Matches both + # require lines and `=> module version` replace targets. + while IFS= read -r line; do + # module is the token starting with the repo prefix; version is the next token. + module="$(printf '%s\n' "$line" | grep -oE "${MODULE_PREFIX}[a-zA-Z0-9._/-]*" | head -1)" + version="$(printf '%s\n' "$line" | grep -oE "v[0-9][^[:space:]]*" | head -1)" + [ -z "$module" ] && continue + [ -z "$version" ] && continue + + # Only pseudo-versions carry a -<14-digit-timestamp>-<12-hex> suffix. + sha="$(printf '%s\n' "$version" | grep -oE '\-[0-9]{14}-[0-9a-f]{12}$' | grep -oE '[0-9a-f]{12}$' || true)" + [ -z "$sha" ] && continue # tagged release, not a pseudo-version + [ "$sha" = "000000000000" ] && continue # zero pseudo-version (replaced module) + + key="${module}@${sha}" + grep -qxF "$key" "$seen_file" && continue + printf '%s\n' "$key" >> "$seen_file" + + # Reachable from the default branch? Treat "not an ancestor" and + # "object missing/GC'd" alike as failures. + if git merge-base --is-ancestor "$sha" "$BASE_REF" 2>/dev/null; then + echo "ok ${key} (reachable from ${BASE_REF}, ${gomod#./})" + else + echo "FAIL ${key} (NOT reachable from ${BASE_REF}; pinned in ${gomod#./})" + fail=1 + fi + done < <(grep -E "${MODULE_PREFIX}[a-zA-Z0-9._/-]* v[0-9]" "$gomod" || true) +done < <(find . -name go.mod -not -path '*/vendor/*' -type f | sort) + +if [ "$fail" -ne 0 ]; then + cat >&2 <). +EOF + exit 1 +fi + +echo "All intra-repo module pins are reachable from ${BASE_REF}."