From bb6b207197b9ddbee8692738908cc54f595afe91 Mon Sep 17 00:00:00 2001 From: Elias Tazartes Date: Fri, 13 Mar 2026 12:09:39 +0100 Subject: [PATCH 01/13] feat(ci): add contract upgrade hygiene check Add a lightweight CI workflow that compares PR contract bytecodes against main and fails fast if bytecode changed without proper version bumps. When bytecode changes, the check asserts: - REINITIALIZER_VERSION was bumped - reinitializeVN() function exists (N = REINITIALIZER_VERSION - 1) - Semantic version (MAJOR/MINOR/PATCH) was bumped When bytecode is unchanged, asserts reinitializer was NOT bumped. Covers both host-contracts and gateway-contracts via a shared script. Runs independently of existing upgrade-tests workflows (no Docker/Anvil). Also updates both foundry.toml files with cbor_metadata=false and bytecode_hash='none' for deterministic path-independent bytecode, and adds OZ remappings to gateway-contracts so forge can compile it. --- .../workflows/contracts-upgrade-hygiene.yml | 149 ++++++++++++++++++ ci/check-upgrade-hygiene.sh | 139 ++++++++++++++++ gateway-contracts/foundry.toml | 7 + host-contracts/foundry.toml | 2 + 4 files changed, 297 insertions(+) create mode 100644 .github/workflows/contracts-upgrade-hygiene.yml create mode 100755 ci/check-upgrade-hygiene.sh diff --git a/.github/workflows/contracts-upgrade-hygiene.yml b/.github/workflows/contracts-upgrade-hygiene.yml new file mode 100644 index 0000000000..5480de509f --- /dev/null +++ b/.github/workflows/contracts-upgrade-hygiene.yml @@ -0,0 +1,149 @@ +name: contracts-upgrade-hygiene + +permissions: {} + +on: + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + check-changes: + name: contracts-upgrade-hygiene/check-changes + permissions: + contents: 'read' + pull-requests: 'read' + runs-on: ubuntu-latest + outputs: + host-contracts: ${{ steps.filter.outputs.host-contracts }} + gateway-contracts: ${{ steps.filter.outputs.gateway-contracts }} + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: 'false' + - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + id: filter + with: + filters: | + host-contracts: + - .github/workflows/contracts-upgrade-hygiene.yml + - ci/check-upgrade-hygiene.sh + - host-contracts/** + gateway-contracts: + - .github/workflows/contracts-upgrade-hygiene.yml + - ci/check-upgrade-hygiene.sh + - gateway-contracts/** + + host-contracts: + name: contracts-upgrade-hygiene/host-contracts + needs: check-changes + if: ${{ needs.check-changes.outputs.host-contracts == 'true' }} + permissions: + contents: 'read' + runs-on: ubuntu-latest + steps: + - name: Checkout PR branch + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: 'false' + + - name: Checkout main branch + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: main + path: main-branch + persist-credentials: 'false' + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@82dee4ba654bd2146511f85f0d013af94670c4de # v1.4.0 + + - name: Install PR dependencies + working-directory: host-contracts + run: | + npm ci + forge soldeer install + + - name: Install main dependencies + working-directory: main-branch/host-contracts + run: | + npm ci + forge soldeer install + + - name: Generate address stubs + run: | + # Host contracts need addresses/FHEVMHostAddresses.sol (generated at deploy time). + # Create identical stubs in both copies so bytecode comparison is not affected. + mkdir -p host-contracts/addresses main-branch/host-contracts/addresses + cat > /tmp/FHEVMHostAddresses.sol << 'SOL' + // SPDX-License-Identifier: BSD-3-Clause-Clear + pragma solidity ^0.8.24; + address constant aclAdd = 0x0000000000000000000000000000000000000001; + address constant fhevmExecutorAdd = 0x0000000000000000000000000000000000000002; + address constant kmsVerifierAdd = 0x0000000000000000000000000000000000000003; + address constant inputVerifierAdd = 0x0000000000000000000000000000000000000004; + address constant hcuLimitAdd = 0x0000000000000000000000000000000000000005; + address constant pauserSetAdd = 0x0000000000000000000000000000000000000006; + SOL + cp /tmp/FHEVMHostAddresses.sol host-contracts/addresses/FHEVMHostAddresses.sol + cp /tmp/FHEVMHostAddresses.sol main-branch/host-contracts/addresses/FHEVMHostAddresses.sol + + - name: Run upgrade hygiene check + run: | + chmod +x ci/check-upgrade-hygiene.sh + ./ci/check-upgrade-hygiene.sh main-branch/host-contracts host-contracts + + gateway-contracts: + name: contracts-upgrade-hygiene/gateway-contracts + needs: check-changes + if: ${{ needs.check-changes.outputs.gateway-contracts == 'true' }} + permissions: + contents: 'read' + runs-on: ubuntu-latest + steps: + - name: Checkout PR branch + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: 'false' + + - name: Checkout main branch + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: main + path: main-branch + persist-credentials: 'false' + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@82dee4ba654bd2146511f85f0d013af94670c4de # v1.4.0 + + - name: Install PR dependencies + working-directory: gateway-contracts + run: npm ci + + - name: Install main dependencies + working-directory: main-branch/gateway-contracts + run: npm ci + + - name: Generate address stubs + run: | + # Gateway contracts need addresses/GatewayAddresses.sol (generated at deploy time). + mkdir -p gateway-contracts/addresses main-branch/gateway-contracts/addresses + cat > /tmp/GatewayAddresses.sol << 'SOL' + // SPDX-License-Identifier: BSD-3-Clause-Clear + pragma solidity ^0.8.24; + address constant gatewayConfigAddress = 0x0000000000000000000000000000000000000001; + address constant decryptionAddress = 0x0000000000000000000000000000000000000002; + address constant ciphertextCommitsAddress = 0x0000000000000000000000000000000000000003; + address constant inputVerificationAddress = 0x0000000000000000000000000000000000000004; + address constant kmsGenerationAddress = 0x0000000000000000000000000000000000000005; + address constant protocolPaymentAddress = 0x0000000000000000000000000000000000000006; + address constant pauserSetAddress = 0x0000000000000000000000000000000000000007; + SOL + cp /tmp/GatewayAddresses.sol gateway-contracts/addresses/GatewayAddresses.sol + cp /tmp/GatewayAddresses.sol main-branch/gateway-contracts/addresses/GatewayAddresses.sol + + - name: Run upgrade hygiene check + run: | + chmod +x ci/check-upgrade-hygiene.sh + ./ci/check-upgrade-hygiene.sh main-branch/gateway-contracts gateway-contracts diff --git a/ci/check-upgrade-hygiene.sh b/ci/check-upgrade-hygiene.sh new file mode 100755 index 0000000000..198b6a8498 --- /dev/null +++ b/ci/check-upgrade-hygiene.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +# ci/check-upgrade-hygiene.sh +# +# Validates that upgradeable contracts have proper version bumps when bytecode changes. +# Compares deployed bytecodes between two copies of a contract package (e.g. main vs PR branch). +# +# Usage: +# ./ci/check-upgrade-hygiene.sh +# +# Example: +# ./ci/check-upgrade-hygiene.sh main-branch/host-contracts host-contracts +# +# Requires: forge (Foundry), jq +# Both directories must have: +# - foundry.toml with cbor_metadata=false and bytecode_hash='none' +# - upgrade-manifest.json listing contract names +# - contracts/.sol for each manifest entry +# - addresses/ stub (generated file) so contracts compile + +set -euo pipefail + +MAIN_DIR="$1" +PR_DIR="$2" + +if [ ! -f "$PR_DIR/upgrade-manifest.json" ]; then + echo "::error::upgrade-manifest.json not found in $PR_DIR" + exit 1 +fi + +ERRORS=0 + +extract_const() { + local file="$1" const="$2" + sed -n "s/.*${const}[[:space:]]*=[[:space:]]*\([0-9]*\).*/\1/p" "$file" +} + +for name in $(jq -r '.[]' "$PR_DIR/upgrade-manifest.json"); do + echo "::group::Checking $name" + + main_sol="$MAIN_DIR/contracts/${name}.sol" + pr_sol="$PR_DIR/contracts/${name}.sol" + + # Skip contracts not present on main (newly added) + if [ ! -f "$main_sol" ]; then + echo "Skipping $name (new contract, not on main)" + echo "::endgroup::" + continue + fi + + if [ ! -f "$pr_sol" ]; then + echo "::error::$name is in upgrade-manifest.json but contracts/${name}.sol not found in PR" + ERRORS=$((ERRORS + 1)) + echo "::endgroup::" + continue + fi + + # --- Extract version constants from both --- + main_reinit=$(extract_const "$main_sol" "REINITIALIZER_VERSION") + pr_reinit=$(extract_const "$pr_sol" "REINITIALIZER_VERSION") + main_major=$(extract_const "$main_sol" "MAJOR_VERSION") + pr_major=$(extract_const "$pr_sol" "MAJOR_VERSION") + main_minor=$(extract_const "$main_sol" "MINOR_VERSION") + pr_minor=$(extract_const "$pr_sol" "MINOR_VERSION") + main_patch=$(extract_const "$main_sol" "PATCH_VERSION") + pr_patch=$(extract_const "$pr_sol" "PATCH_VERSION") + + for var in main_reinit pr_reinit main_major pr_major main_minor pr_minor main_patch pr_patch; do + if [ -z "${!var}" ]; then + echo "::error::Failed to parse $var for $name" + ERRORS=$((ERRORS + 1)) + echo "::endgroup::" + continue 2 + fi + done + + # --- Compare bytecodes (paths relative to --root) --- + main_bytecode=$(forge inspect "contracts/${name}.sol:$name" --root "$MAIN_DIR" deployedBytecode) + pr_bytecode=$(forge inspect "contracts/${name}.sol:$name" --root "$PR_DIR" deployedBytecode) + + bytecode_changed=false + if [ "$main_bytecode" != "$pr_bytecode" ]; then + bytecode_changed=true + fi + + version_changed=false + if [ "$main_major" != "$pr_major" ] || [ "$main_minor" != "$pr_minor" ] || [ "$main_patch" != "$pr_patch" ]; then + version_changed=true + fi + + reinit_changed=false + if [ "$main_reinit" != "$pr_reinit" ]; then + reinit_changed=true + fi + + if [ "$bytecode_changed" = true ]; then + echo "$name: bytecode CHANGED" + + # Check 1: REINITIALIZER_VERSION must be bumped + if [ "$reinit_changed" = false ]; then + echo "::error::$name bytecode changed but REINITIALIZER_VERSION was not bumped (still $pr_reinit)" + ERRORS=$((ERRORS + 1)) + fi + + # Check 2: reinitializeVN function must exist (convention: N = REINITIALIZER_VERSION - 1) + if [ "$reinit_changed" = true ]; then + expected_n=$((pr_reinit - 1)) + expected_fn="reinitializeV${expected_n}" + # Look for function declaration (not just any mention) + if ! grep -qE "function[[:space:]]+${expected_fn}[[:space:]]*\(" "$pr_sol"; then + echo "::error::$name has REINITIALIZER_VERSION=$pr_reinit but no $expected_fn() function found" + ERRORS=$((ERRORS + 1)) + fi + fi + + # Check 3: Semantic version must be bumped + if [ "$version_changed" = false ]; then + echo "::error::$name bytecode changed but semantic version was not bumped (still v${pr_major}.${pr_minor}.${pr_patch})" + ERRORS=$((ERRORS + 1)) + fi + + else + echo "$name: bytecode unchanged" + + # Inverse check: reinitializer should NOT be bumped if bytecode didn't change + if [ "$reinit_changed" = true ]; then + echo "::error::$name REINITIALIZER_VERSION bumped ($main_reinit -> $pr_reinit) but bytecode is unchanged" + ERRORS=$((ERRORS + 1)) + fi + fi + + echo "::endgroup::" +done + +if [ "$ERRORS" -gt 0 ]; then + echo "::error::Upgrade hygiene check failed with $ERRORS error(s)" + exit 1 +fi + +echo "All contracts passed upgrade hygiene checks" diff --git a/gateway-contracts/foundry.toml b/gateway-contracts/foundry.toml index c77611f5d9..f8c901533d 100644 --- a/gateway-contracts/foundry.toml +++ b/gateway-contracts/foundry.toml @@ -2,3 +2,10 @@ src = 'contracts' libs = ['node_modules'] solc = '0.8.24' +cbor_metadata = false +bytecode_hash = 'none' + +remappings = [ + '@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/', + '@openzeppelin/contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable/', +] diff --git a/host-contracts/foundry.toml b/host-contracts/foundry.toml index 8e95a13d12..79268ce7f9 100644 --- a/host-contracts/foundry.toml +++ b/host-contracts/foundry.toml @@ -5,6 +5,8 @@ libs = ['node_modules', 'lib', "dependencies"] test = 'test' cache_path = 'cache_forge' solc = '0.8.24' +cbor_metadata = false +bytecode_hash = 'none' remappings = [ '@fhevm-foundry/=./fhevm-foundry/', From c9aff6f91706ab27e41beb2a454ce6a52af976c1 Mon Sep 17 00:00:00 2001 From: Elias Tazartes Date: Fri, 13 Mar 2026 12:28:35 +0100 Subject: [PATCH 02/13] fix: align foundry.toml between main and PR checkouts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copy the PR's foundry.toml (with cbor_metadata=false and bytecode_hash='none') to the main checkout before compiling. Without this, main compiles with its original settings, producing different metadata → false bytecode diffs for every contract. --- .github/workflows/contracts-upgrade-hygiene.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/contracts-upgrade-hygiene.yml b/.github/workflows/contracts-upgrade-hygiene.yml index 5480de509f..c0989f314f 100644 --- a/.github/workflows/contracts-upgrade-hygiene.yml +++ b/.github/workflows/contracts-upgrade-hygiene.yml @@ -71,6 +71,9 @@ jobs: npm ci forge soldeer install + - name: Align compilation settings + run: cp host-contracts/foundry.toml main-branch/host-contracts/foundry.toml + - name: Generate address stubs run: | # Host contracts need addresses/FHEVMHostAddresses.sol (generated at deploy time). @@ -125,6 +128,9 @@ jobs: working-directory: main-branch/gateway-contracts run: npm ci + - name: Align compilation settings + run: cp gateway-contracts/foundry.toml main-branch/gateway-contracts/foundry.toml + - name: Generate address stubs run: | # Gateway contracts need addresses/GatewayAddresses.sol (generated at deploy time). From cee37c6bd89b68d3a436da5afb196902a2e88996 Mon Sep 17 00:00:00 2001 From: Elias Tazartes Date: Fri, 13 Mar 2026 12:39:25 +0100 Subject: [PATCH 03/13] refactor: harden and optimize check-upgrade-hygiene script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace 8 per-contract sed calls with single-pass awk extraction (fixes latent bug where multiple matches could silently corrupt values, and [0-9]* matching zero digits) - Pre-compile both roots in parallel (forge build) so all forge inspect calls are cache hits — halves compilation wall time in CI - Handle forge inspect failures gracefully (report + continue) instead of aborting the entire script under set -e - Remove unnecessary chmod +x in workflow (file already has exec bit) --- .../workflows/contracts-upgrade-hygiene.yml | 8 +-- ci/check-upgrade-hygiene.sh | 49 +++++++++++++------ 2 files changed, 36 insertions(+), 21 deletions(-) diff --git a/.github/workflows/contracts-upgrade-hygiene.yml b/.github/workflows/contracts-upgrade-hygiene.yml index c0989f314f..510e15100d 100644 --- a/.github/workflows/contracts-upgrade-hygiene.yml +++ b/.github/workflows/contracts-upgrade-hygiene.yml @@ -93,9 +93,7 @@ jobs: cp /tmp/FHEVMHostAddresses.sol main-branch/host-contracts/addresses/FHEVMHostAddresses.sol - name: Run upgrade hygiene check - run: | - chmod +x ci/check-upgrade-hygiene.sh - ./ci/check-upgrade-hygiene.sh main-branch/host-contracts host-contracts + run: ./ci/check-upgrade-hygiene.sh main-branch/host-contracts host-contracts gateway-contracts: name: contracts-upgrade-hygiene/gateway-contracts @@ -150,6 +148,4 @@ jobs: cp /tmp/GatewayAddresses.sol main-branch/gateway-contracts/addresses/GatewayAddresses.sol - name: Run upgrade hygiene check - run: | - chmod +x ci/check-upgrade-hygiene.sh - ./ci/check-upgrade-hygiene.sh main-branch/gateway-contracts gateway-contracts + run: ./ci/check-upgrade-hygiene.sh main-branch/gateway-contracts gateway-contracts diff --git a/ci/check-upgrade-hygiene.sh b/ci/check-upgrade-hygiene.sh index 198b6a8498..9d30795180 100755 --- a/ci/check-upgrade-hygiene.sh +++ b/ci/check-upgrade-hygiene.sh @@ -29,11 +29,26 @@ fi ERRORS=0 -extract_const() { - local file="$1" const="$2" - sed -n "s/.*${const}[[:space:]]*=[[:space:]]*\([0-9]*\).*/\1/p" "$file" +# Extract all four version constants from a .sol file in a single pass. +# Returns: REINITIALIZER_VERSION MAJOR_VERSION MINOR_VERSION PATCH_VERSION +extract_versions() { + awk ' + /REINITIALIZER_VERSION[[:space:]]*=[[:space:]]*[0-9]/ { gsub(/[^0-9]/,"",$NF); reinit=$NF } + /MAJOR_VERSION[[:space:]]*=[[:space:]]*[0-9]/ { gsub(/[^0-9]/,"",$NF); major=$NF } + /MINOR_VERSION[[:space:]]*=[[:space:]]*[0-9]/ { gsub(/[^0-9]/,"",$NF); minor=$NF } + /PATCH_VERSION[[:space:]]*=[[:space:]]*[0-9]/ { gsub(/[^0-9]/,"",$NF); patch=$NF } + END { print reinit, major, minor, patch } + ' "$1" } +# Pre-compile both roots in parallel so all forge inspect calls are cache hits. +forge build --root "$MAIN_DIR" & +pid_main=$! +forge build --root "$PR_DIR" & +pid_pr=$! +wait "$pid_main" || { echo "::error::forge build failed for $MAIN_DIR"; exit 1; } +wait "$pid_pr" || { echo "::error::forge build failed for $PR_DIR"; exit 1; } + for name in $(jq -r '.[]' "$PR_DIR/upgrade-manifest.json"); do echo "::group::Checking $name" @@ -54,15 +69,9 @@ for name in $(jq -r '.[]' "$PR_DIR/upgrade-manifest.json"); do continue fi - # --- Extract version constants from both --- - main_reinit=$(extract_const "$main_sol" "REINITIALIZER_VERSION") - pr_reinit=$(extract_const "$pr_sol" "REINITIALIZER_VERSION") - main_major=$(extract_const "$main_sol" "MAJOR_VERSION") - pr_major=$(extract_const "$pr_sol" "MAJOR_VERSION") - main_minor=$(extract_const "$main_sol" "MINOR_VERSION") - pr_minor=$(extract_const "$pr_sol" "MINOR_VERSION") - main_patch=$(extract_const "$main_sol" "PATCH_VERSION") - pr_patch=$(extract_const "$pr_sol" "PATCH_VERSION") + # --- Extract version constants from both (single pass per file) --- + read -r main_reinit main_major main_minor main_patch < <(extract_versions "$main_sol") + read -r pr_reinit pr_major pr_minor pr_patch < <(extract_versions "$pr_sol") for var in main_reinit pr_reinit main_major pr_major main_minor pr_minor main_patch pr_patch; do if [ -z "${!var}" ]; then @@ -73,9 +82,19 @@ for name in $(jq -r '.[]' "$PR_DIR/upgrade-manifest.json"); do fi done - # --- Compare bytecodes (paths relative to --root) --- - main_bytecode=$(forge inspect "contracts/${name}.sol:$name" --root "$MAIN_DIR" deployedBytecode) - pr_bytecode=$(forge inspect "contracts/${name}.sol:$name" --root "$PR_DIR" deployedBytecode) + # --- Compare bytecodes (paths relative to --root, builds are cached) --- + if ! main_bytecode=$(forge inspect "contracts/${name}.sol:$name" --root "$MAIN_DIR" deployedBytecode 2>&1); then + echo "::error::Failed to inspect $name on main: $main_bytecode" + ERRORS=$((ERRORS + 1)) + echo "::endgroup::" + continue + fi + if ! pr_bytecode=$(forge inspect "contracts/${name}.sol:$name" --root "$PR_DIR" deployedBytecode 2>&1); then + echo "::error::Failed to inspect $name on PR: $pr_bytecode" + ERRORS=$((ERRORS + 1)) + echo "::endgroup::" + continue + fi bytecode_changed=false if [ "$main_bytecode" != "$pr_bytecode" ]; then From 76337219f037b29ac2d53d5d416e14bd0a10d96a Mon Sep 17 00:00:00 2001 From: Elias Tazartes Date: Fri, 13 Mar 2026 12:42:57 +0100 Subject: [PATCH 04/13] fix: extract first integer after = in awk version parsing The previous $NF approach grabbed the last whitespace-delimited field, which would silently pick up digits from trailing comments (e.g. `MAJOR_VERSION = 12; // was 11` extracted 11, not 12). Now uses val_after_eq() to extract the first integer after `=`. --- ci/check-upgrade-hygiene.sh | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ci/check-upgrade-hygiene.sh b/ci/check-upgrade-hygiene.sh index 9d30795180..55f90ca611 100755 --- a/ci/check-upgrade-hygiene.sh +++ b/ci/check-upgrade-hygiene.sh @@ -33,10 +33,11 @@ ERRORS=0 # Returns: REINITIALIZER_VERSION MAJOR_VERSION MINOR_VERSION PATCH_VERSION extract_versions() { awk ' - /REINITIALIZER_VERSION[[:space:]]*=[[:space:]]*[0-9]/ { gsub(/[^0-9]/,"",$NF); reinit=$NF } - /MAJOR_VERSION[[:space:]]*=[[:space:]]*[0-9]/ { gsub(/[^0-9]/,"",$NF); major=$NF } - /MINOR_VERSION[[:space:]]*=[[:space:]]*[0-9]/ { gsub(/[^0-9]/,"",$NF); minor=$NF } - /PATCH_VERSION[[:space:]]*=[[:space:]]*[0-9]/ { gsub(/[^0-9]/,"",$NF); patch=$NF } + function val_after_eq(line) { sub(/.*=[[:space:]]*/, "", line); sub(/[^0-9].*/, "", line); return line } + /REINITIALIZER_VERSION[[:space:]]*=[[:space:]]*[0-9]/ { reinit = val_after_eq($0) } + /MAJOR_VERSION[[:space:]]*=[[:space:]]*[0-9]/ { major = val_after_eq($0) } + /MINOR_VERSION[[:space:]]*=[[:space:]]*[0-9]/ { minor = val_after_eq($0) } + /PATCH_VERSION[[:space:]]*=[[:space:]]*[0-9]/ { patch = val_after_eq($0) } END { print reinit, major, minor, patch } ' "$1" } From 2133dfe2ce60057c4d5914de3ce7610a3a9ce7dc Mon Sep 17 00:00:00 2001 From: Elias Tazartes Date: Fri, 13 Mar 2026 12:47:22 +0100 Subject: [PATCH 05/13] fix: prevent stderr warnings from contaminating bytecode comparison forge inspect can emit compiler warnings to stderr even on success. With 2>&1, those warnings were mixed into the bytecode string, causing false positives when warnings differed between main and PR. Now stderr is discarded on success (2>/dev/null) and only captured on failure for the error message. --- ci/check-upgrade-hygiene.sh | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ci/check-upgrade-hygiene.sh b/ci/check-upgrade-hygiene.sh index 55f90ca611..a6cc71cc9d 100755 --- a/ci/check-upgrade-hygiene.sh +++ b/ci/check-upgrade-hygiene.sh @@ -84,14 +84,16 @@ for name in $(jq -r '.[]' "$PR_DIR/upgrade-manifest.json"); do done # --- Compare bytecodes (paths relative to --root, builds are cached) --- - if ! main_bytecode=$(forge inspect "contracts/${name}.sol:$name" --root "$MAIN_DIR" deployedBytecode 2>&1); then - echo "::error::Failed to inspect $name on main: $main_bytecode" + # Capture only stdout for bytecode; stderr goes to /dev/null on success (warnings), + # but on failure we re-run to capture the error message. + if ! main_bytecode=$(forge inspect "contracts/${name}.sol:$name" --root "$MAIN_DIR" deployedBytecode 2>/dev/null); then + echo "::error::Failed to inspect $name on main:$(forge inspect "contracts/${name}.sol:$name" --root "$MAIN_DIR" deployedBytecode 2>&1 || true)" ERRORS=$((ERRORS + 1)) echo "::endgroup::" continue fi - if ! pr_bytecode=$(forge inspect "contracts/${name}.sol:$name" --root "$PR_DIR" deployedBytecode 2>&1); then - echo "::error::Failed to inspect $name on PR: $pr_bytecode" + if ! pr_bytecode=$(forge inspect "contracts/${name}.sol:$name" --root "$PR_DIR" deployedBytecode 2>/dev/null); then + echo "::error::Failed to inspect $name on PR:$(forge inspect "contracts/${name}.sol:$name" --root "$PR_DIR" deployedBytecode 2>&1 || true)" ERRORS=$((ERRORS + 1)) echo "::endgroup::" continue From feb16674972c0408f4f0f0e0e812bcbd8beac593 Mon Sep 17 00:00:00 2001 From: Elias Tazartes Date: Fri, 13 Mar 2026 12:58:02 +0100 Subject: [PATCH 06/13] refactor: deduplicate workflow with matrix, extract stubs to files - Replace two near-identical jobs with a single matrix job (host-contracts, gateway-contracts), cutting workflow from 151 to 92 lines - Move inline Solidity address heredocs to committed ci/stubs/ files - Add missing PaymentBridgingAddresses.sol stub (was breaking gateway CI) - Drop forge build pre-step: forge inspect already compiles and caches on first call, and forge build required ALL stubs including non-manifest contracts which caused failures - Trim script from 161 to 110 lines: shorter header, flattened control flow --- .../workflows/contracts-upgrade-hygiene.yml | 115 +++++------------- ci/check-upgrade-hygiene.sh | 105 ++++------------ .../gateway-contracts/GatewayAddresses.sol | 9 ++ .../PaymentBridgingAddresses.sol | 4 + .../host-contracts/FHEVMHostAddresses.sol | 8 ++ 5 files changed, 76 insertions(+), 165 deletions(-) create mode 100644 ci/stubs/gateway-contracts/GatewayAddresses.sol create mode 100644 ci/stubs/gateway-contracts/PaymentBridgingAddresses.sol create mode 100644 ci/stubs/host-contracts/FHEVMHostAddresses.sol diff --git a/.github/workflows/contracts-upgrade-hygiene.yml b/.github/workflows/contracts-upgrade-hygiene.yml index 510e15100d..9eb3682b4b 100644 --- a/.github/workflows/contracts-upgrade-hygiene.yml +++ b/.github/workflows/contracts-upgrade-hygiene.yml @@ -17,8 +17,7 @@ jobs: pull-requests: 'read' runs-on: ubuntu-latest outputs: - host-contracts: ${{ steps.filter.outputs.host-contracts }} - gateway-contracts: ${{ steps.filter.outputs.gateway-contracts }} + packages: ${{ steps.filter.outputs.changes }} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: @@ -30,19 +29,30 @@ jobs: host-contracts: - .github/workflows/contracts-upgrade-hygiene.yml - ci/check-upgrade-hygiene.sh + - ci/stubs/host-contracts/** - host-contracts/** gateway-contracts: - .github/workflows/contracts-upgrade-hygiene.yml - ci/check-upgrade-hygiene.sh + - ci/stubs/gateway-contracts/** - gateway-contracts/** - host-contracts: - name: contracts-upgrade-hygiene/host-contracts + check: + name: contracts-upgrade-hygiene/${{ matrix.package }} needs: check-changes - if: ${{ needs.check-changes.outputs.host-contracts == 'true' }} + if: ${{ needs.check-changes.outputs.packages != '[]' }} permissions: contents: 'read' runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + package: ${{ fromJSON(needs.check-changes.outputs.packages) }} + include: + - package: host-contracts + extra-deps: forge soldeer install + - package: gateway-contracts + extra-deps: '' steps: - name: Checkout PR branch uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -59,93 +69,24 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@82dee4ba654bd2146511f85f0d013af94670c4de # v1.4.0 - - name: Install PR dependencies - working-directory: host-contracts + - name: Install dependencies run: | - npm ci - forge soldeer install - - - name: Install main dependencies - working-directory: main-branch/host-contracts - run: | - npm ci - forge soldeer install - - - name: Align compilation settings - run: cp host-contracts/foundry.toml main-branch/host-contracts/foundry.toml - - - name: Generate address stubs - run: | - # Host contracts need addresses/FHEVMHostAddresses.sol (generated at deploy time). - # Create identical stubs in both copies so bytecode comparison is not affected. - mkdir -p host-contracts/addresses main-branch/host-contracts/addresses - cat > /tmp/FHEVMHostAddresses.sol << 'SOL' - // SPDX-License-Identifier: BSD-3-Clause-Clear - pragma solidity ^0.8.24; - address constant aclAdd = 0x0000000000000000000000000000000000000001; - address constant fhevmExecutorAdd = 0x0000000000000000000000000000000000000002; - address constant kmsVerifierAdd = 0x0000000000000000000000000000000000000003; - address constant inputVerifierAdd = 0x0000000000000000000000000000000000000004; - address constant hcuLimitAdd = 0x0000000000000000000000000000000000000005; - address constant pauserSetAdd = 0x0000000000000000000000000000000000000006; - SOL - cp /tmp/FHEVMHostAddresses.sol host-contracts/addresses/FHEVMHostAddresses.sol - cp /tmp/FHEVMHostAddresses.sol main-branch/host-contracts/addresses/FHEVMHostAddresses.sol - - - name: Run upgrade hygiene check - run: ./ci/check-upgrade-hygiene.sh main-branch/host-contracts host-contracts - - gateway-contracts: - name: contracts-upgrade-hygiene/gateway-contracts - needs: check-changes - if: ${{ needs.check-changes.outputs.gateway-contracts == 'true' }} - permissions: - contents: 'read' - runs-on: ubuntu-latest - steps: - - name: Checkout PR branch - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - persist-credentials: 'false' - - - name: Checkout main branch - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - ref: main - path: main-branch - persist-credentials: 'false' - - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@82dee4ba654bd2146511f85f0d013af94670c4de # v1.4.0 - - - name: Install PR dependencies - working-directory: gateway-contracts - run: npm ci - - - name: Install main dependencies - working-directory: main-branch/gateway-contracts - run: npm ci + npm --prefix ${{ matrix.package }} ci + npm --prefix main-branch/${{ matrix.package }} ci + if [ -n "${{ matrix.extra-deps }}" ]; then + (cd ${{ matrix.package }} && ${{ matrix.extra-deps }}) + (cd main-branch/${{ matrix.package }} && ${{ matrix.extra-deps }}) + fi - name: Align compilation settings - run: cp gateway-contracts/foundry.toml main-branch/gateway-contracts/foundry.toml + run: cp ${{ matrix.package }}/foundry.toml main-branch/${{ matrix.package }}/foundry.toml - name: Generate address stubs run: | - # Gateway contracts need addresses/GatewayAddresses.sol (generated at deploy time). - mkdir -p gateway-contracts/addresses main-branch/gateway-contracts/addresses - cat > /tmp/GatewayAddresses.sol << 'SOL' - // SPDX-License-Identifier: BSD-3-Clause-Clear - pragma solidity ^0.8.24; - address constant gatewayConfigAddress = 0x0000000000000000000000000000000000000001; - address constant decryptionAddress = 0x0000000000000000000000000000000000000002; - address constant ciphertextCommitsAddress = 0x0000000000000000000000000000000000000003; - address constant inputVerificationAddress = 0x0000000000000000000000000000000000000004; - address constant kmsGenerationAddress = 0x0000000000000000000000000000000000000005; - address constant protocolPaymentAddress = 0x0000000000000000000000000000000000000006; - address constant pauserSetAddress = 0x0000000000000000000000000000000000000007; - SOL - cp /tmp/GatewayAddresses.sol gateway-contracts/addresses/GatewayAddresses.sol - cp /tmp/GatewayAddresses.sol main-branch/gateway-contracts/addresses/GatewayAddresses.sol + for dir in ${{ matrix.package }} main-branch/${{ matrix.package }}; do + mkdir -p "$dir/addresses" + cp ci/stubs/${{ matrix.package }}/*.sol "$dir/addresses/" + done - name: Run upgrade hygiene check - run: ./ci/check-upgrade-hygiene.sh main-branch/gateway-contracts gateway-contracts + run: ./ci/check-upgrade-hygiene.sh main-branch/${{ matrix.package }} ${{ matrix.package }} diff --git a/ci/check-upgrade-hygiene.sh b/ci/check-upgrade-hygiene.sh index a6cc71cc9d..68198cddb3 100755 --- a/ci/check-upgrade-hygiene.sh +++ b/ci/check-upgrade-hygiene.sh @@ -1,22 +1,6 @@ #!/usr/bin/env bash -# ci/check-upgrade-hygiene.sh -# -# Validates that upgradeable contracts have proper version bumps when bytecode changes. -# Compares deployed bytecodes between two copies of a contract package (e.g. main vs PR branch). -# -# Usage: -# ./ci/check-upgrade-hygiene.sh -# -# Example: -# ./ci/check-upgrade-hygiene.sh main-branch/host-contracts host-contracts -# -# Requires: forge (Foundry), jq -# Both directories must have: -# - foundry.toml with cbor_metadata=false and bytecode_hash='none' -# - upgrade-manifest.json listing contract names -# - contracts/.sol for each manifest entry -# - addresses/ stub (generated file) so contracts compile - +# Checks that upgradeable contracts have proper version bumps when bytecode changes. +# Usage: ./ci/check-upgrade-hygiene.sh set -euo pipefail MAIN_DIR="$1" @@ -29,8 +13,7 @@ fi ERRORS=0 -# Extract all four version constants from a .sol file in a single pass. -# Returns: REINITIALIZER_VERSION MAJOR_VERSION MINOR_VERSION PATCH_VERSION +# Extract REINITIALIZER_VERSION, MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION in one pass. extract_versions() { awk ' function val_after_eq(line) { sub(/.*=[[:space:]]*/, "", line); sub(/[^0-9].*/, "", line); return line } @@ -42,21 +25,12 @@ extract_versions() { ' "$1" } -# Pre-compile both roots in parallel so all forge inspect calls are cache hits. -forge build --root "$MAIN_DIR" & -pid_main=$! -forge build --root "$PR_DIR" & -pid_pr=$! -wait "$pid_main" || { echo "::error::forge build failed for $MAIN_DIR"; exit 1; } -wait "$pid_pr" || { echo "::error::forge build failed for $PR_DIR"; exit 1; } - for name in $(jq -r '.[]' "$PR_DIR/upgrade-manifest.json"); do echo "::group::Checking $name" main_sol="$MAIN_DIR/contracts/${name}.sol" pr_sol="$PR_DIR/contracts/${name}.sol" - # Skip contracts not present on main (newly added) if [ ! -f "$main_sol" ]; then echo "Skipping $name (new contract, not on main)" echo "::endgroup::" @@ -64,13 +38,12 @@ for name in $(jq -r '.[]' "$PR_DIR/upgrade-manifest.json"); do fi if [ ! -f "$pr_sol" ]; then - echo "::error::$name is in upgrade-manifest.json but contracts/${name}.sol not found in PR" + echo "::error::$name listed in upgrade-manifest.json but missing in PR" ERRORS=$((ERRORS + 1)) echo "::endgroup::" continue fi - # --- Extract version constants from both (single pass per file) --- read -r main_reinit main_major main_minor main_patch < <(extract_versions "$main_sol") read -r pr_reinit pr_major pr_minor pr_patch < <(extract_versions "$pr_sol") @@ -83,73 +56,49 @@ for name in $(jq -r '.[]' "$PR_DIR/upgrade-manifest.json"); do fi done - # --- Compare bytecodes (paths relative to --root, builds are cached) --- - # Capture only stdout for bytecode; stderr goes to /dev/null on success (warnings), - # but on failure we re-run to capture the error message. + # forge inspect compiles on first call and caches; stderr suppressed to avoid warning noise. if ! main_bytecode=$(forge inspect "contracts/${name}.sol:$name" --root "$MAIN_DIR" deployedBytecode 2>/dev/null); then - echo "::error::Failed to inspect $name on main:$(forge inspect "contracts/${name}.sol:$name" --root "$MAIN_DIR" deployedBytecode 2>&1 || true)" + echo "::error::Failed to compile $name on main" ERRORS=$((ERRORS + 1)) echo "::endgroup::" continue fi if ! pr_bytecode=$(forge inspect "contracts/${name}.sol:$name" --root "$PR_DIR" deployedBytecode 2>/dev/null); then - echo "::error::Failed to inspect $name on PR:$(forge inspect "contracts/${name}.sol:$name" --root "$PR_DIR" deployedBytecode 2>&1 || true)" + echo "::error::Failed to compile $name on PR" ERRORS=$((ERRORS + 1)) echo "::endgroup::" continue fi - bytecode_changed=false - if [ "$main_bytecode" != "$pr_bytecode" ]; then - bytecode_changed=true - fi - - version_changed=false - if [ "$main_major" != "$pr_major" ] || [ "$main_minor" != "$pr_minor" ] || [ "$main_patch" != "$pr_patch" ]; then - version_changed=true - fi - - reinit_changed=false - if [ "$main_reinit" != "$pr_reinit" ]; then - reinit_changed=true - fi - - if [ "$bytecode_changed" = true ]; then - echo "$name: bytecode CHANGED" - - # Check 1: REINITIALIZER_VERSION must be bumped - if [ "$reinit_changed" = false ]; then - echo "::error::$name bytecode changed but REINITIALIZER_VERSION was not bumped (still $pr_reinit)" + if [ "$main_bytecode" = "$pr_bytecode" ]; then + echo "$name: bytecode unchanged" + if [ "$main_reinit" != "$pr_reinit" ]; then + echo "::error::$name REINITIALIZER_VERSION bumped ($main_reinit -> $pr_reinit) but bytecode is unchanged" ERRORS=$((ERRORS + 1)) fi + echo "::endgroup::" + continue + fi - # Check 2: reinitializeVN function must exist (convention: N = REINITIALIZER_VERSION - 1) - if [ "$reinit_changed" = true ]; then - expected_n=$((pr_reinit - 1)) - expected_fn="reinitializeV${expected_n}" - # Look for function declaration (not just any mention) - if ! grep -qE "function[[:space:]]+${expected_fn}[[:space:]]*\(" "$pr_sol"; then - echo "::error::$name has REINITIALIZER_VERSION=$pr_reinit but no $expected_fn() function found" - ERRORS=$((ERRORS + 1)) - fi - fi - - # Check 3: Semantic version must be bumped - if [ "$version_changed" = false ]; then - echo "::error::$name bytecode changed but semantic version was not bumped (still v${pr_major}.${pr_minor}.${pr_patch})" - ERRORS=$((ERRORS + 1)) - fi + echo "$name: bytecode CHANGED" + if [ "$main_reinit" = "$pr_reinit" ]; then + echo "::error::$name bytecode changed but REINITIALIZER_VERSION was not bumped (still $pr_reinit)" + ERRORS=$((ERRORS + 1)) else - echo "$name: bytecode unchanged" - - # Inverse check: reinitializer should NOT be bumped if bytecode didn't change - if [ "$reinit_changed" = true ]; then - echo "::error::$name REINITIALIZER_VERSION bumped ($main_reinit -> $pr_reinit) but bytecode is unchanged" + # Convention: reinitializeV{N-1} for REINITIALIZER_VERSION=N + expected_fn="reinitializeV$((pr_reinit - 1))" + if ! grep -qE "function[[:space:]]+${expected_fn}[[:space:]]*\(" "$pr_sol"; then + echo "::error::$name has REINITIALIZER_VERSION=$pr_reinit but no $expected_fn() function found" ERRORS=$((ERRORS + 1)) fi fi + if [ "$main_major" = "$pr_major" ] && [ "$main_minor" = "$pr_minor" ] && [ "$main_patch" = "$pr_patch" ]; then + echo "::error::$name bytecode changed but semantic version was not bumped (still v${pr_major}.${pr_minor}.${pr_patch})" + ERRORS=$((ERRORS + 1)) + fi + echo "::endgroup::" done diff --git a/ci/stubs/gateway-contracts/GatewayAddresses.sol b/ci/stubs/gateway-contracts/GatewayAddresses.sol new file mode 100644 index 0000000000..fb8fdd05e4 --- /dev/null +++ b/ci/stubs/gateway-contracts/GatewayAddresses.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: BSD-3-Clause-Clear +pragma solidity ^0.8.24; +address constant gatewayConfigAddress = 0x0000000000000000000000000000000000000001; +address constant decryptionAddress = 0x0000000000000000000000000000000000000002; +address constant ciphertextCommitsAddress = 0x0000000000000000000000000000000000000003; +address constant inputVerificationAddress = 0x0000000000000000000000000000000000000004; +address constant kmsGenerationAddress = 0x0000000000000000000000000000000000000005; +address constant protocolPaymentAddress = 0x0000000000000000000000000000000000000006; +address constant pauserSetAddress = 0x0000000000000000000000000000000000000007; diff --git a/ci/stubs/gateway-contracts/PaymentBridgingAddresses.sol b/ci/stubs/gateway-contracts/PaymentBridgingAddresses.sol new file mode 100644 index 0000000000..3df423f918 --- /dev/null +++ b/ci/stubs/gateway-contracts/PaymentBridgingAddresses.sol @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: BSD-3-Clause-Clear +pragma solidity ^0.8.24; +address constant feesSenderToBurnerAddress = 0x0000000000000000000000000000000000000008; +address constant zamaOFTAddress = 0x0000000000000000000000000000000000000009; diff --git a/ci/stubs/host-contracts/FHEVMHostAddresses.sol b/ci/stubs/host-contracts/FHEVMHostAddresses.sol new file mode 100644 index 0000000000..8de94ae307 --- /dev/null +++ b/ci/stubs/host-contracts/FHEVMHostAddresses.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: BSD-3-Clause-Clear +pragma solidity ^0.8.24; +address constant aclAdd = 0x0000000000000000000000000000000000000001; +address constant fhevmExecutorAdd = 0x0000000000000000000000000000000000000002; +address constant kmsVerifierAdd = 0x0000000000000000000000000000000000000003; +address constant inputVerifierAdd = 0x0000000000000000000000000000000000000004; +address constant hcuLimitAdd = 0x0000000000000000000000000000000000000005; +address constant pauserSetAdd = 0x0000000000000000000000000000000000000006; From 7c838fb0257d78916aa375661ba5c5fd8d1a9dd9 Mon Sep 17 00:00:00 2001 From: Elias Tazartes Date: Fri, 13 Mar 2026 13:03:06 +0100 Subject: [PATCH 07/13] fix: use working-directory for npm ci, skip empty extra-deps npm --prefix doesn't work with npm ci when package-lock.json is in the target directory. Use working-directory instead. Also guard extra-deps step with matrix.if to avoid empty subshell syntax error. --- .../workflows/contracts-upgrade-hygiene.yml | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/.github/workflows/contracts-upgrade-hygiene.yml b/.github/workflows/contracts-upgrade-hygiene.yml index 9eb3682b4b..8eeb541cdc 100644 --- a/.github/workflows/contracts-upgrade-hygiene.yml +++ b/.github/workflows/contracts-upgrade-hygiene.yml @@ -69,14 +69,19 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@82dee4ba654bd2146511f85f0d013af94670c4de # v1.4.0 - - name: Install dependencies + - name: Install PR dependencies + working-directory: ${{ matrix.package }} + run: npm ci + + - name: Install main dependencies + working-directory: main-branch/${{ matrix.package }} + run: npm ci + + - name: Install Forge dependencies + if: matrix.extra-deps != '' run: | - npm --prefix ${{ matrix.package }} ci - npm --prefix main-branch/${{ matrix.package }} ci - if [ -n "${{ matrix.extra-deps }}" ]; then - (cd ${{ matrix.package }} && ${{ matrix.extra-deps }}) - (cd main-branch/${{ matrix.package }} && ${{ matrix.extra-deps }}) - fi + (cd ${{ matrix.package }} && ${{ matrix.extra-deps }}) + (cd main-branch/${{ matrix.package }} && ${{ matrix.extra-deps }}) - name: Align compilation settings run: cp ${{ matrix.package }}/foundry.toml main-branch/${{ matrix.package }}/foundry.toml From 99a4931db66914630cd718cc36c4d476b50ff4eb Mon Sep 17 00:00:00 2001 From: Elias Tazartes Date: Fri, 13 Mar 2026 13:03:45 +0100 Subject: [PATCH 08/13] fix: add explanatory comments on permissions (zizmor) --- .github/workflows/contracts-upgrade-hygiene.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/contracts-upgrade-hygiene.yml b/.github/workflows/contracts-upgrade-hygiene.yml index 8eeb541cdc..8e9927d547 100644 --- a/.github/workflows/contracts-upgrade-hygiene.yml +++ b/.github/workflows/contracts-upgrade-hygiene.yml @@ -13,8 +13,8 @@ jobs: check-changes: name: contracts-upgrade-hygiene/check-changes permissions: - contents: 'read' - pull-requests: 'read' + contents: 'read' # Required to checkout repository code + pull-requests: 'read' # Required to read pull request for paths-filter runs-on: ubuntu-latest outputs: packages: ${{ steps.filter.outputs.changes }} @@ -42,7 +42,7 @@ jobs: needs: check-changes if: ${{ needs.check-changes.outputs.packages != '[]' }} permissions: - contents: 'read' + contents: 'read' # Required to checkout repository code runs-on: ubuntu-latest strategy: fail-fast: false From 93c8523d2c6b41147cf85bc157abbd305f9752fb Mon Sep 17 00:00:00 2001 From: Elias Tazartes Date: Fri, 13 Mar 2026 13:11:12 +0100 Subject: [PATCH 09/13] fix: use env vars instead of template expansion in run blocks (zizmor) zizmor flags ${{ matrix.* }} in run: blocks as code injection risk. Pass matrix values through env vars instead, which is the recommended safe pattern for GitHub Actions. --- .../workflows/contracts-upgrade-hygiene.yml | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/.github/workflows/contracts-upgrade-hygiene.yml b/.github/workflows/contracts-upgrade-hygiene.yml index 8e9927d547..9cf5bb8ced 100644 --- a/.github/workflows/contracts-upgrade-hygiene.yml +++ b/.github/workflows/contracts-upgrade-hygiene.yml @@ -79,19 +79,24 @@ jobs: - name: Install Forge dependencies if: matrix.extra-deps != '' + env: + PACKAGE: ${{ matrix.package }} + EXTRA_DEPS: ${{ matrix.extra-deps }} run: | - (cd ${{ matrix.package }} && ${{ matrix.extra-deps }}) - (cd main-branch/${{ matrix.package }} && ${{ matrix.extra-deps }}) + (cd "$PACKAGE" && $EXTRA_DEPS) + (cd "main-branch/$PACKAGE" && $EXTRA_DEPS) - - name: Align compilation settings - run: cp ${{ matrix.package }}/foundry.toml main-branch/${{ matrix.package }}/foundry.toml - - - name: Generate address stubs + - name: Setup compilation + env: + PACKAGE: ${{ matrix.package }} run: | - for dir in ${{ matrix.package }} main-branch/${{ matrix.package }}; do + cp "$PACKAGE/foundry.toml" "main-branch/$PACKAGE/foundry.toml" + for dir in "$PACKAGE" "main-branch/$PACKAGE"; do mkdir -p "$dir/addresses" - cp ci/stubs/${{ matrix.package }}/*.sol "$dir/addresses/" + cp ci/stubs/"$PACKAGE"/*.sol "$dir/addresses/" done - name: Run upgrade hygiene check - run: ./ci/check-upgrade-hygiene.sh main-branch/${{ matrix.package }} ${{ matrix.package }} + env: + PACKAGE: ${{ matrix.package }} + run: ./ci/check-upgrade-hygiene.sh "main-branch/$PACKAGE" "$PACKAGE" From 351471851e8aa090882170753f57d70e5b588174 Mon Sep 17 00:00:00 2001 From: Elias Tazartes Date: Fri, 13 Mar 2026 13:14:01 +0100 Subject: [PATCH 10/13] refactor: rewrite check-upgrade-hygiene in TypeScript (Bun) Replace the bash script with a TypeScript version run via Bun. Much more readable: regex instead of awk, typed data structures, standard control flow. Adds setup-bun step to the workflow. --- .../workflows/contracts-upgrade-hygiene.yml | 9 +- ci/check-upgrade-hygiene.sh | 110 ------------- ci/check-upgrade-hygiene.ts | 148 ++++++++++++++++++ 3 files changed, 154 insertions(+), 113 deletions(-) delete mode 100755 ci/check-upgrade-hygiene.sh create mode 100644 ci/check-upgrade-hygiene.ts diff --git a/.github/workflows/contracts-upgrade-hygiene.yml b/.github/workflows/contracts-upgrade-hygiene.yml index 9cf5bb8ced..45eecd136d 100644 --- a/.github/workflows/contracts-upgrade-hygiene.yml +++ b/.github/workflows/contracts-upgrade-hygiene.yml @@ -28,12 +28,12 @@ jobs: filters: | host-contracts: - .github/workflows/contracts-upgrade-hygiene.yml - - ci/check-upgrade-hygiene.sh + - ci/check-upgrade-hygiene.ts - ci/stubs/host-contracts/** - host-contracts/** gateway-contracts: - .github/workflows/contracts-upgrade-hygiene.yml - - ci/check-upgrade-hygiene.sh + - ci/check-upgrade-hygiene.ts - ci/stubs/gateway-contracts/** - gateway-contracts/** @@ -66,6 +66,9 @@ jobs: path: main-branch persist-credentials: 'false' + - name: Install Bun + uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2 + - name: Install Foundry uses: foundry-rs/foundry-toolchain@82dee4ba654bd2146511f85f0d013af94670c4de # v1.4.0 @@ -99,4 +102,4 @@ jobs: - name: Run upgrade hygiene check env: PACKAGE: ${{ matrix.package }} - run: ./ci/check-upgrade-hygiene.sh "main-branch/$PACKAGE" "$PACKAGE" + run: bun ci/check-upgrade-hygiene.ts "main-branch/$PACKAGE" "$PACKAGE" diff --git a/ci/check-upgrade-hygiene.sh b/ci/check-upgrade-hygiene.sh deleted file mode 100755 index 68198cddb3..0000000000 --- a/ci/check-upgrade-hygiene.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash -# Checks that upgradeable contracts have proper version bumps when bytecode changes. -# Usage: ./ci/check-upgrade-hygiene.sh -set -euo pipefail - -MAIN_DIR="$1" -PR_DIR="$2" - -if [ ! -f "$PR_DIR/upgrade-manifest.json" ]; then - echo "::error::upgrade-manifest.json not found in $PR_DIR" - exit 1 -fi - -ERRORS=0 - -# Extract REINITIALIZER_VERSION, MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION in one pass. -extract_versions() { - awk ' - function val_after_eq(line) { sub(/.*=[[:space:]]*/, "", line); sub(/[^0-9].*/, "", line); return line } - /REINITIALIZER_VERSION[[:space:]]*=[[:space:]]*[0-9]/ { reinit = val_after_eq($0) } - /MAJOR_VERSION[[:space:]]*=[[:space:]]*[0-9]/ { major = val_after_eq($0) } - /MINOR_VERSION[[:space:]]*=[[:space:]]*[0-9]/ { minor = val_after_eq($0) } - /PATCH_VERSION[[:space:]]*=[[:space:]]*[0-9]/ { patch = val_after_eq($0) } - END { print reinit, major, minor, patch } - ' "$1" -} - -for name in $(jq -r '.[]' "$PR_DIR/upgrade-manifest.json"); do - echo "::group::Checking $name" - - main_sol="$MAIN_DIR/contracts/${name}.sol" - pr_sol="$PR_DIR/contracts/${name}.sol" - - if [ ! -f "$main_sol" ]; then - echo "Skipping $name (new contract, not on main)" - echo "::endgroup::" - continue - fi - - if [ ! -f "$pr_sol" ]; then - echo "::error::$name listed in upgrade-manifest.json but missing in PR" - ERRORS=$((ERRORS + 1)) - echo "::endgroup::" - continue - fi - - read -r main_reinit main_major main_minor main_patch < <(extract_versions "$main_sol") - read -r pr_reinit pr_major pr_minor pr_patch < <(extract_versions "$pr_sol") - - for var in main_reinit pr_reinit main_major pr_major main_minor pr_minor main_patch pr_patch; do - if [ -z "${!var}" ]; then - echo "::error::Failed to parse $var for $name" - ERRORS=$((ERRORS + 1)) - echo "::endgroup::" - continue 2 - fi - done - - # forge inspect compiles on first call and caches; stderr suppressed to avoid warning noise. - if ! main_bytecode=$(forge inspect "contracts/${name}.sol:$name" --root "$MAIN_DIR" deployedBytecode 2>/dev/null); then - echo "::error::Failed to compile $name on main" - ERRORS=$((ERRORS + 1)) - echo "::endgroup::" - continue - fi - if ! pr_bytecode=$(forge inspect "contracts/${name}.sol:$name" --root "$PR_DIR" deployedBytecode 2>/dev/null); then - echo "::error::Failed to compile $name on PR" - ERRORS=$((ERRORS + 1)) - echo "::endgroup::" - continue - fi - - if [ "$main_bytecode" = "$pr_bytecode" ]; then - echo "$name: bytecode unchanged" - if [ "$main_reinit" != "$pr_reinit" ]; then - echo "::error::$name REINITIALIZER_VERSION bumped ($main_reinit -> $pr_reinit) but bytecode is unchanged" - ERRORS=$((ERRORS + 1)) - fi - echo "::endgroup::" - continue - fi - - echo "$name: bytecode CHANGED" - - if [ "$main_reinit" = "$pr_reinit" ]; then - echo "::error::$name bytecode changed but REINITIALIZER_VERSION was not bumped (still $pr_reinit)" - ERRORS=$((ERRORS + 1)) - else - # Convention: reinitializeV{N-1} for REINITIALIZER_VERSION=N - expected_fn="reinitializeV$((pr_reinit - 1))" - if ! grep -qE "function[[:space:]]+${expected_fn}[[:space:]]*\(" "$pr_sol"; then - echo "::error::$name has REINITIALIZER_VERSION=$pr_reinit but no $expected_fn() function found" - ERRORS=$((ERRORS + 1)) - fi - fi - - if [ "$main_major" = "$pr_major" ] && [ "$main_minor" = "$pr_minor" ] && [ "$main_patch" = "$pr_patch" ]; then - echo "::error::$name bytecode changed but semantic version was not bumped (still v${pr_major}.${pr_minor}.${pr_patch})" - ERRORS=$((ERRORS + 1)) - fi - - echo "::endgroup::" -done - -if [ "$ERRORS" -gt 0 ]; then - echo "::error::Upgrade hygiene check failed with $ERRORS error(s)" - exit 1 -fi - -echo "All contracts passed upgrade hygiene checks" diff --git a/ci/check-upgrade-hygiene.ts b/ci/check-upgrade-hygiene.ts new file mode 100644 index 0000000000..638f987dcc --- /dev/null +++ b/ci/check-upgrade-hygiene.ts @@ -0,0 +1,148 @@ +#!/usr/bin/env bun +// Checks that upgradeable contracts have proper version bumps when bytecode changes. +// Usage: bun ci/check-upgrade-hygiene.ts + +import { readFileSync, existsSync } from "fs"; +import { execSync } from "child_process"; +import { join } from "path"; + +const [mainDir, prDir] = process.argv.slice(2); +if (!mainDir || !prDir) { + console.error("Usage: bun ci/check-upgrade-hygiene.ts "); + process.exit(1); +} + +const manifestPath = join(prDir, "upgrade-manifest.json"); +if (!existsSync(manifestPath)) { + console.error(`::error::upgrade-manifest.json not found in ${prDir}`); + process.exit(1); +} + +const VERSION_RE = /(?REINITIALIZER_VERSION|MAJOR_VERSION|MINOR_VERSION|PATCH_VERSION)\s*=\s*(?\d+)/g; + +function extractVersions(filePath: string) { + const src = readFileSync(filePath, "utf-8"); + const versions: Record = {}; + for (const { groups } of src.matchAll(VERSION_RE)) { + versions[groups!.name] = Number(groups!.value); + } + return versions; +} + +function forgeInspect(contract: string, root: string): string | null { + try { + return execSync(`forge inspect "contracts/${contract}.sol:${contract}" --root "${root}" deployedBytecode`, { + encoding: "utf-8", + stdio: ["pipe", "pipe", "ignore"], + }).trim(); + } catch { + return null; + } +} + +const contracts: string[] = JSON.parse(readFileSync(manifestPath, "utf-8")); +let errors = 0; + +for (const name of contracts) { + console.log(`::group::Checking ${name}`); + + const mainSol = join(mainDir, "contracts", `${name}.sol`); + const prSol = join(prDir, "contracts", `${name}.sol`); + + if (!existsSync(mainSol)) { + console.log(`Skipping ${name} (new contract, not on main)`); + console.log("::endgroup::"); + continue; + } + + if (!existsSync(prSol)) { + console.error(`::error::${name} listed in upgrade-manifest.json but missing in PR`); + errors++; + console.log("::endgroup::"); + continue; + } + + const mainV = extractVersions(mainSol); + const prV = extractVersions(prSol); + + for (const key of ["REINITIALIZER_VERSION", "MAJOR_VERSION", "MINOR_VERSION", "PATCH_VERSION"]) { + if (mainV[key] == null || prV[key] == null) { + console.error(`::error::Failed to parse ${key} for ${name}`); + errors++; + } + } + if (errors > 0) { + console.log("::endgroup::"); + continue; + } + + const mainBytecode = forgeInspect(name, mainDir); + if (mainBytecode == null) { + console.error(`::error::Failed to compile ${name} on main`); + errors++; + console.log("::endgroup::"); + continue; + } + + const prBytecode = forgeInspect(name, prDir); + if (prBytecode == null) { + console.error(`::error::Failed to compile ${name} on PR`); + errors++; + console.log("::endgroup::"); + continue; + } + + const bytecodeChanged = mainBytecode !== prBytecode; + const reinitChanged = mainV.REINITIALIZER_VERSION !== prV.REINITIALIZER_VERSION; + const versionChanged = + mainV.MAJOR_VERSION !== prV.MAJOR_VERSION || + mainV.MINOR_VERSION !== prV.MINOR_VERSION || + mainV.PATCH_VERSION !== prV.PATCH_VERSION; + + if (!bytecodeChanged) { + console.log(`${name}: bytecode unchanged`); + if (reinitChanged) { + console.error( + `::error::${name} REINITIALIZER_VERSION bumped (${mainV.REINITIALIZER_VERSION} -> ${prV.REINITIALIZER_VERSION}) but bytecode is unchanged`, + ); + errors++; + } + console.log("::endgroup::"); + continue; + } + + console.log(`${name}: bytecode CHANGED`); + + if (!reinitChanged) { + console.error( + `::error::${name} bytecode changed but REINITIALIZER_VERSION was not bumped (still ${prV.REINITIALIZER_VERSION})`, + ); + errors++; + } else { + // Convention: reinitializeV{N-1} for REINITIALIZER_VERSION=N + const expectedFn = `reinitializeV${prV.REINITIALIZER_VERSION - 1}`; + const prSrc = readFileSync(prSol, "utf-8"); + if (!new RegExp(`function\\s+${expectedFn}\\s*\\(`).test(prSrc)) { + console.error( + `::error::${name} has REINITIALIZER_VERSION=${prV.REINITIALIZER_VERSION} but no ${expectedFn}() function found`, + ); + errors++; + } + } + + if (!versionChanged) { + console.error( + `::error::${name} bytecode changed but semantic version was not bumped (still v${prV.MAJOR_VERSION}.${prV.MINOR_VERSION}.${prV.PATCH_VERSION})`, + ); + errors++; + } + + console.log("::endgroup::"); +} + +if (errors > 0) { + console.error(`::error::Upgrade hygiene check failed with ${errors} error(s)`); + process.exit(1); +} + +console.log("All contracts passed upgrade hygiene checks"); From 1d771ca1f5ee49fea463893567eae7d34cca19fc Mon Sep 17 00:00:00 2001 From: Elias Tazartes Date: Fri, 13 Mar 2026 13:27:22 +0100 Subject: [PATCH 11/13] refactor: harden upgrade hygiene check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix cumulative error count bug: use local `parseFailed` flag so a parse failure in contract A doesn't skip contract B - Use try/finally for ::endgroup:: (7 sites → 1) - Strip both // and /* */ comments before checking for reinitializeV{N} function (prevents false positives from commented-out signatures) - Surface forge stderr on compilation failure instead of swallowing it - Avoid redundant file read: extractVersions returns source alongside versions - Document why foundry.toml is copied to main's checkout --- .../workflows/contracts-upgrade-hygiene.yml | 1 + ci/check-upgrade-hygiene.ts | 150 +++++++++--------- 2 files changed, 74 insertions(+), 77 deletions(-) diff --git a/.github/workflows/contracts-upgrade-hygiene.yml b/.github/workflows/contracts-upgrade-hygiene.yml index 45eecd136d..27b1470371 100644 --- a/.github/workflows/contracts-upgrade-hygiene.yml +++ b/.github/workflows/contracts-upgrade-hygiene.yml @@ -93,6 +93,7 @@ jobs: env: PACKAGE: ${{ matrix.package }} run: | + # Use PR's foundry.toml for both so compiler settings match (cbor_metadata, bytecode_hash) cp "$PACKAGE/foundry.toml" "main-branch/$PACKAGE/foundry.toml" for dir in "$PACKAGE" "main-branch/$PACKAGE"; do mkdir -p "$dir/addresses" diff --git a/ci/check-upgrade-hygiene.ts b/ci/check-upgrade-hygiene.ts index 638f987dcc..2d49bba71f 100644 --- a/ci/check-upgrade-hygiene.ts +++ b/ci/check-upgrade-hygiene.ts @@ -21,21 +21,22 @@ if (!existsSync(manifestPath)) { const VERSION_RE = /(?REINITIALIZER_VERSION|MAJOR_VERSION|MINOR_VERSION|PATCH_VERSION)\s*=\s*(?\d+)/g; function extractVersions(filePath: string) { - const src = readFileSync(filePath, "utf-8"); + const source = readFileSync(filePath, "utf-8"); const versions: Record = {}; - for (const { groups } of src.matchAll(VERSION_RE)) { + for (const { groups } of source.matchAll(VERSION_RE)) { versions[groups!.name] = Number(groups!.value); } - return versions; + return { versions, source }; } function forgeInspect(contract: string, root: string): string | null { try { return execSync(`forge inspect "contracts/${contract}.sol:${contract}" --root "${root}" deployedBytecode`, { encoding: "utf-8", - stdio: ["pipe", "pipe", "ignore"], + stdio: ["pipe", "pipe", "pipe"], }).trim(); - } catch { + } catch (e: any) { + if (e.stderr) console.error(String(e.stderr)); return null; } } @@ -45,99 +46,94 @@ let errors = 0; for (const name of contracts) { console.log(`::group::Checking ${name}`); + try { + const mainSol = join(mainDir, "contracts", `${name}.sol`); + const prSol = join(prDir, "contracts", `${name}.sol`); - const mainSol = join(mainDir, "contracts", `${name}.sol`); - const prSol = join(prDir, "contracts", `${name}.sol`); + if (!existsSync(mainSol)) { + console.log(`Skipping ${name} (new contract, not on main)`); + continue; + } - if (!existsSync(mainSol)) { - console.log(`Skipping ${name} (new contract, not on main)`); - console.log("::endgroup::"); - continue; - } + if (!existsSync(prSol)) { + console.error(`::error::${name} listed in upgrade-manifest.json but missing in PR`); + errors++; + continue; + } - if (!existsSync(prSol)) { - console.error(`::error::${name} listed in upgrade-manifest.json but missing in PR`); - errors++; - console.log("::endgroup::"); - continue; - } + const { versions: mainV } = extractVersions(mainSol); + const { versions: prV, source: prSrc } = extractVersions(prSol); - const mainV = extractVersions(mainSol); - const prV = extractVersions(prSol); + let parseFailed = false; + for (const key of ["REINITIALIZER_VERSION", "MAJOR_VERSION", "MINOR_VERSION", "PATCH_VERSION"]) { + if (mainV[key] == null || prV[key] == null) { + console.error(`::error::Failed to parse ${key} for ${name}`); + errors++; + parseFailed = true; + } + } + if (parseFailed) continue; - for (const key of ["REINITIALIZER_VERSION", "MAJOR_VERSION", "MINOR_VERSION", "PATCH_VERSION"]) { - if (mainV[key] == null || prV[key] == null) { - console.error(`::error::Failed to parse ${key} for ${name}`); + const mainBytecode = forgeInspect(name, mainDir); + if (mainBytecode == null) { + console.error(`::error::Failed to compile ${name} on main`); errors++; + continue; } - } - if (errors > 0) { - console.log("::endgroup::"); - continue; - } - const mainBytecode = forgeInspect(name, mainDir); - if (mainBytecode == null) { - console.error(`::error::Failed to compile ${name} on main`); - errors++; - console.log("::endgroup::"); - continue; - } + const prBytecode = forgeInspect(name, prDir); + if (prBytecode == null) { + console.error(`::error::Failed to compile ${name} on PR`); + errors++; + continue; + } - const prBytecode = forgeInspect(name, prDir); - if (prBytecode == null) { - console.error(`::error::Failed to compile ${name} on PR`); - errors++; - console.log("::endgroup::"); - continue; - } + const bytecodeChanged = mainBytecode !== prBytecode; + const reinitChanged = mainV.REINITIALIZER_VERSION !== prV.REINITIALIZER_VERSION; + const versionChanged = + mainV.MAJOR_VERSION !== prV.MAJOR_VERSION || + mainV.MINOR_VERSION !== prV.MINOR_VERSION || + mainV.PATCH_VERSION !== prV.PATCH_VERSION; + + if (!bytecodeChanged) { + console.log(`${name}: bytecode unchanged`); + if (reinitChanged) { + console.error( + `::error::${name} REINITIALIZER_VERSION bumped (${mainV.REINITIALIZER_VERSION} -> ${prV.REINITIALIZER_VERSION}) but bytecode is unchanged`, + ); + errors++; + } + continue; + } - const bytecodeChanged = mainBytecode !== prBytecode; - const reinitChanged = mainV.REINITIALIZER_VERSION !== prV.REINITIALIZER_VERSION; - const versionChanged = - mainV.MAJOR_VERSION !== prV.MAJOR_VERSION || - mainV.MINOR_VERSION !== prV.MINOR_VERSION || - mainV.PATCH_VERSION !== prV.PATCH_VERSION; + console.log(`${name}: bytecode CHANGED`); - if (!bytecodeChanged) { - console.log(`${name}: bytecode unchanged`); - if (reinitChanged) { + if (!reinitChanged) { console.error( - `::error::${name} REINITIALIZER_VERSION bumped (${mainV.REINITIALIZER_VERSION} -> ${prV.REINITIALIZER_VERSION}) but bytecode is unchanged`, + `::error::${name} bytecode changed but REINITIALIZER_VERSION was not bumped (still ${prV.REINITIALIZER_VERSION})`, ); errors++; + } else { + // Convention: reinitializeV{N-1} for REINITIALIZER_VERSION=N + const expectedFn = `reinitializeV${prV.REINITIALIZER_VERSION - 1}`; + const uncommented = prSrc.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, ""); + if (!new RegExp(`function\\s+${expectedFn}\\s*\\(`).test(uncommented)) { + console.error( + `::error::${name} has REINITIALIZER_VERSION=${prV.REINITIALIZER_VERSION} but no ${expectedFn}() function found`, + ); + errors++; + } } - console.log("::endgroup::"); - continue; - } - console.log(`${name}: bytecode CHANGED`); - - if (!reinitChanged) { - console.error( - `::error::${name} bytecode changed but REINITIALIZER_VERSION was not bumped (still ${prV.REINITIALIZER_VERSION})`, - ); - errors++; - } else { - // Convention: reinitializeV{N-1} for REINITIALIZER_VERSION=N - const expectedFn = `reinitializeV${prV.REINITIALIZER_VERSION - 1}`; - const prSrc = readFileSync(prSol, "utf-8"); - if (!new RegExp(`function\\s+${expectedFn}\\s*\\(`).test(prSrc)) { + if (!versionChanged) { console.error( - `::error::${name} has REINITIALIZER_VERSION=${prV.REINITIALIZER_VERSION} but no ${expectedFn}() function found`, + `::error::${name} bytecode changed but semantic version was not bumped (still v${prV.MAJOR_VERSION}.${prV.MINOR_VERSION}.${prV.PATCH_VERSION})`, ); errors++; } + } finally { + console.log("::endgroup::"); } - - if (!versionChanged) { - console.error( - `::error::${name} bytecode changed but semantic version was not bumped (still v${prV.MAJOR_VERSION}.${prV.MINOR_VERSION}.${prV.PATCH_VERSION})`, - ); - errors++; - } - - console.log("::endgroup::"); } if (errors > 0) { From df7988b338fecccb607f2b442196250aa350f77f Mon Sep 17 00:00:00 2001 From: Elias Tazartes Date: Fri, 13 Mar 2026 13:43:24 +0100 Subject: [PATCH 12/13] docs: add explanatory comments to CI address stubs --- ci/stubs/gateway-contracts/GatewayAddresses.sol | 2 ++ ci/stubs/gateway-contracts/PaymentBridgingAddresses.sol | 2 ++ ci/stubs/host-contracts/FHEVMHostAddresses.sol | 2 ++ 3 files changed, 6 insertions(+) diff --git a/ci/stubs/gateway-contracts/GatewayAddresses.sol b/ci/stubs/gateway-contracts/GatewayAddresses.sol index fb8fdd05e4..cd5400e287 100644 --- a/ci/stubs/gateway-contracts/GatewayAddresses.sol +++ b/ci/stubs/gateway-contracts/GatewayAddresses.sol @@ -1,4 +1,6 @@ // SPDX-License-Identifier: BSD-3-Clause-Clear +// Stub: the real addresses/ files are generated at deploy time and gitignored. +// These dummy values let forge compile the contracts for bytecode comparison in CI. pragma solidity ^0.8.24; address constant gatewayConfigAddress = 0x0000000000000000000000000000000000000001; address constant decryptionAddress = 0x0000000000000000000000000000000000000002; diff --git a/ci/stubs/gateway-contracts/PaymentBridgingAddresses.sol b/ci/stubs/gateway-contracts/PaymentBridgingAddresses.sol index 3df423f918..74a035fa1e 100644 --- a/ci/stubs/gateway-contracts/PaymentBridgingAddresses.sol +++ b/ci/stubs/gateway-contracts/PaymentBridgingAddresses.sol @@ -1,4 +1,6 @@ // SPDX-License-Identifier: BSD-3-Clause-Clear +// Stub: the real addresses/ files are generated at deploy time and gitignored. +// These dummy values let forge compile the contracts for bytecode comparison in CI. pragma solidity ^0.8.24; address constant feesSenderToBurnerAddress = 0x0000000000000000000000000000000000000008; address constant zamaOFTAddress = 0x0000000000000000000000000000000000000009; diff --git a/ci/stubs/host-contracts/FHEVMHostAddresses.sol b/ci/stubs/host-contracts/FHEVMHostAddresses.sol index 8de94ae307..1ed52d2f66 100644 --- a/ci/stubs/host-contracts/FHEVMHostAddresses.sol +++ b/ci/stubs/host-contracts/FHEVMHostAddresses.sol @@ -1,4 +1,6 @@ // SPDX-License-Identifier: BSD-3-Clause-Clear +// Stub: the real addresses/ files are generated at deploy time and gitignored. +// These dummy values let forge compile the contracts for bytecode comparison in CI. pragma solidity ^0.8.24; address constant aclAdd = 0x0000000000000000000000000000000000000001; address constant fhevmExecutorAdd = 0x0000000000000000000000000000000000000002; From 48d1154d8943cdcd8a32fddfd984fb915d172fea Mon Sep 17 00:00:00 2001 From: Elias Tazartes Date: Fri, 13 Mar 2026 14:00:11 +0100 Subject: [PATCH 13/13] fix: add (bpr) suffix and conditional cancel-in-progress --- .github/workflows/contracts-upgrade-hygiene.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/contracts-upgrade-hygiene.yml b/.github/workflows/contracts-upgrade-hygiene.yml index 27b1470371..341565d665 100644 --- a/.github/workflows/contracts-upgrade-hygiene.yml +++ b/.github/workflows/contracts-upgrade-hygiene.yml @@ -7,7 +7,7 @@ on: concurrency: group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} jobs: check-changes: @@ -38,7 +38,7 @@ jobs: - gateway-contracts/** check: - name: contracts-upgrade-hygiene/${{ matrix.package }} + name: contracts-upgrade-hygiene/${{ matrix.package }} (bpr) needs: check-changes if: ${{ needs.check-changes.outputs.packages != '[]' }} permissions: