|
| 1 | +name: Foundry |
| 2 | + |
| 3 | +on: |
| 4 | + workflow_dispatch: |
| 5 | + pull_request: |
| 6 | + push: |
| 7 | + branches: |
| 8 | + - main |
| 9 | + |
| 10 | + |
| 11 | +env: |
| 12 | + FOUNDRY_PROFILE: medium |
| 13 | + |
| 14 | +jobs: |
| 15 | + # ----------------------------------------------------------------------- |
| 16 | + # Forge Test |
| 17 | + # ----------------------------------------------------------------------- |
| 18 | + |
| 19 | + test-suite: |
| 20 | + name: Test |
| 21 | + runs-on: protocol-x64-16core |
| 22 | + strategy: |
| 23 | + matrix: |
| 24 | + suite: [Unit, Integration, Fork] |
| 25 | + |
| 26 | + steps: |
| 27 | + # Check out repository with all submodules for complete codebase access. |
| 28 | + - uses: actions/checkout@v4 |
| 29 | + with: |
| 30 | + submodules: recursive |
| 31 | + |
| 32 | + # Restore Forge cache |
| 33 | + - name: Cache Forge Build |
| 34 | + uses: actions/cache@v3 |
| 35 | + with: |
| 36 | + path: | |
| 37 | + cache/ |
| 38 | + out/ |
| 39 | + key: ${{ runner.os }}-forge-${{ hashFiles('**/foundry.toml', '**/remappings.txt', 'src/**/*.sol', 'lib/**/*.sol') }} |
| 40 | + restore-keys: | |
| 41 | + ${{ runner.os }}-forge- |
| 42 | +
|
| 43 | + # Install the Foundry toolchain. |
| 44 | + - name: Install Foundry |
| 45 | + uses: foundry-rs/foundry-toolchain@v1 |
| 46 | + with: |
| 47 | + version: stable |
| 48 | + |
| 49 | + # Run Forge's formatting checker to ensure consistent code style. |
| 50 | + - name: "Forge Fmt" |
| 51 | + run: | |
| 52 | + forge fmt --check |
| 53 | + FOUNDRY_PROFILE=test forge fmt --check |
| 54 | + id: fmt |
| 55 | + |
| 56 | + # Build the project and display contract sizes. |
| 57 | + - name: Forge Build |
| 58 | + run: | |
| 59 | + forge --version |
| 60 | + forge build --sizes |
| 61 | +
|
| 62 | + # Run the test suite in parallel based on the matrix configuration. |
| 63 | + - name: Run ${{ matrix.suite }} tests |
| 64 | + run: | |
| 65 | + case "${{ matrix.suite }}" in |
| 66 | + Unit) forge test --no-match-contract Integration ;; |
| 67 | + Integration) forge test --match-contract Integration ;; |
| 68 | + Fork) forge test --match-contract Integration ;; |
| 69 | + esac |
| 70 | + env: |
| 71 | + FOUNDRY_PROFILE: ${{ matrix.suite == 'Fork' && 'forktest' || 'medium' }} |
| 72 | + RPC_MAINNET: https://billowing-capable-sound.quiknode.pro/ |
| 73 | + |
| 74 | + # ----------------------------------------------------------------------- |
| 75 | + # Forge Storage Diff |
| 76 | + # ----------------------------------------------------------------------- |
| 77 | + |
| 78 | + storage-diff: |
| 79 | + name: Test (Storage) |
| 80 | + runs-on: protocol-x64-16core |
| 81 | + steps: |
| 82 | + # Check out repository with all submodules for complete codebase access. |
| 83 | + - uses: actions/checkout@v4 |
| 84 | + with: |
| 85 | + submodules: recursive |
| 86 | + |
| 87 | + # Restore Forge cache |
| 88 | + - name: Cache Forge Build |
| 89 | + uses: actions/cache@v3 |
| 90 | + with: |
| 91 | + path: | |
| 92 | + cache/ |
| 93 | + out/ |
| 94 | + key: ${{ runner.os }}-forge-${{ hashFiles('**/foundry.toml', '**/remappings.txt', 'src/**/*.sol', 'lib/**/*.sol') }} |
| 95 | + restore-keys: | |
| 96 | + ${{ runner.os }}-forge- |
| 97 | +
|
| 98 | + # Install the Foundry toolchain. |
| 99 | + - name: "Install Foundry" |
| 100 | + uses: foundry-rs/foundry-toolchain@v1 |
| 101 | + with: |
| 102 | + version: stable |
| 103 | + |
| 104 | + # Run storage diff check to detect storage layout incompatibilities. |
| 105 | + - name: "Mainnet Storage Diff" |
| 106 | + run: | |
| 107 | + bash bin/storage-diff.sh --rpc-url ${{ secrets.RPC_MAINNET }} --etherscan-key ${{ secrets.ETHERSCAN_API_KEY }} --input .github/configs/storage-diff.json |
| 108 | + id: storage-diff |
| 109 | + |
| 110 | + # ----------------------------------------------------------------------- |
| 111 | + # Forge Coverage |
| 112 | + # ----------------------------------------------------------------------- |
| 113 | + |
| 114 | + run-coverage: |
| 115 | + name: Coverage |
| 116 | + runs-on: protocol-x64-16core |
| 117 | + # Only run coverage checks on main, preprod, testnet, and mainnet branches, or PRs targeting these branches |
| 118 | + if: | |
| 119 | + github.ref == 'refs/heads/main' || |
| 120 | + github.ref == 'refs/heads/preprod' || |
| 121 | + github.ref == 'refs/heads/testnet' || |
| 122 | + github.ref == 'refs/heads/mainnet' || |
| 123 | + github.base_ref == 'main' || |
| 124 | + github.base_ref == 'preprod' || |
| 125 | + github.base_ref == 'testnet' || |
| 126 | + github.base_ref == 'mainnet' |
| 127 | + strategy: |
| 128 | + fail-fast: true |
| 129 | + steps: |
| 130 | + # Check out repository with all submodules for complete codebase access. |
| 131 | + - uses: actions/checkout@v4 |
| 132 | + with: |
| 133 | + submodules: recursive |
| 134 | + |
| 135 | + # Restore Foundry and Forge cache |
| 136 | + - name: Cache Foundry Dependencies |
| 137 | + uses: actions/cache@v3 |
| 138 | + with: |
| 139 | + path: | |
| 140 | + ~/.cargo |
| 141 | + ~/.foundry |
| 142 | + out/ |
| 143 | + cache/ |
| 144 | + key: ${{ runner.os }}-forge-${{ hashFiles('**/foundry.toml', '**/remappings.txt', 'src/**/*.sol', 'lib/**/*.sol', '**/Cargo.lock') }} |
| 145 | + restore-keys: | |
| 146 | + ${{ runner.os }}-foundry- |
| 147 | + |
| 148 | + # Install the Foundry toolchain. |
| 149 | + - name: "Install Foundry" |
| 150 | + uses: foundry-rs/foundry-toolchain@v1 |
| 151 | + with: |
| 152 | + version: stable |
| 153 | + |
| 154 | + # Install LCOV for coverage report generation. |
| 155 | + - name: Install LCOV (Prebuilt) |
| 156 | + run: | |
| 157 | + curl -L https://github.com/linux-test-project/lcov/releases/download/v1.16/lcov-1.16.tar.gz | tar xz |
| 158 | + sudo cp lcov-1.16/bin/* /usr/local/bin/ |
| 159 | + sudo cp -r lcov-1.16/man/* /usr/share/man/ |
| 160 | +
|
| 161 | + # Build the project and display contract sizes. |
| 162 | + - name: "Forge Build" |
| 163 | + run: | |
| 164 | + forge --version |
| 165 | + forge build --sizes |
| 166 | + id: build |
| 167 | + |
| 168 | + # Run Forge coverage with LCOV report format, excluding test and script files |
| 169 | + - name: Forge Coverage |
| 170 | + run: | |
| 171 | + FOUNDRY_DENY_WARNINGS=false \ |
| 172 | + FOUNDRY_PROFILE=coverage \ |
| 173 | + FOUNDRY_CACHE=true \ |
| 174 | + FOUNDRY_CACHE_PATH=cache \ |
| 175 | + forge coverage --report lcov --report summary --no-match-coverage "script|test" -j $(nproc) |
| 176 | + genhtml -q -o report ./lcov.info |
| 177 | +
|
| 178 | + # Upload coverage report as artifact before potential failure |
| 179 | + - name: Upload Coverage Report |
| 180 | + uses: actions/upload-artifact@v4 |
| 181 | + with: |
| 182 | + name: code-coverage-report |
| 183 | + path: report/* |
| 184 | + if-no-files-found: error |
| 185 | + |
| 186 | + # Check coverage threshold after uploading report |
| 187 | + - name: Check Coverage Threshold |
| 188 | + run: | |
| 189 | + LINES_PCT=$(lcov --summary lcov.info | grep "lines" | cut -d ':' -f 2 | cut -d '%' -f 1 | tr -d '[:space:]') |
| 190 | + FUNCTIONS_PCT=$(lcov --summary lcov.info | grep "functions" | cut -d ':' -f 2 | cut -d '%' -f 1 | tr -d '[:space:]') |
| 191 | + FAILED=0 |
| 192 | + |
| 193 | + if (( $(echo "$LINES_PCT < 90" | bc -l) )); then |
| 194 | + echo -e "\033[1;31mβ Lines coverage ($LINES_PCT%) is below minimum threshold of 90%\033[0m" |
| 195 | + FAILED=1 |
| 196 | + else |
| 197 | + echo -e "\033[1;32mβ
Lines coverage ($LINES_PCT%) meets minimum threshold of 90%\033[0m" |
| 198 | + fi |
| 199 | + |
| 200 | + if (( $(echo "$FUNCTIONS_PCT < 90" | bc -l) )); then |
| 201 | + echo -e "\033[1;31mβ Functions coverage ($FUNCTIONS_PCT%) is below minimum threshold of 90%\033[0m" |
| 202 | + FAILED=1 |
| 203 | + else |
| 204 | + echo -e "\033[1;32mβ
Functions coverage ($FUNCTIONS_PCT%) meets minimum threshold of 90%\033[0m" |
| 205 | + fi |
| 206 | + |
| 207 | + if [ $FAILED -eq 1 ]; then |
| 208 | + exit 1 |
| 209 | + fi |
| 210 | +
|
| 211 | + # ----------------------------------------------------------------------- |
| 212 | + # Forge Size Diff |
| 213 | + # ----------------------------------------------------------------------- |
| 214 | + |
| 215 | + compare-contract-sizes: |
| 216 | + name: Size Diff |
| 217 | + runs-on: protocol-x64-16core |
| 218 | + steps: |
| 219 | + # Check out repository with all submodules for complete codebase access. |
| 220 | + - uses: actions/checkout@v4 |
| 221 | + with: |
| 222 | + submodules: recursive |
| 223 | + |
| 224 | + # Install the Foundry toolchain. |
| 225 | + - name: "Install Foundry" |
| 226 | + uses: foundry-rs/foundry-toolchain@v1 |
| 227 | + with: |
| 228 | + version: stable |
| 229 | + |
| 230 | + - name: Build contracts on PR branch |
| 231 | + run: | |
| 232 | + forge build --json --sizes | jq '.' > pr_sizes.json |
| 233 | +
|
| 234 | + - name: Checkout target branch |
| 235 | + run: | |
| 236 | + git fetch origin ${{ github.base_ref }} |
| 237 | + git checkout ${{ github.base_ref }} |
| 238 | +
|
| 239 | + - name: Build contracts on target branch |
| 240 | + run: | |
| 241 | + forge build --json --sizes | jq '.' > target_sizes.json |
| 242 | +
|
| 243 | + - name: Compare contract sizes using Bash |
| 244 | + run: | |
| 245 | + # Extract contract names |
| 246 | + contracts=$(jq -r 'keys[]' pr_sizes.json) |
| 247 | +
|
| 248 | + # Track if there are any differences |
| 249 | + has_differences=0 |
| 250 | +
|
| 251 | + echo -e "\nπ \033[1;34mContract Size Comparison Report\033[0m π\n" |
| 252 | +
|
| 253 | + # Iterate through contracts and compare sizes |
| 254 | + for contract in $contracts; do |
| 255 | + pr_runtime=$(jq -r --arg contract "$contract" '.[$contract].runtime_size // 0' pr_sizes.json) |
| 256 | + pr_init=$(jq -r --arg contract "$contract" '.[$contract].init_size // 0' pr_sizes.json) |
| 257 | +
|
| 258 | + target_runtime=$(jq -r --arg contract "$contract" '.[$contract].runtime_size // 0' target_sizes.json) |
| 259 | + target_init=$(jq -r --arg contract "$contract" '.[$contract].init_size // 0' target_sizes.json) |
| 260 | +
|
| 261 | + runtime_diff=$((pr_runtime - target_runtime)) |
| 262 | + init_diff=$((pr_init - target_init)) |
| 263 | +
|
| 264 | + if [ "$runtime_diff" -ne 0 ] || [ "$init_diff" -ne 0 ]; then |
| 265 | + echo -e "\033[1;36mπ $contract:\033[0m" |
| 266 | + if [ "$runtime_diff" -ne 0 ]; then |
| 267 | + if [ "$runtime_diff" -gt 0 ]; then |
| 268 | + echo -e " Runtime: \033[1;31m+$runtime_diff bytes\033[0m π" |
| 269 | + else |
| 270 | + echo -e " Runtime: \033[1;32m$runtime_diff bytes\033[0m π" |
| 271 | + fi |
| 272 | + fi |
| 273 | + if [ "$init_diff" -ne 0 ]; then |
| 274 | + if [ "$init_diff" -gt 0 ]; then |
| 275 | + echo -e " Init: \033[1;31m+$init_diff bytes\033[0m π" |
| 276 | + else |
| 277 | + echo -e " Init: \033[1;32m$init_diff bytes\033[0m π" |
| 278 | + fi |
| 279 | + fi |
| 280 | + has_differences=1 |
| 281 | + fi |
| 282 | + done |
| 283 | +
|
| 284 | + if [ "$has_differences" -eq 0 ]; then |
| 285 | + echo -e "\033[1;32mβ¨ No contract size changes detected β¨\033[0m" |
| 286 | + fi |
0 commit comments