-
Notifications
You must be signed in to change notification settings - Fork 594
feat(ci): add interface change detection workflow #7597
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 8 commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
86ba0b0
feat(ci): add interface change detection workflow
larryob 913725b
Add interface/ to gitignore
larryob d7975c1
fix(ci): only fail interface check on function removals
larryob 4e8ee88
Merge main into interface-checker
larryob 6531b95
fix: update interface-analysis workflow to pnpm
larryob 75d0592
feat: detect return type changes in interface checker
larryob 6ada4df
feat: expand interface checker to detect all ABI removals
larryob 5211afa
Add TypeScript unit tests for interface checker
larryob 33726e6
Include abstract contracts in interface checker
larryob e2a1e80
Refactor interface tests to use template strings
larryob 692fd69
Make ContractConfig properties required and simplify additions_only
larryob 4df0aab
Refactor interface.sh to reduce code duplication
larryob 1df012a
Simplify constructor handling in interface.sh
larryob File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| name: Check Interface Changes | ||
|
|
||
| on: | ||
| pull_request: | ||
| paths: | ||
| - 'solidity/**' | ||
| - '.github/workflows/interface-analysis.yml' | ||
| workflow_dispatch: | ||
| inputs: | ||
| base: | ||
| description: 'Branch to compare against' | ||
| required: true | ||
| default: 'main' | ||
|
|
||
| jobs: | ||
| diff-check: | ||
| runs-on: ubuntu-latest | ||
| # Skip on changeset version PRs, as interfaces may change with version bumps | ||
| if: github.head_ref != 'changeset-release/main' | ||
| steps: | ||
| # Checkout the PR branch | ||
| - name: Checkout PR branch | ||
| uses: actions/checkout@v6 | ||
| with: | ||
| ref: ${{ github.event.pull_request.head.sha || github.sha }} | ||
| submodules: recursive | ||
|
|
||
| - uses: actions/setup-node@v6 | ||
| with: | ||
| node-version-file: .nvmrc | ||
|
|
||
| - name: pnpm-cache | ||
| uses: ./.github/actions/pnpm-cache | ||
|
|
||
| - name: pnpm-install | ||
| run: pnpm install --frozen-lockfile | ||
|
|
||
| - name: Setup Foundry | ||
| uses: ./.github/actions/setup-foundry | ||
|
|
||
| # Run the command on PR branch | ||
| - name: Run command on PR branch | ||
| run: pnpm -C solidity interface HEAD-interface | ||
|
|
||
| # Checkout the target branch (base) | ||
| - name: Checkout target branch (base) contracts | ||
| env: | ||
| BASE_REF: ${{ github.event.inputs.base || github.event.pull_request.base.sha }} | ||
| run: | | ||
| # Fetch the base reference | ||
| git fetch origin $BASE_REF | ||
| # Check if BASE_REF is a commit SHA (40 hex characters) or a branch name | ||
| if [[ "$BASE_REF" =~ ^[0-9a-f]{40}$ ]]; then | ||
| # For commit SHAs, checkout directly without origin/ prefix | ||
| git checkout $BASE_REF -- solidity/contracts | ||
| else | ||
| # For branch names, use origin/ prefix | ||
| git checkout origin/$BASE_REF -- solidity/contracts | ||
| fi | ||
|
|
||
| # Run the command on the target branch | ||
| - name: Run command on target branch | ||
| run: pnpm -C solidity interface base-interface | ||
|
|
||
| # Compare outputs (only fail on function removals) | ||
| - name: Compare outputs (fail on function removals) | ||
| run: pnpm -C solidity interface test-interface base-interface HEAD-interface |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,4 +28,6 @@ typechain-types/ | |
| typechain/ | ||
|
|
||
| # bytecode dumps | ||
| bytecode/ | ||
| bytecode/ | ||
| # interface dumps | ||
| interface/ | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,187 @@ | ||
| #!/bin/bash | ||
|
|
||
| # Usage: | ||
| # ./interface.sh [output-path] - Generate interface files | ||
| # ./interface.sh test-interface <base> <head> - Compare interfaces and fail on removals | ||
|
|
||
| OUTPUT_PATH=${1:-interface} | ||
|
|
||
| # If called with "test-interface", run comparison mode | ||
| if [ "$1" = "test-interface" ]; then | ||
| BASE_DIR=$2 | ||
| HEAD_DIR=$3 | ||
|
|
||
| if [ -z "$BASE_DIR" ] || [ -z "$HEAD_DIR" ]; then | ||
| echo "Usage: ./interface.sh test-interface <base-dir> <head-dir>" | ||
| exit 1 | ||
| fi | ||
|
|
||
| REMOVED_ITEMS="" | ||
| ADDED_ITEMS="" | ||
| HAS_REMOVALS=false | ||
|
|
||
| # Check each contract in base for removed ABI entries | ||
| for base_file in "$BASE_DIR"/*.json; do | ||
| [ -f "$base_file" ] || continue | ||
| contract_name=$(basename "$base_file" -abi.json) | ||
| head_file="$HEAD_DIR/$contract_name-abi.json" | ||
|
|
||
| if [ ! -f "$head_file" ]; then | ||
| echo "WARNING: Contract $contract_name was removed entirely" | ||
| HAS_REMOVALS=true | ||
| REMOVED_ITEMS="$REMOVED_ITEMS\n Contract removed: $contract_name" | ||
| continue | ||
| fi | ||
|
|
||
| # Extract function signatures: functionName(inputs)->(outputs) | ||
| base_funcs=$(jq -r '.[] | select(.type == "function") | "function " + .name + "(" + ([.inputs[].type] | join(",")) + ")->(" + ([.outputs[].type] | join(",")) + ")"' "$base_file" 2>/dev/null | sort) | ||
| head_funcs=$(jq -r '.[] | select(.type == "function") | "function " + .name + "(" + ([.inputs[].type] | join(",")) + ")->(" + ([.outputs[].type] | join(",")) + ")"' "$head_file" 2>/dev/null | sort) | ||
|
|
||
| # Extract event signatures: eventName(type1,type2,...) | ||
| base_events=$(jq -r '.[] | select(.type == "event") | "event " + .name + "(" + ([.inputs[].type] | join(",")) + ")"' "$base_file" 2>/dev/null | sort) | ||
| head_events=$(jq -r '.[] | select(.type == "event") | "event " + .name + "(" + ([.inputs[].type] | join(",")) + ")"' "$head_file" 2>/dev/null | sort) | ||
|
|
||
| # Extract error signatures: errorName(type1,type2,...) | ||
| base_errors=$(jq -r '.[] | select(.type == "error") | "error " + .name + "(" + ([.inputs[].type] | join(",")) + ")"' "$base_file" 2>/dev/null | sort) | ||
| head_errors=$(jq -r '.[] | select(.type == "error") | "error " + .name + "(" + ([.inputs[].type] | join(",")) + ")"' "$head_file" 2>/dev/null | sort) | ||
|
|
||
| # Extract constructor signature | ||
| base_constructor=$(jq -r '.[] | select(.type == "constructor") | "constructor(" + ([.inputs[].type] | join(",")) + ")"' "$base_file" 2>/dev/null) | ||
| head_constructor=$(jq -r '.[] | select(.type == "constructor") | "constructor(" + ([.inputs[].type] | join(",")) + ")"' "$head_file" 2>/dev/null) | ||
|
|
||
| # Extract fallback/receive | ||
| base_fallback=$(jq -r '.[] | select(.type == "fallback" or .type == "receive") | .type' "$base_file" 2>/dev/null | sort) | ||
| head_fallback=$(jq -r '.[] | select(.type == "fallback" or .type == "receive") | .type' "$head_file" 2>/dev/null | sort) | ||
|
larryob marked this conversation as resolved.
Outdated
larryob marked this conversation as resolved.
Outdated
|
||
|
|
||
| # Check for removed functions | ||
| while IFS= read -r item; do | ||
| [ -z "$item" ] && continue | ||
| if ! echo "$head_funcs" | grep -qxF "$item"; then | ||
| HAS_REMOVALS=true | ||
| REMOVED_ITEMS="$REMOVED_ITEMS\n $contract_name: $item" | ||
| fi | ||
| done <<< "$base_funcs" | ||
|
|
||
| # Check for removed events | ||
| while IFS= read -r item; do | ||
| [ -z "$item" ] && continue | ||
| if ! echo "$head_events" | grep -qxF "$item"; then | ||
| HAS_REMOVALS=true | ||
| REMOVED_ITEMS="$REMOVED_ITEMS\n $contract_name: $item" | ||
| fi | ||
| done <<< "$base_events" | ||
|
larryob marked this conversation as resolved.
Outdated
|
||
|
|
||
| # Check for removed errors | ||
| while IFS= read -r item; do | ||
| [ -z "$item" ] && continue | ||
| if ! echo "$head_errors" | grep -qxF "$item"; then | ||
| HAS_REMOVALS=true | ||
| REMOVED_ITEMS="$REMOVED_ITEMS\n $contract_name: $item" | ||
| fi | ||
| done <<< "$base_errors" | ||
|
|
||
| # Check for constructor changes | ||
| if [ -n "$base_constructor" ] && [ "$base_constructor" != "$head_constructor" ]; then | ||
| HAS_REMOVALS=true | ||
| REMOVED_ITEMS="$REMOVED_ITEMS\n $contract_name: $base_constructor -> ${head_constructor:-removed}" | ||
| fi | ||
|
|
||
| # Check for removed fallback/receive | ||
| while IFS= read -r item; do | ||
| [ -z "$item" ] && continue | ||
| if ! echo "$head_fallback" | grep -qxF "$item"; then | ||
| HAS_REMOVALS=true | ||
| REMOVED_ITEMS="$REMOVED_ITEMS\n $contract_name: $item" | ||
| fi | ||
| done <<< "$base_fallback" | ||
|
|
||
| # Track additions (non-breaking, for info) | ||
| while IFS= read -r item; do | ||
| [ -z "$item" ] && continue | ||
| if ! echo "$base_funcs" | grep -qxF "$item"; then | ||
| ADDED_ITEMS="$ADDED_ITEMS\n $contract_name: $item" | ||
| fi | ||
| done <<< "$head_funcs" | ||
|
|
||
| while IFS= read -r item; do | ||
| [ -z "$item" ] && continue | ||
| if ! echo "$base_events" | grep -qxF "$item"; then | ||
| ADDED_ITEMS="$ADDED_ITEMS\n $contract_name: $item" | ||
| fi | ||
| done <<< "$head_events" | ||
|
|
||
| while IFS= read -r item; do | ||
| [ -z "$item" ] && continue | ||
| if ! echo "$base_errors" | grep -qxF "$item"; then | ||
| ADDED_ITEMS="$ADDED_ITEMS\n $contract_name: $item" | ||
| fi | ||
| done <<< "$head_errors" | ||
| done | ||
|
|
||
| # Check for new contracts (additions) | ||
| for head_file in "$HEAD_DIR"/*.json; do | ||
| [ -f "$head_file" ] || continue | ||
| contract_name=$(basename "$head_file" -abi.json) | ||
| base_file="$BASE_DIR/$contract_name-abi.json" | ||
|
|
||
| if [ ! -f "$base_file" ]; then | ||
| echo "INFO: New contract added: $contract_name" | ||
| fi | ||
| done | ||
|
|
||
| # Report results | ||
| if [ -n "$ADDED_ITEMS" ]; then | ||
| echo "" | ||
| echo "ABI entries added (non-breaking):" | ||
| echo -e "$ADDED_ITEMS" | ||
| fi | ||
|
|
||
| if [ "$HAS_REMOVALS" = true ]; then | ||
| echo "" | ||
| echo "ERROR: ABI entries removed or changed (breaking change):" | ||
| echo -e "$REMOVED_ITEMS" | ||
| echo "" | ||
| echo "This PR removes or modifies entries in contract ABIs, which is a breaking change." | ||
| echo "If this is intentional, please review carefully." | ||
| exit 1 | ||
| fi | ||
|
|
||
| echo "" | ||
| echo "No breaking interface changes detected." | ||
| exit 0 | ||
| fi | ||
|
|
||
| # Generation mode | ||
| EXCLUDE="test|mock|interfaces|libs|upgrade|dependencies" | ||
|
|
||
| IFS=$'\n' | ||
| CONTRACT_FILES=($(find ./contracts -type f)) | ||
| unset IFS | ||
|
larryob marked this conversation as resolved.
|
||
|
|
||
| echo "Generating interfaces (ABIs) in $OUTPUT_PATH" | ||
| mkdir -p $OUTPUT_PATH | ||
|
larryob marked this conversation as resolved.
|
||
|
|
||
| for file in "${CONTRACT_FILES[@]}"; | ||
| do | ||
| if [[ $file =~ .*($EXCLUDE).* ]]; then | ||
| continue | ||
| fi | ||
|
|
||
| # Skip files that don't end in .sol | ||
| if [[ ! "$file" =~ \.sol$ ]]; then | ||
| continue | ||
| fi | ||
|
|
||
| # Extract all contract names from the file | ||
| contracts=$(grep -o '^contract [A-Za-z0-9_][A-Za-z0-9_]*' "$file" | sed 's/^contract //') | ||
|
larryob marked this conversation as resolved.
Outdated
|
||
|
|
||
| if [ -z "$contracts" ]; then | ||
| continue | ||
| fi | ||
|
|
||
| # Process each contract found in the file | ||
| for contract in $contracts; do | ||
| echo "Generating interface of $contract" | ||
| forge inspect "$contract" abi --json > "$OUTPUT_PATH/$contract-abi.json" | ||
| done | ||
|
larryob marked this conversation as resolved.
|
||
| done | ||
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
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.