Skip to content

Commit 75f77f4

Browse files
evgenikoclaude
andauthored
ci(evm): enforce contract version bump checks (#826)
* ci(evm): enforce contract version bump checks * ci(evm): avoid false version-check failure on tagged HEAD * ci(evm): fix tag glob escaping and reject leading-zero semver components Escape the `+` in the workflow tag filter so it matches literally, and tighten is_semver to reject leading-zero components (e.g. 01.2.3) which would cause bash arithmetic to misinterpret them as octal. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * ci(evm): skip version-bump check when PR does not touch src/ Only require the contract version to exceed the latest release tag when the PR actually modifies EVM source files. PRs that only change CI, scripts, or docs now run the sync check (NttManager == WormholeTransceiver) without comparing against the latest tag. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c3774e0 commit 75f77f4

File tree

2 files changed

+211
-0
lines changed

2 files changed

+211
-0
lines changed

.github/workflows/evm.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ on:
66
push:
77
branches:
88
- main
9+
tags:
10+
- 'v*\+evm'
911

1012
env:
1113
FOUNDRY_PROFILE: ci
@@ -20,6 +22,32 @@ defaults:
2022
working-directory: ./evm
2123

2224
jobs:
25+
# Ensure contract version constants stay in sync and are bumped after release
26+
check-version:
27+
name: Check version
28+
runs-on: ubuntu-latest
29+
steps:
30+
- uses: actions/checkout@v6
31+
with:
32+
fetch-depth: 0
33+
34+
- run: |
35+
if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
36+
./scripts/check-version --tag "${GITHUB_REF_NAME}"
37+
else
38+
# If this exact commit is already tagged as an EVM release, verify the
39+
# tag matches. Otherwise, require a version bump only when src/ changed.
40+
head_evm_tag="$(git tag --points-at HEAD --list 'v*+evm' | head -n1)"
41+
if [[ -n "${head_evm_tag}" ]]; then
42+
./scripts/check-version --tag "${head_evm_tag}"
43+
elif git diff --quiet "origin/${GITHUB_BASE_REF:-main}...HEAD" -- src/; then
44+
echo "No EVM source changes; skipping version-bump requirement"
45+
./scripts/check-version
46+
else
47+
./scripts/check-version --require-newer-than-latest-tag
48+
fi
49+
fi
50+
2351
# Fast lint check - no build needed, runs in parallel with other jobs
2452
lint:
2553
name: forge fmt

evm/scripts/check-version

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
#!/usr/bin/env bash
2+
3+
set -euo pipefail
4+
5+
cd "$(dirname "$0")"/..
6+
7+
failed=0
8+
tag=""
9+
require_newer_than_latest_tag=false
10+
11+
usage() {
12+
echo "Usage: $0 [--tag vX.Y.Z+evm] [--require-newer-than-latest-tag]" >&2
13+
}
14+
15+
is_semver() {
16+
[[ "$1" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]
17+
}
18+
19+
version_gt() {
20+
local left="$1"
21+
local right="$2"
22+
local l1 l2 l3 r1 r2 r3
23+
24+
IFS='.' read -r l1 l2 l3 <<< "$left"
25+
IFS='.' read -r r1 r2 r3 <<< "$right"
26+
27+
if (( l1 > r1 )); then return 0; fi
28+
if (( l1 < r1 )); then return 1; fi
29+
if (( l2 > r2 )); then return 0; fi
30+
if (( l2 < r2 )); then return 1; fi
31+
if (( l3 > r3 )); then return 0; fi
32+
return 1
33+
}
34+
35+
latest_evm_tag_version() {
36+
local latest_version=""
37+
local tag_name
38+
local tag_version
39+
40+
while IFS= read -r tag_name; do
41+
tag_version="${tag_name#v}"
42+
tag_version="${tag_version%%+*}"
43+
44+
if ! is_semver "$tag_version"; then
45+
continue
46+
fi
47+
48+
if [[ -z "$latest_version" ]] || version_gt "$tag_version" "$latest_version"; then
49+
latest_version="$tag_version"
50+
fi
51+
done < <(git tag --list 'v*+evm')
52+
53+
echo "$latest_version"
54+
}
55+
56+
extract_version() {
57+
local file="$1"
58+
local constant="$2"
59+
local matches
60+
local match_count
61+
62+
if [[ ! -f "$file" ]]; then
63+
echo "Error: $file not found" >&2
64+
return 1
65+
fi
66+
67+
matches=$(
68+
sed -nE \
69+
"s/^[[:space:]]*string[[:space:]]+public[[:space:]]+constant[[:space:]]+$constant[[:space:]]*=[[:space:]]*\"([^\"]+)\";[[:space:]]*$/\\1/p" \
70+
"$file"
71+
)
72+
73+
if [[ -z "$matches" ]]; then
74+
echo "Error: could not parse $constant in $file" >&2
75+
echo " Expected format: string public constant $constant = \"X.Y.Z\";" >&2
76+
return 1
77+
fi
78+
79+
match_count=$(printf '%s\n' "$matches" | wc -l | tr -d ' ')
80+
if [[ "$match_count" -ne 1 ]]; then
81+
echo "Error: found multiple $constant definitions in $file" >&2
82+
return 1
83+
fi
84+
85+
echo "$matches"
86+
}
87+
88+
while [[ $# -gt 0 ]]; do
89+
case "$1" in
90+
--tag)
91+
if [[ -n "$tag" ]]; then
92+
echo "Error: --tag specified multiple times" >&2
93+
usage
94+
exit 1
95+
fi
96+
tag="${2:-}"
97+
if [[ -z "$tag" ]]; then
98+
echo "Error: --tag requires a tag name argument (e.g. v1.2.0+evm)" >&2
99+
usage
100+
exit 1
101+
fi
102+
shift 2
103+
;;
104+
--require-newer-than-latest-tag)
105+
require_newer_than_latest_tag=true
106+
shift
107+
;;
108+
*)
109+
usage
110+
exit 1
111+
;;
112+
esac
113+
done
114+
115+
if [[ -n "$tag" && "$require_newer_than_latest_tag" == true ]]; then
116+
echo "Error: --tag and --require-newer-than-latest-tag cannot be used together" >&2
117+
usage
118+
exit 1
119+
fi
120+
121+
manager_version=$(extract_version \
122+
"src/NttManager/NttManager.sol" \
123+
"NTT_MANAGER_VERSION")
124+
125+
transceiver_version=$(extract_version \
126+
"src/Transceiver/WormholeTransceiver/WormholeTransceiver.sol" \
127+
"WORMHOLE_TRANSCEIVER_VERSION")
128+
129+
echo "NttManager version: $manager_version"
130+
echo "WormholeTransceiver version: $transceiver_version"
131+
132+
if [[ "$manager_version" != "$transceiver_version" ]]; then
133+
echo "Error: NttManager version ($manager_version) does not match WormholeTransceiver version ($transceiver_version)" >&2
134+
failed=$((failed + 1))
135+
fi
136+
137+
if [[ "$require_newer_than_latest_tag" == true ]]; then
138+
latest_version="$(latest_evm_tag_version)"
139+
140+
if [[ -z "$latest_version" ]]; then
141+
echo "Error: no EVM tags found matching v*+evm" >&2
142+
echo " Ensure tags are fetched (e.g. actions/checkout with fetch-depth: 0)" >&2
143+
failed=$((failed + 1))
144+
elif ! is_semver "$manager_version"; then
145+
echo "Error: contract version is not a valid semver: $manager_version" >&2
146+
failed=$((failed + 1))
147+
elif ! version_gt "$manager_version" "$latest_version"; then
148+
echo "Error: contract version ($manager_version) is not newer than latest EVM tag version ($latest_version)" >&2
149+
echo " Bump NTT_MANAGER_VERSION and WORMHOLE_TRANSCEIVER_VERSION after each EVM release" >&2
150+
failed=$((failed + 1))
151+
else
152+
echo "Contract version is newer than latest EVM tag version: $latest_version"
153+
fi
154+
fi
155+
156+
if [[ -n "$tag" ]]; then
157+
if [[ ! "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(\+.+)?$ ]]; then
158+
echo "Error: invalid tag format: $tag" >&2
159+
echo " Expected format: vX.Y.Z or vX.Y.Z+suffix (e.g. v1.2.0+evm)" >&2
160+
failed=$((failed + 1))
161+
else
162+
tag_version="${tag#v}"
163+
tag_version="${tag_version%%+*}"
164+
165+
if [[ "$manager_version" != "$tag_version" ]]; then
166+
echo "Error: Contract version ($manager_version) does not match tag version ($tag_version)" >&2
167+
echo " Update NTT_MANAGER_VERSION in NttManager.sol to \"$tag_version\"" >&2
168+
echo " Update WORMHOLE_TRANSCEIVER_VERSION in WormholeTransceiver.sol to \"$tag_version\"" >&2
169+
failed=$((failed + 1))
170+
else
171+
echo "Tag version matches: $tag_version"
172+
fi
173+
fi
174+
fi
175+
176+
if [[ $failed -gt 0 ]]; then
177+
echo ""
178+
echo "Version check failed with $failed error(s)" >&2
179+
exit 1
180+
fi
181+
182+
echo ""
183+
echo "Version check passed"

0 commit comments

Comments
 (0)