From 7db6013e878fc5967ee015ecf849efab972bf4f1 Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Wed, 27 Nov 2024 03:46:32 +0000 Subject: [PATCH 01/23] feat(ci): carve out storage layout CI Since the storage layout CI needs the API keys to run, it does not work in PRs originating from forked repos. To avoid those unnecessary errors, this PR splits the workflow into two parts: the first that generates the compiled layouts and the second that fetches the deployed layouts and compares them. Such a split is possible because the second type of workflow runs in a trusted context, and hence, the API key is accessible to it. --- .github/workflows/compare-layouts.yml | 235 +++++++++++++++++ .../compare_deployed_storage_layout.py | 87 ------- .github/workflows/compare_storage_layout.py | 81 ------ .github/workflows/forge-ci.yml | 162 +++++------- .github/workflows/lint.yml | 24 +- ...y-setup.yml => reusable-foundry-setup.yml} | 10 +- .github/workflows/status-comment.yml | 80 ++++++ package-lock.json | 242 ++++++++++++++++++ package.json | 1 + script/compareLayouts.js | 47 ++++ src/storage/ClientChainGatewayStorage.sol | 2 +- 11 files changed, 678 insertions(+), 293 deletions(-) create mode 100644 .github/workflows/compare-layouts.yml delete mode 100644 .github/workflows/compare_deployed_storage_layout.py delete mode 100755 .github/workflows/compare_storage_layout.py rename .github/workflows/{foundry-setup.yml => reusable-foundry-setup.yml} (82%) create mode 100644 .github/workflows/status-comment.yml create mode 100644 script/compareLayouts.js diff --git a/.github/workflows/compare-layouts.yml b/.github/workflows/compare-layouts.yml new file mode 100644 index 00000000..6d829718 --- /dev/null +++ b/.github/workflows/compare-layouts.yml @@ -0,0 +1,235 @@ +name: Compare Storage Layouts + +on: + workflow_run: + workflows: ["Forge CI"] + types: + - completed + +permissions: + contents: read + statuses: write + pull-requests: write + +jobs: + # Do this job first so that the commit status is updated to pending ASAP. + set-commit-status: + runs-on: ubuntu-latest + steps: + - name: Set commit status + # trigger is no matter what, because the status should be updated + if: always() + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # this step would have been better located in forge-ci.yml, but since it needs the secret + # it is placed here. first, it is updated to pending here and failure/success is updated later. + run: | + curl -X POST \ + -H "Authorization: token $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + -d '{ + "state": "pending", + "context": "Compare Storage Layouts", + "description": "In progress...", + "target_url": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + }' \ + https://api.github.com/repos/${{ github.repository }}/statuses/${{ github.event.workflow_run.head_commit.id }} + + setup: + # A full job can be used as a reusable workflow but not a step. + uses: ./.github/workflows/reusable-foundry-setup.yml + with: + # The below line does not accept environment variables, + # so it becomes the single source of truth for the version, within this workflow. + # Any `pinning` of the version should be done here and forge-ci.yml. + foundry-version: nightly + + # The actual job to compare the storage layouts. + compare-storage-layouts: + needs: setup + runs-on: ubuntu-latest + + env: + ALCHEMY_API_KEY: ${{ secrets.ALCHEMY_API_KEY }} + ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + steps: + # Log the workflow trigger details for debugging. + - name: Echo workflow trigger details + run: | + echo "Workflow run event: ${{ github.event.workflow_run.event }}" + echo "Workflow run conclusion: ${{ github.event.workflow_run.conclusion }}" + echo "Workflow run name: ${{ github.event.workflow_run.name }}" + echo "Workflow run URL: ${{ github.event.workflow_run.html_url }}" + echo "Commit SHA: ${{ github.event.workflow_run.head_commit.id }}" + echo "Workflow Run ID: ${{ github.event.workflow_run.id }}" + # The repository needs to be available for script/deployedContracts.json + # and script/compareLayouts.js. We do not restore the build data because + # the compiled layouts are restored from the artifact and not rebuilt. + - name: Checkout the repository + if: ${{ github.event.workflow_run.conclusion == 'success' }} + uses: actions/checkout@v4 + - name: Get PR number + id: pr-context + if: ${{ github.event.workflow_run.event == 'pull_request' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_TARGET_REPO: ${{ github.repository }} + PR_BRANCH: |- + ${{ + (github.event.workflow_run.head_repository.owner.login != github.event.workflow_run.repository.owner.login) + && format('{0}:{1}', github.event.workflow_run.head_repository.owner.login, github.event.workflow_run.head_branch) + || github.event.workflow_run.head_branch + }} + run: | + gh pr view --repo "${PR_TARGET_REPO}" "${PR_BRANCH}" \ + --json 'number' --jq '"number=\(.number)"' \ + >> "${GITHUB_OUTPUT}" + - name: Set message + id: set-message + env: + WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} + WORKFLOW_NAME: ${{ github.workflow }} + SHA: ${{ github.event.workflow_run.head_commit.id }} + run: | + message="🚀 The $WORKFLOW_NAME workflow has started." + echo "message=$message Check the [workflow run]($WORKFLOW_URL) for progress. ($SHA)" >> "${GITHUB_OUTPUT}" + - name: Update CI Status + uses: marocchino/sticky-pull-request-comment@v2 + if: ${{ github.event.workflow_run.event == 'pull_request' }} + with: + header: ${{ github.workflow }} + hide_details: true + number: ${{ steps.pr-context.outputs.number }} + message: ${{ steps.set-message.outputs.message }} + # The toolchain is needed to run `cast storage` + - name: Restore cached Foundry toolchain + if: ${{ github.event.workflow_run.conclusion == 'success' }} + uses: actions/cache/restore@v3 + with: + path: ${{ needs.setup.outputs.installation-dir }} + key: ${{ needs.setup.outputs.cache-key }} + - name: Add Foundry to PATH + if: ${{ github.event.workflow_run.conclusion == 'success' }} + run: echo "${{ needs.setup.outputs.installation-dir }}" >> $GITHUB_PATH + - name: Fetch the deployed layouts + if: ${{ github.event.workflow_run.conclusion == 'success' }} + run: | + set -e + data=$(cat script/deployedContracts.json) + bootstrap=$(echo "$data" | jq -r '.clientChain.bootstrapLogic // empty') + clientGateway=$(echo "$data" | jq -r '.clientChain.clientGatewayLogic // empty') + vault=$(echo "$data" | jq -r '.clientChain.vaultImplementation // empty') + rewardVault=$(echo "$data" | jq -r '.clientChain.rewardVaultImplementation // empty') + capsule=$(echo "$data" | jq -r '.clientChain.capsuleImplementation // empty') + pwd=$(pwd) + cd /tmp + # Create an array of contract names and addresses + declare -A contracts=( + ["Bootstrap"]="$bootstrap" + ["ClientChainGateway"]="$clientGateway" + ["Vault"]="$vault" + ["RewardVault"]="$rewardVault" + ["ExoCapsule"]="$capsule" + ) + # Iterate over the array and run `cast storage` for each contract + for contract in "${!contracts[@]}"; do + address=${contracts[$contract]} + if [[ -n $address ]]; then + echo "Processing $contract at address $address" + cast storage --json "$address" --rpc-url "https://eth-sepolia.g.alchemy.com/v2/$ALCHEMY_API_KEY" \ + --etherscan-api-key "$ETHERSCAN_API_KEY" > "$contract.deployed.json" + mv "$contract.deployed.json" "$pwd" + else + echo "Skipping $contract as no address is provided" + fi + done + cd "$pwd" + # Restore the layouts from the previous job + - name: Restore the layout files from the artifact + if: ${{ github.event.workflow_run.conclusion == 'success' }} + uses: dawidd6/action-download-artifact@v6 + with: + name: storage-layouts-${{ github.event.workflow_run.head_commit.id }} + run_id: ${{ github.event.workflow_run.id }} + - name: Extract the restored layouts + if: ${{ github.event.workflow_run.conclusion == 'success' }} + run: unzip storage-layouts.zip + - name: Set up Node.js + if: ${{ github.event.workflow_run.conclusion == 'success' }} + uses: actions/setup-node@v2 + with: + node-version: '18' + - name: Clear npm cache + if: ${{ github.event.workflow_run.conclusion == 'success' }} + run: npm cache clean --force + - name: Install the required dependency + if: ${{ github.event.workflow_run.conclusion == 'success' }} + run: npm install @openzeppelin/upgrades-core + - name: Compare the layouts + if: ${{ github.event.workflow_run.conclusion == 'success' }} + id: compare-layouts + run: | + node script/compareLayouts.js + # Even if this fails, the CI status should be updated. + continue-on-error: true + - name: Update parent commit status + if: always() + # if the outcome is not set, it will post failure + run: | + outcome=$( + [[ "${{ steps.compare-layouts.outcome }}" == "success" ]] && + echo "success" || + echo "failure" + ) + description=$( + [[ "${{ steps.compare-layouts.outcome }}" == "success" ]] && + echo "Storage layouts match" || + ([[ "${{ steps.compare-layouts.outcome }}" == "failure" ]] && + echo "Storage layouts do not match" || + echo "Job skipped since ${{ github.event.workflow_run.name }} failed.") + ) + curl -X POST \ + -H "Authorization: token $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + -d "{ + \"state\": \"${outcome}\", + \"context\": \"Compare Storage Layouts\", + \"description\": \"${description}\", + \"target_url\": \"https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}\" + }" \ + https://api.github.com/repos/${{ github.repository }}/statuses/${{ github.event.workflow_run.head_commit.id }} + - name: Set message again + id: set-message-again + env: + WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} + WORKFLOW_NAME: ${{ github.workflow }} + SHA: ${{ github.event.workflow_run.head_commit.id }} + run: | + if [ ${{ steps.compare-layouts.outcome }} == "success" ]; then + message="✅ The $WORKFLOW_NAME workflow has completed successfully." + elif [ ${{ steps.compare-layouts.outcome }} == "failure" ]; then + message="❌ The $WORKFLOW_NAME workflow has failed!" + else + message="⏭ The $WORKFLOW_NAME workflow was skipped." + fi + echo "message=$message Check the [workflow run]($WORKFLOW_URL) for details. ($SHA)" >> "${GITHUB_OUTPUT}" + - name: Update CI Status + uses: marocchino/sticky-pull-request-comment@v2 + if: ${{ github.event.workflow_run.event == 'pull_request' }} + with: + header: ${{ github.workflow }} + hide_details: true + number: ${{ steps.pr-context.outputs.number }} + message: ${{ steps.set-message-again.outputs.message }} + - name: Exit with the correct code + if: always() + # if the outcome is not set, it will exit 1. so, a failure in the parent job will + # result in a failure here. + run: | + if [[ "${{ steps.compare-layouts.outcome }}" == "success" ]]; then + exit 0 + else + exit 1 + fi diff --git a/.github/workflows/compare_deployed_storage_layout.py b/.github/workflows/compare_deployed_storage_layout.py deleted file mode 100644 index 3c3737aa..00000000 --- a/.github/workflows/compare_deployed_storage_layout.py +++ /dev/null @@ -1,87 +0,0 @@ -#!/usr/bin/env python - -import json -import subprocess -import pandas as pd -import os -from compare_storage_layout import parse_output, compare_layouts, get_current_layout - -def get_deployed_addresses(): - with open('script/deployedContracts.json', 'r') as f: - data = json.load(f) - return { - 'Bootstrap': data['clientChain'].get('bootstrapLogic'), - 'ClientChainGateway': data['clientChain'].get('clientGatewayLogic'), - 'Vault': data['clientChain'].get('vaultImplementation'), - 'RewardVault': data['clientChain'].get('rewardVaultImplementation'), - 'ExoCapsule': data['clientChain'].get('capsuleImplementation') - } - -def get_storage_layout(contract_name, address, rpc_url, etherscan_api_key): - if not address: - print(f"Skipping {contract_name} as it's not deployed.") - return pd.DataFrame() - - result = subprocess.run(['cast', 'storage', address, '--rpc-url', rpc_url, '--etherscan-api-key', etherscan_api_key], capture_output=True, text=True) - print(f"finish executing: cast storage {address} --rpc-url ...") - - if result.returncode != 0: - raise Exception(f"Error getting current layout for {contract_name}: {result.stderr}") - - return parse_output(contract_name, result.stdout.split('\n')) - -def load_and_parse_layout(contract_name, path): - with open(path, 'r') as f: - lines = f.readlines() - return parse_output(contract_name, lines) - -if __name__ == "__main__": - try: - api_key = os.getenv('ALCHEMY_API_KEY') - if not api_key: - raise ValueError("ALCHEMY_API_KEY environment variable is not set") - etherscan_api_key = os.getenv('ETHERSCAN_API_KEY') - if not etherscan_api_key: - raise ValueError("ETHERSCAN_API_KEY environment variable is not set") - - # Construct the RPC URL for Sepolia - rpc_url = f"https://eth-sepolia.g.alchemy.com/v2/{api_key}" - - addresses = get_deployed_addresses() - all_mismatches = {} - - for contract_name, address in addresses.items(): - print(f"Checking {contract_name}...") - deployed_layout = get_storage_layout(contract_name, address, rpc_url, etherscan_api_key) - if deployed_layout.empty: - print(f"No deployed layout found for {contract_name}.") - continue - - current_layout = get_current_layout(contract_name) - if current_layout.empty: - raise ValueError(f"Error: No valid entries of current layout found for {contract_name}.") - - mismatches = compare_layouts(deployed_layout, current_layout) - if mismatches: - all_mismatches[contract_name] = mismatches - - # then we load the layout file of ExocoreGateway on target branch and compare it with the current layout - print("Checking ExocoreGateway...") - target_branch_layout = load_and_parse_layout('ExocoreGateway', 'ExocoreGateway_target.txt') - current_layout = get_current_layout('ExocoreGateway') - mismatches = compare_layouts(target_branch_layout, current_layout) - if mismatches: - all_mismatches['ExocoreGateway'] = mismatches - - if all_mismatches: - print("Mismatches found for current contracts:") - for contract, mismatches in all_mismatches.items(): - print(f"{contract}:") - for mismatch in mismatches: - print(f" {mismatch}") - exit(1) - else: - print("Storage layout is compatible with all deployed contracts.") - except Exception as e: - print(f"Error: {e}") - exit(1) diff --git a/.github/workflows/compare_storage_layout.py b/.github/workflows/compare_storage_layout.py deleted file mode 100755 index 7ec6aee7..00000000 --- a/.github/workflows/compare_storage_layout.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python - -import pandas as pd -import subprocess - -def parse_output(contract_name, lines): - # Clean up the output and create a dataframe - data = [] - separator_line = len(lines) - for i, line in enumerate(lines): # start from the line next to the separator - if i > separator_line and line.startswith('|'): - parts = [part.strip() for part in line.split('|')[1:-1]] # Remove empty first and last elements - data.append(parts[:6]) # Keep Name, Type, Slot, Offset, Bytes, Contract - elif line.startswith('|') and 'Name' in line: - separator_line = i + 1 - - if not data: - raise Exception(f"No valid storage layout data found for {contract_name}") - - df = pd.DataFrame(data, columns=['Name', 'Type', 'Slot', 'Offset', 'Bytes', 'Contract']) - - # Convert numeric columns - for col in ['Slot', 'Offset', 'Bytes']: - df[col] = pd.to_numeric(df[col]) - - return df - -def get_current_layout(contract_name): - result = subprocess.run(['forge', 'inspect', f'src/core/{contract_name}.sol:{contract_name}', 'storage-layout', '--pretty'], capture_output=True, text=True) - print(f"finished executing forge inspect for {contract_name}") - - if result.returncode != 0: - raise Exception(f"Error getting current layout for {contract_name}: {result.stderr}") - - return parse_output(contract_name, result.stdout.split('\n')) - -def compare_layouts(old_layout, new_layout): - mismatches = [] - - # Ensure both dataframes have the same columns - columns = ['Name', 'Type', 'Slot', 'Offset', 'Bytes'] - old_layout = old_layout[columns].copy() - new_layout = new_layout[columns].copy() - - # Compare non-gap variables - for index, row in old_layout.iterrows(): - if row['Name'] != '__gap': - current_row = new_layout.loc[new_layout['Name'] == row['Name']] - if current_row.empty: - mismatches.append(f"Variable {row['Name']} is missing in the current layout") - elif not current_row.iloc[0].equals(row): - mismatches.append(f"Variable {row['Name']} has changed") - - if not mismatches: - print("No mismatches found") - - return mismatches - -if __name__ == "__main__": - try: - clientChainGateway_layout = get_current_layout("ClientChainGateway") - bootstrap_layout = get_current_layout("Bootstrap") - - if clientChainGateway_layout.empty: - raise ValueError("Error: No valid entries found for ClientChainGateway.") - - if bootstrap_layout.empty: - raise ValueError("Error: No valid entries found for Bootstrap.") - - mismatches = compare_layouts(bootstrap_layout, clientChainGateway_layout) - - if mismatches: - print(f"Mismatches found: {len(mismatches)}") - for mismatch in mismatches: - print(mismatch) - exit(1) - else: - print("All entries in Bootstrap match ClientChainGateway at the correct positions.") - except Exception as e: - print(f"Error: {e}") - exit(1) diff --git a/.github/workflows/forge-ci.yml b/.github/workflows/forge-ci.yml index a5e0e735..6d7d38ac 100644 --- a/.github/workflows/forge-ci.yml +++ b/.github/workflows/forge-ci.yml @@ -1,4 +1,4 @@ -name: Forge CI to build, test, format and compare storage layout +name: Forge CI on: merge_group: @@ -12,21 +12,25 @@ on: jobs: setup: - uses: ./.github/workflows/foundry-setup.yml + # A full job can be used as a reusable workflow but not a step. + uses: ./.github/workflows/reusable-foundry-setup.yml with: + # The below line does not accept environment variables, + # so it becomes the single source of truth for the version. foundry-version: nightly build: runs-on: ubuntu-latest needs: setup outputs: - installation-dir: ${{ needs.setup.outputs.installation-dir }} + # The cache-key only contains the version name. It is only used so that the name does not + # need to be repeated everywhere; instead setting the `foundry-version` above suffices. cache-key: ${{ needs.setup.outputs.cache-key }} + # Github's cache actions are a bit weird to deal with. It wouldn't let me restore the + # binaries to /usr/bin, so I restored them to the original location and added it to PATH. + # This output will let us carry it to other jobs. + installation-dir: ${{ needs.setup.outputs.installation-dir }} steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - submodules: recursive - name: Restore cached Foundry toolchain uses: actions/cache/restore@v3 with: @@ -34,20 +38,12 @@ jobs: key: ${{ needs.setup.outputs.cache-key }} - name: Add Foundry to PATH run: echo "${{ needs.setup.outputs.installation-dir }}" >> $GITHUB_PATH + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive - name: Build run: forge build - - name: Add comment for build failure - if: failure() - uses: actions/github-script@v6 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: 'The build has failed. Please check the logs.' - }) - name: Cache build artifacts uses: actions/cache/save@v3 with: @@ -56,14 +52,12 @@ jobs: ./out ./cache ./broadcast - key: ${{ runner.os }}-build-${{ github.sha }} + key: build-${{ github.event.pull_request.head.sha || github.event.after || github.sha }} test: runs-on: ubuntu-latest needs: build steps: - - name: Checkout repository - uses: actions/checkout@v4 - name: Restore cached Foundry toolchain uses: actions/cache/restore@v3 with: @@ -71,6 +65,8 @@ jobs: key: ${{ needs.build.outputs.cache-key }} - name: Add Foundry to PATH run: echo "${{ needs.build.outputs.installation-dir }}" >> $GITHUB_PATH + - name: Checkout repository + uses: actions/checkout@v4 - name: Restore build artifacts uses: actions/cache/restore@v3 with: @@ -79,30 +75,16 @@ jobs: ./out ./cache ./broadcast - key: ${{ runner.os }}-build-${{ github.sha }} + key: build-${{ github.event.pull_request.head.sha || github.event.after || github.sha }} - name: Test run: forge test -vvv - name: Set test snapshot as summary run: NO_COLOR=1 forge snapshot >> $GITHUB_STEP_SUMMARY - - name: Add comment for test failure - if: failure() - uses: actions/github-script@v6 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: 'The tests have failed. Please check the logs.' - }) format: runs-on: ubuntu-latest needs: build steps: - - name: Checkout repository - uses: actions/checkout@v4 - name: Restore cached Foundry toolchain uses: actions/cache/restore@v3 with: @@ -110,6 +92,8 @@ jobs: key: ${{ needs.build.outputs.cache-key }} - name: Add Foundry to PATH run: echo "${{ needs.build.outputs.installation-dir }}" >> $GITHUB_PATH + - name: Checkout repository + uses: actions/checkout@v4 - name: Restore build artifacts uses: actions/cache/restore@v3 with: @@ -118,31 +102,14 @@ jobs: ./out ./cache ./broadcast - key: ${{ runner.os }}-build-${{ github.sha }} + key: build-${{ github.event.pull_request.head.sha || github.event.after || github.sha }} - name: Check formatting run: forge fmt --check - - name: Add comment for format check failure - if: failure() - uses: actions/github-script@v6 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: 'The code is not formatted correctly. Please run `forge fmt` and push the changes.' - }) - compare-storage-layout: + extract-storage-layouts: runs-on: ubuntu-latest needs: build - env: - ALCHEMY_API_KEY: ${{ secrets.ALCHEMY_API_KEY }} - ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }} steps: - - name: Checkout repository - uses: actions/checkout@v4 - name: Restore cached Foundry toolchain uses: actions/cache/restore@v3 with: @@ -150,56 +117,55 @@ jobs: key: ${{ needs.build.outputs.cache-key }} - name: Add Foundry to PATH run: echo "${{ needs.build.outputs.installation-dir }}" >> $GITHUB_PATH - - name: Restore build artifacts - uses: actions/cache/restore@v3 - with: - path: | - ./lib - ./out - ./cache - ./broadcast - key: ${{ runner.os }}-build-${{ github.sha }} - - name: Checkout target branch + - name: Checkout base branch or previous commit uses: actions/checkout@v4 with: - ref: ${{ github.event.pull_request.base.ref }} + ref: ${{ github.event.pull_request.base.ref || github.event.before }} + # We don't have a `lib` folder to restore for this step, so we + # recursively checkout the submodules. In other steps, we use the + # `lib` folder from the `build` job. submodules: recursive - - name: Generate target branch layout files + - name: Generate base branch layout file + # Note that this `run` will do a `forge build` so we don't need to do it ourselves. + # The build artifacts of this step are not relevant to us either, so we don't need to + # cache them. run: | - forge inspect src/core/ExocoreGateway.sol:ExocoreGateway storage-layout --pretty > ExocoreGateway_target.txt - - name: Cache target branch layout file - uses: actions/cache/save@v3 - with: - path: ExocoreGateway_target.txt - key: ${{ runner.os }}-exocore-target-${{ github.sha }} + forge inspect src/core/ExocoreGateway.sol:ExocoreGateway storage-layout > ExocoreGateway.base.json + - name: Copy base branch layout file + run: cp ExocoreGateway.base.json /tmp/ + # Now we can generate the layout for the PR level. - name: Checkout back to PR uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} - - name: Restore target branch layout file + # Restoring these will help make `forge inspect` faster. + - name: Restore build artifacts uses: actions/cache/restore@v3 with: - path: ExocoreGateway_target.txt - key: ${{ runner.os }}-exocore-target-${{ github.sha }} - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.12.4' - - name: Install pandas - run: pip install --root-user-action=ignore pandas==2.2.2 - - name: Run the comparison script for Bootstrap and ClientChainGateway - run: python .github/workflows/compare_storage_layout.py - - name: Run the comparison script for deployed contracts - run: python .github/workflows/compare_deployed_storage_layout.py - - name: Add comment for storage layout mismatch failure - if: failure() - uses: actions/github-script@v6 + path: | + ./lib + ./out + ./cache + ./broadcast + key: build-${{ github.event.pull_request.head.sha || github.event.after || github.sha }} + # This step restores the base branch layout file, while the next step generates the PR + # branch layout files. + - name: Restore base branch layout file + uses: actions/cache/restore@v3 with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: 'Storage layout compatibility check failed. This could be due to a mismatch between Bootstrap and ClientChainGateway, or incompatibility with deployed contracts on Sepolia. Please check the logs for details.' - }) + path: ExocoreGateway.base.json + key: ExocoreGateway-base-layout-${{ github.event.pull_request.head.sha || github.event.after || github.sha }} + - name: Generate storage layout files for the PR + run: | + for file in Bootstrap ClientChainGateway RewardVault Vault ExocoreGateway ExoCapsule; do + forge inspect src/core/${file}.sol:${file} storage-layout > ${file}.compiled.json; + done + - name: Copy back base branch layout file + run: cp /tmp/ExocoreGateway.base.json ./ExocoreGateway.base.json + - name: Zip storage layout files + run: zip storage-layouts.zip *.compiled.json ExocoreGateway.base.json + - name: Upload storage layout files as an artifact + uses: actions/upload-artifact@v4 + with: + path: storage-layouts.zip + name: storage-layouts-${{ github.event.pull_request.head.sha || github.event.after || github.sha }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e1addf49..a67cde82 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,4 +1,4 @@ -name: Run `solhint` linter +name: Solhint on: merge_group: @@ -11,11 +11,10 @@ on: - "*" jobs: - check: + lint: strategy: fail-fast: true - name: Foundry project runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -23,11 +22,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v2 with: - # Node 22.5 has a bug - # https://github.com/nodejs/node/pull/53904 - # TODO: Once a version with the bug fix (above) is released, - # the version pin can be moved back to 22 - node-version: '22.4' + node-version: '22' - name: Clear npm cache run: npm cache clean --force @@ -38,16 +33,3 @@ jobs: - name: Run Solhint run: | npx solhint 'src/**/*.sol' -c ./src/.solhint.json - - - name: Add comment on failure - if: failure() - uses: actions/github-script@v6 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: 'Linting failed. Please check the logs.' - }) diff --git a/.github/workflows/foundry-setup.yml b/.github/workflows/reusable-foundry-setup.yml similarity index 82% rename from .github/workflows/foundry-setup.yml rename to .github/workflows/reusable-foundry-setup.yml index d4367318..36760729 100644 --- a/.github/workflows/foundry-setup.yml +++ b/.github/workflows/reusable-foundry-setup.yml @@ -10,13 +10,13 @@ on: outputs: installation-dir: description: "The installation directory of Foundry toolchain" - value: ${{ jobs.setup.outputs.installation-dir }} + value: ${{ jobs.install.outputs.installation-dir }} cache-key: description: "The cache key for Foundry toolchain" - value: ${{ jobs.setup.outputs.cache-key }} + value: ${{ jobs.install.outputs.cache-key }} jobs: - setup: + install: runs-on: ubuntu-latest outputs: cache-key: ${{ steps.set-cache-key.outputs.cache-key }} @@ -40,8 +40,8 @@ jobs: uses: actions/cache/save@v3 with: path: ${{ steps.find-path.outputs.installation-dir }} - key: ${{ runner.os }}-foundry-${{ inputs.foundry-version }} + key: foundry-${{ inputs.foundry-version }} - name: Set cache key id: set-cache-key run: | - echo "cache-key=${{ runner.os }}-foundry-${{ inputs.foundry-version }}" >> "$GITHUB_OUTPUT" + echo "cache-key=foundry-${{ inputs.foundry-version }}" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/status-comment.yml b/.github/workflows/status-comment.yml new file mode 100644 index 00000000..560bd78b --- /dev/null +++ b/.github/workflows/status-comment.yml @@ -0,0 +1,80 @@ +name: Comment CI status on PR + +on: + workflow_run: + workflows: + - "Forge CI" + - "Slither Analysis" + - "Solhint" + # Nested workflow_run is not supported, so this doesn't work. Instead + # that workflow should make comments by itself. + # - "Compare Storage Layouts" + types: + - completed + - requested + +permissions: + pull-requests: write + issues: read + +jobs: + comment_status: + runs-on: ubuntu-latest + steps: + # Log the workflow trigger details for debugging. + - name: Echo workflow trigger details + run: | + echo "Event action: ${{ github.event.action }}" + echo "Workflow run event: ${{ github.event.workflow_run.event }}" + echo "Workflow run conclusion: ${{ github.event.workflow_run.conclusion }}" + echo "Workflow run name: ${{ github.event.workflow_run.name }}" + echo "Workflow run URL: ${{ github.event.workflow_run.html_url }}" + echo "Commit SHA: ${{ github.event.workflow_run.head_commit.id }}" + echo "Workflow Run ID: ${{ github.event.workflow_run.id }}" + - name: Get PR number + id: pr-context + if: ${{ github.event.workflow_run.event == 'pull_request' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_TARGET_REPO: ${{ github.repository }} + PR_BRANCH: |- + ${{ + (github.event.workflow_run.head_repository.owner.login != github.event.workflow_run.repository.owner.login) + && format('{0}:{1}', github.event.workflow_run.head_repository.owner.login, github.event.workflow_run.head_branch) + || github.event.workflow_run.head_branch + }} + run: | + gh pr view --repo "${PR_TARGET_REPO}" "${PR_BRANCH}" \ + --json 'number' --jq '"number=\(.number)"' \ + >> "${GITHUB_OUTPUT}" + # Construct the message + - name: Set message + id: set-message + if: ${{ github.event.workflow_run.event == 'pull_request' }} + env: + WORKFLOW_NAME: ${{ github.event.workflow_run.name }} + WORKFLOW_URL: ${{ github.event.workflow_run.html_url }} + WORKFLOW_CONCLUSION: ${{ github.event.workflow_run.conclusion }} + SHA: ${{ github.event.workflow_run.head_commit.id }} + run: | + if [ "${{ github.event.action }}" == "requested" ]; then + message="🚀 The $WORKFLOW_NAME workflow has started." + elif [ "${{ github.event.workflow_run.conclusion }}" == "success" ]; then + message="✅ The $WORKFLOW_NAME workflow has completed successfully." + elif [ "${{ github.event.workflow_run.conclusion }}" == "failure" ]; then + message="❌ The $WORKFLOW_NAME workflow has failed!" + elif [ "${{ github.event.workflow_run.conclusion }}" == "cancelled" ]; then + message="⏚ī¸ The $WORKFLOW_NAME workflow was cancelled." + else + message="❓ The $WORKFLOW_NAME workflow has completed with an unknown status." + fi + echo "message=$message Check the [workflow run]($WORKFLOW_URL) for details. ($SHA)" >> "${GITHUB_OUTPUT}" + # Finally, post the status comment on the PR + - name: Update CI Status + uses: marocchino/sticky-pull-request-comment@v2 + if: ${{ github.event.workflow_run.event == 'pull_request' }} + with: + header: ${{ github.event.workflow_run.name }} + hide_details: true + number: ${{ steps.pr-context.outputs.number }} + message: ${{ steps.set-message.outputs.message }} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e66d06cc..732b39c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@openzeppelin/upgrades-core": "^1.40.0", "abbrev": "^1.0.9", "abstract-level": "^1.0.3", "acorn": "^8.11.2", @@ -1740,6 +1741,97 @@ "hardhat": "^2.0.4" } }, + "node_modules/@nomicfoundation/slang": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang/-/slang-0.17.0.tgz", + "integrity": "sha512-1GlkGRcGpVnjFw9Z1vvDKOKo2mzparFt7qrl2pDxWp+jrVtlvej98yCMX52pVyrYE7ZeOSZFnx/DtsSgoukStQ==", + "dependencies": { + "@nomicfoundation/slang-darwin-arm64": "0.17.0", + "@nomicfoundation/slang-darwin-x64": "0.17.0", + "@nomicfoundation/slang-linux-arm64-gnu": "0.17.0", + "@nomicfoundation/slang-linux-arm64-musl": "0.17.0", + "@nomicfoundation/slang-linux-x64-gnu": "0.17.0", + "@nomicfoundation/slang-linux-x64-musl": "0.17.0", + "@nomicfoundation/slang-win32-arm64-msvc": "0.17.0", + "@nomicfoundation/slang-win32-ia32-msvc": "0.17.0", + "@nomicfoundation/slang-win32-x64-msvc": "0.17.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nomicfoundation/slang-darwin-arm64": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-darwin-arm64/-/slang-darwin-arm64-0.17.0.tgz", + "integrity": "sha512-O0q94EUtoWy9A5kOTOa9/khtxXDYnLqmuda9pQELurSiwbQEVCPQL8kb34VbOW+ifdre66JM/05Xw9JWhIZ9sA==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nomicfoundation/slang-darwin-x64": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-darwin-x64/-/slang-darwin-x64-0.17.0.tgz", + "integrity": "sha512-IaDbHzvT08sBK2HyGzonWhq1uu8IxdjmTqAWHr25Oh/PYnamdi8u4qchZXXYKz/DHLoYN3vIpBXoqLQIomhD/g==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nomicfoundation/slang-linux-arm64-gnu": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-linux-arm64-gnu/-/slang-linux-arm64-gnu-0.17.0.tgz", + "integrity": "sha512-Lj4anvOsQZxs1SycG8VyT2Rl2oqIhyLSUCgGepTt3CiJ/bM+8r8bLJIgh8vKkki4BWz49YsYIgaJB2IPv8FFTw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nomicfoundation/slang-linux-arm64-musl": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-linux-arm64-musl/-/slang-linux-arm64-musl-0.17.0.tgz", + "integrity": "sha512-/xkTCa9d5SIWUBQE3BmLqDFfJRr4yUBwbl4ynPiGUpRXrD69cs6pWKkwjwz/FdBpXqVo36I+zY95qzoTj/YhOA==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nomicfoundation/slang-linux-x64-gnu": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-linux-x64-gnu/-/slang-linux-x64-gnu-0.17.0.tgz", + "integrity": "sha512-oe5IO5vntOqYvTd67deCHPIWuSuWm6aYtT2/0Kqz2/VLtGz4ClEulBSRwfnNzBVtw2nksWipE1w8BzhImI7Syg==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nomicfoundation/slang-linux-x64-musl": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-linux-x64-musl/-/slang-linux-x64-musl-0.17.0.tgz", + "integrity": "sha512-PpYCI5K/kgLAMXaPY0V4VST5gCDprEOh7z/47tbI8kJQumI5odjsj/Cs8MpTo7/uRH6flKYbVNgUzcocWVYrAQ==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nomicfoundation/slang-win32-arm64-msvc": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-win32-arm64-msvc/-/slang-win32-arm64-msvc-0.17.0.tgz", + "integrity": "sha512-u/Mkf7OjokdBilP7QOJj6QYJU4/mjkbKnTX21wLyCIzeVWS7yafRPYpBycKIBj2pRRZ6ceAY5EqRpb0aiCq+0Q==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nomicfoundation/slang-win32-ia32-msvc": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-win32-ia32-msvc/-/slang-win32-ia32-msvc-0.17.0.tgz", + "integrity": "sha512-XJBVQfNnZQUv0tP2JSJ573S+pmgrLWgqSZOGaMllnB/TL1gRci4Z7dYRJUF2s82GlRJE+FHSI2Ro6JISKmlXCg==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nomicfoundation/slang-win32-x64-msvc": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-win32-x64-msvc/-/slang-win32-x64-msvc-0.17.0.tgz", + "integrity": "sha512-zPGsAeiTfqfPNYHD8BfrahQmYzA78ZraoHKTGraq/1xwJwzBK4bu/NtvVA4pJjBV+B4L6DCxVhSbpn40q26JQA==", + "engines": { + "node": ">= 10" + } + }, "node_modules/@nomicfoundation/solidity-analyzer": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer/-/solidity-analyzer-0.1.1.tgz", @@ -1775,6 +1867,123 @@ "node": ">= 10" } }, + "node_modules/@openzeppelin/upgrades-core": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/@openzeppelin/upgrades-core/-/upgrades-core-1.40.0.tgz", + "integrity": "sha512-4bPSXdEqHsNRL5T1ybPLneWGYjzGl6XWGWkv7aUoFFgz8mOdarstRBX1Wi4XJFw6IeHPUI7mMSQr2jdz8Y2ypQ==", + "dependencies": { + "@nomicfoundation/slang": "^0.17.0", + "cbor": "^9.0.0", + "chalk": "^4.1.0", + "compare-versions": "^6.0.0", + "debug": "^4.1.1", + "ethereumjs-util": "^7.0.3", + "minimatch": "^9.0.5", + "minimist": "^1.2.7", + "proper-lockfile": "^4.1.1", + "solidity-ast": "^0.4.51" + }, + "bin": { + "openzeppelin-upgrades-core": "dist/cli/cli.js" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/cbor": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-9.0.2.tgz", + "integrity": "sha512-JPypkxsB10s9QOWwa6zwPzqE1Md3vqpPc+cai4sAecuCsRyAtAl/pMyhPlMbT/xtPnm2dznJZYRLui57qiRhaQ==", + "dependencies": { + "nofilter": "^3.1.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@pnpm/config.env-replace": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", @@ -3038,6 +3247,11 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-3.0.2.tgz", "integrity": "sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==" }, + "node_modules/compare-versions": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz", + "integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -5939,6 +6153,16 @@ "asap": "~2.0.6" } }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, "node_modules/proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", @@ -6208,6 +6432,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -6530,6 +6762,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -6834,6 +7071,11 @@ "node": ">=8" } }, + "node_modules/solidity-ast": { + "version": "0.4.59", + "resolved": "https://registry.npmjs.org/solidity-ast/-/solidity-ast-0.4.59.tgz", + "integrity": "sha512-I+CX0wrYUN9jDfYtcgWSe+OAowaXy8/1YQy7NS4ni5IBDmIYBq7ZzaP/7QqouLjzZapmQtvGLqCaYgoUWqBo5g==" + }, "node_modules/solidity-coverage": { "version": "0.8.5", "resolved": "https://registry.npmjs.org/solidity-coverage/-/solidity-coverage-0.8.5.tgz", diff --git a/package.json b/package.json index 87cd8749..d7c72afa 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "test": "test" }, "dependencies": { + "@openzeppelin/upgrades-core": "^1.40.0", "abbrev": "^1.0.9", "abstract-level": "^1.0.3", "acorn": "^8.11.2", diff --git a/script/compareLayouts.js b/script/compareLayouts.js new file mode 100644 index 00000000..9e0fab34 --- /dev/null +++ b/script/compareLayouts.js @@ -0,0 +1,47 @@ +const fs = require('fs'); +const { getStorageUpgradeReport } = require('@openzeppelin/upgrades-core/dist/storage'); + +// Mapping of deployed and compiled file names +const fileMappings = [ + { before: 'Bootstrap.deployed.json', after: 'Bootstrap.compiled.json' }, + { before: 'ClientChainGateway.deployed.json', after: 'ClientChainGateway.compiled.json' }, + { before: 'Vault.deployed.json', after: 'Vault.compiled.json' }, + { before: 'RewardVault.deployed.json', after: 'RewardVault.compiled.json' }, + { before: 'ExoCapsule.deployed.json', after: 'ExoCapsule.compiled.json' }, + { before: 'ExocoreGateway.base.json', after: 'ExocoreGateway.compiled.json' }, + { before: 'Bootstrap.compiled.json', after: 'ClientChainGateway.compiled.json' }, +]; + +// Loop through each mapping, load JSON files, and run the comparison +fileMappings.forEach(({ before, after }) => { + console.log(`🔍 Comparing ${before} and ${after}...`); + try { + // Check if both files exist and exit silently if not + if (!fs.existsSync(before)) { + console.log(`⚠ī¸ Skipping: ${before} does not exist.`); + return; + } + if (!fs.existsSync(after)) { + console.log(`⚠ī¸ Skipping: ${after} does not exist.`); + return; + } + // Load the JSON files + const deployedData = JSON.parse(fs.readFileSync(before, 'utf8')); + const compiledData = JSON.parse(fs.readFileSync(after, 'utf8')); + + // Run the storage upgrade comparison + const report = getStorageUpgradeReport(deployedData, compiledData, { unsafeAllowCustomTypes: true }); + + // Print the report if issues are found + if (!report.ok) { + console.log(`⚠ī¸ Issues found in ${before} and ${after}:`); + console.log(report.explain()); + process.exitCode = 1; + } else { + console.log(`✅ No issues detected between ${before} and ${after}.`); + } + } catch (error) { + console.error(`❌ Error processing ${before} or ${after}: ${error.message}`); + process.exitCode = 1; + } +}); diff --git a/src/storage/ClientChainGatewayStorage.sol b/src/storage/ClientChainGatewayStorage.sol index dc161ee2..47f0a235 100644 --- a/src/storage/ClientChainGatewayStorage.sol +++ b/src/storage/ClientChainGatewayStorage.sol @@ -41,7 +41,7 @@ contract ClientChainGatewayStorage is BootstrapStorage { IRewardVault public rewardVault; /// @dev Storage gap to allow for future upgrades. - uint256[40] private __gap; + uint256[39] private __gap; /* ----------------------------- restaking events ------------------------------ */ From f84c3a166b065cd682b98929ae00cf417e293b7f Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Wed, 27 Nov 2024 04:10:19 +0000 Subject: [PATCH 02/23] fix(ci): update status fast --- .github/workflows/compare-layouts.yml | 77 ++++++++++++++------------- 1 file changed, 41 insertions(+), 36 deletions(-) diff --git a/.github/workflows/compare-layouts.yml b/.github/workflows/compare-layouts.yml index 6d829718..efa64b11 100644 --- a/.github/workflows/compare-layouts.yml +++ b/.github/workflows/compare-layouts.yml @@ -12,9 +12,12 @@ permissions: pull-requests: write jobs: - # Do this job first so that the commit status is updated to pending ASAP. + # The cache storage in the reusable foundry setup takes far too long. + # Do this job first to update the commit status and comment ASAP. set-commit-status: runs-on: ubuntu-latest + outputs: + number: ${{ steps.pr-context.outputs.number }} steps: - name: Set commit status # trigger is no matter what, because the status should be updated @@ -34,6 +37,39 @@ jobs: "target_url": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" }' \ https://api.github.com/repos/${{ github.repository }}/statuses/${{ github.event.workflow_run.head_commit.id }} + - name: Get PR number + id: pr-context + if: ${{ github.event.workflow_run.event == 'pull_request' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_TARGET_REPO: ${{ github.repository }} + PR_BRANCH: |- + ${{ + (github.event.workflow_run.head_repository.owner.login != github.event.workflow_run.repository.owner.login) + && format('{0}:{1}', github.event.workflow_run.head_repository.owner.login, github.event.workflow_run.head_branch) + || github.event.workflow_run.head_branch + }} + run: | + gh pr view --repo "${PR_TARGET_REPO}" "${PR_BRANCH}" \ + --json 'number' --jq '"number=\(.number)"' \ + >> "${GITHUB_OUTPUT}" + - name: Set message + id: set-message + env: + WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} + WORKFLOW_NAME: ${{ github.workflow }} + SHA: ${{ github.event.workflow_run.head_commit.id }} + run: | + message="🚀 The $WORKFLOW_NAME workflow has started." + echo "message=$message Check the [workflow run]($WORKFLOW_URL) for progress. ($SHA)" >> "${GITHUB_OUTPUT}" + - name: Update CI Status + uses: marocchino/sticky-pull-request-comment@v2 + if: ${{ github.event.workflow_run.event == 'pull_request' }} + with: + header: ${{ github.workflow }} + hide_details: true + number: ${{ steps.pr-context.outputs.number }} + message: ${{ steps.set-message.outputs.message }} setup: # A full job can be used as a reusable workflow but not a step. @@ -46,7 +82,9 @@ jobs: # The actual job to compare the storage layouts. compare-storage-layouts: - needs: setup + needs: + - setup + - set-commit-status runs-on: ubuntu-latest env: @@ -70,39 +108,6 @@ jobs: - name: Checkout the repository if: ${{ github.event.workflow_run.conclusion == 'success' }} uses: actions/checkout@v4 - - name: Get PR number - id: pr-context - if: ${{ github.event.workflow_run.event == 'pull_request' }} - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_TARGET_REPO: ${{ github.repository }} - PR_BRANCH: |- - ${{ - (github.event.workflow_run.head_repository.owner.login != github.event.workflow_run.repository.owner.login) - && format('{0}:{1}', github.event.workflow_run.head_repository.owner.login, github.event.workflow_run.head_branch) - || github.event.workflow_run.head_branch - }} - run: | - gh pr view --repo "${PR_TARGET_REPO}" "${PR_BRANCH}" \ - --json 'number' --jq '"number=\(.number)"' \ - >> "${GITHUB_OUTPUT}" - - name: Set message - id: set-message - env: - WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} - WORKFLOW_NAME: ${{ github.workflow }} - SHA: ${{ github.event.workflow_run.head_commit.id }} - run: | - message="🚀 The $WORKFLOW_NAME workflow has started." - echo "message=$message Check the [workflow run]($WORKFLOW_URL) for progress. ($SHA)" >> "${GITHUB_OUTPUT}" - - name: Update CI Status - uses: marocchino/sticky-pull-request-comment@v2 - if: ${{ github.event.workflow_run.event == 'pull_request' }} - with: - header: ${{ github.workflow }} - hide_details: true - number: ${{ steps.pr-context.outputs.number }} - message: ${{ steps.set-message.outputs.message }} # The toolchain is needed to run `cast storage` - name: Restore cached Foundry toolchain if: ${{ github.event.workflow_run.conclusion == 'success' }} @@ -221,7 +226,7 @@ jobs: with: header: ${{ github.workflow }} hide_details: true - number: ${{ steps.pr-context.outputs.number }} + number: ${{ needs.set-commit-status.outputs.number }} message: ${{ steps.set-message-again.outputs.message }} - name: Exit with the correct code if: always() From 7d5a4b4e37335009cc427a9a126ec8ed708c89ac Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Wed, 27 Nov 2024 04:23:12 +0000 Subject: [PATCH 03/23] feat(ci): carve out storage layout CI Since the storage layout CI needs the API keys to run, it does not work in PRs originating from forked repos. To avoid those unnecessary errors, this PR splits the workflow into two parts: the first that generates the compiled layouts and the second that fetches the deployed layouts and compares them. Such a split is possible because the second type of workflow runs in a trusted context, and hence, the API key is accessible to it. --- .github/workflows/compare-layouts.yml | 240 +++++++++++++++++ .../compare_deployed_storage_layout.py | 87 ------- .github/workflows/compare_storage_layout.py | 81 ------ .github/workflows/forge-ci.yml | 162 +++++------- .github/workflows/lint.yml | 24 +- ...y-setup.yml => reusable-foundry-setup.yml} | 10 +- .github/workflows/status-comment.yml | 80 ++++++ package-lock.json | 242 ++++++++++++++++++ package.json | 1 + script/compareLayouts.js | 47 ++++ src/storage/ClientChainGatewayStorage.sol | 2 +- 11 files changed, 683 insertions(+), 293 deletions(-) create mode 100644 .github/workflows/compare-layouts.yml delete mode 100644 .github/workflows/compare_deployed_storage_layout.py delete mode 100755 .github/workflows/compare_storage_layout.py rename .github/workflows/{foundry-setup.yml => reusable-foundry-setup.yml} (82%) create mode 100644 .github/workflows/status-comment.yml create mode 100644 script/compareLayouts.js diff --git a/.github/workflows/compare-layouts.yml b/.github/workflows/compare-layouts.yml new file mode 100644 index 00000000..efa64b11 --- /dev/null +++ b/.github/workflows/compare-layouts.yml @@ -0,0 +1,240 @@ +name: Compare Storage Layouts + +on: + workflow_run: + workflows: ["Forge CI"] + types: + - completed + +permissions: + contents: read + statuses: write + pull-requests: write + +jobs: + # The cache storage in the reusable foundry setup takes far too long. + # Do this job first to update the commit status and comment ASAP. + set-commit-status: + runs-on: ubuntu-latest + outputs: + number: ${{ steps.pr-context.outputs.number }} + steps: + - name: Set commit status + # trigger is no matter what, because the status should be updated + if: always() + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # this step would have been better located in forge-ci.yml, but since it needs the secret + # it is placed here. first, it is updated to pending here and failure/success is updated later. + run: | + curl -X POST \ + -H "Authorization: token $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + -d '{ + "state": "pending", + "context": "Compare Storage Layouts", + "description": "In progress...", + "target_url": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + }' \ + https://api.github.com/repos/${{ github.repository }}/statuses/${{ github.event.workflow_run.head_commit.id }} + - name: Get PR number + id: pr-context + if: ${{ github.event.workflow_run.event == 'pull_request' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_TARGET_REPO: ${{ github.repository }} + PR_BRANCH: |- + ${{ + (github.event.workflow_run.head_repository.owner.login != github.event.workflow_run.repository.owner.login) + && format('{0}:{1}', github.event.workflow_run.head_repository.owner.login, github.event.workflow_run.head_branch) + || github.event.workflow_run.head_branch + }} + run: | + gh pr view --repo "${PR_TARGET_REPO}" "${PR_BRANCH}" \ + --json 'number' --jq '"number=\(.number)"' \ + >> "${GITHUB_OUTPUT}" + - name: Set message + id: set-message + env: + WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} + WORKFLOW_NAME: ${{ github.workflow }} + SHA: ${{ github.event.workflow_run.head_commit.id }} + run: | + message="🚀 The $WORKFLOW_NAME workflow has started." + echo "message=$message Check the [workflow run]($WORKFLOW_URL) for progress. ($SHA)" >> "${GITHUB_OUTPUT}" + - name: Update CI Status + uses: marocchino/sticky-pull-request-comment@v2 + if: ${{ github.event.workflow_run.event == 'pull_request' }} + with: + header: ${{ github.workflow }} + hide_details: true + number: ${{ steps.pr-context.outputs.number }} + message: ${{ steps.set-message.outputs.message }} + + setup: + # A full job can be used as a reusable workflow but not a step. + uses: ./.github/workflows/reusable-foundry-setup.yml + with: + # The below line does not accept environment variables, + # so it becomes the single source of truth for the version, within this workflow. + # Any `pinning` of the version should be done here and forge-ci.yml. + foundry-version: nightly + + # The actual job to compare the storage layouts. + compare-storage-layouts: + needs: + - setup + - set-commit-status + runs-on: ubuntu-latest + + env: + ALCHEMY_API_KEY: ${{ secrets.ALCHEMY_API_KEY }} + ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + steps: + # Log the workflow trigger details for debugging. + - name: Echo workflow trigger details + run: | + echo "Workflow run event: ${{ github.event.workflow_run.event }}" + echo "Workflow run conclusion: ${{ github.event.workflow_run.conclusion }}" + echo "Workflow run name: ${{ github.event.workflow_run.name }}" + echo "Workflow run URL: ${{ github.event.workflow_run.html_url }}" + echo "Commit SHA: ${{ github.event.workflow_run.head_commit.id }}" + echo "Workflow Run ID: ${{ github.event.workflow_run.id }}" + # The repository needs to be available for script/deployedContracts.json + # and script/compareLayouts.js. We do not restore the build data because + # the compiled layouts are restored from the artifact and not rebuilt. + - name: Checkout the repository + if: ${{ github.event.workflow_run.conclusion == 'success' }} + uses: actions/checkout@v4 + # The toolchain is needed to run `cast storage` + - name: Restore cached Foundry toolchain + if: ${{ github.event.workflow_run.conclusion == 'success' }} + uses: actions/cache/restore@v3 + with: + path: ${{ needs.setup.outputs.installation-dir }} + key: ${{ needs.setup.outputs.cache-key }} + - name: Add Foundry to PATH + if: ${{ github.event.workflow_run.conclusion == 'success' }} + run: echo "${{ needs.setup.outputs.installation-dir }}" >> $GITHUB_PATH + - name: Fetch the deployed layouts + if: ${{ github.event.workflow_run.conclusion == 'success' }} + run: | + set -e + data=$(cat script/deployedContracts.json) + bootstrap=$(echo "$data" | jq -r '.clientChain.bootstrapLogic // empty') + clientGateway=$(echo "$data" | jq -r '.clientChain.clientGatewayLogic // empty') + vault=$(echo "$data" | jq -r '.clientChain.vaultImplementation // empty') + rewardVault=$(echo "$data" | jq -r '.clientChain.rewardVaultImplementation // empty') + capsule=$(echo "$data" | jq -r '.clientChain.capsuleImplementation // empty') + pwd=$(pwd) + cd /tmp + # Create an array of contract names and addresses + declare -A contracts=( + ["Bootstrap"]="$bootstrap" + ["ClientChainGateway"]="$clientGateway" + ["Vault"]="$vault" + ["RewardVault"]="$rewardVault" + ["ExoCapsule"]="$capsule" + ) + # Iterate over the array and run `cast storage` for each contract + for contract in "${!contracts[@]}"; do + address=${contracts[$contract]} + if [[ -n $address ]]; then + echo "Processing $contract at address $address" + cast storage --json "$address" --rpc-url "https://eth-sepolia.g.alchemy.com/v2/$ALCHEMY_API_KEY" \ + --etherscan-api-key "$ETHERSCAN_API_KEY" > "$contract.deployed.json" + mv "$contract.deployed.json" "$pwd" + else + echo "Skipping $contract as no address is provided" + fi + done + cd "$pwd" + # Restore the layouts from the previous job + - name: Restore the layout files from the artifact + if: ${{ github.event.workflow_run.conclusion == 'success' }} + uses: dawidd6/action-download-artifact@v6 + with: + name: storage-layouts-${{ github.event.workflow_run.head_commit.id }} + run_id: ${{ github.event.workflow_run.id }} + - name: Extract the restored layouts + if: ${{ github.event.workflow_run.conclusion == 'success' }} + run: unzip storage-layouts.zip + - name: Set up Node.js + if: ${{ github.event.workflow_run.conclusion == 'success' }} + uses: actions/setup-node@v2 + with: + node-version: '18' + - name: Clear npm cache + if: ${{ github.event.workflow_run.conclusion == 'success' }} + run: npm cache clean --force + - name: Install the required dependency + if: ${{ github.event.workflow_run.conclusion == 'success' }} + run: npm install @openzeppelin/upgrades-core + - name: Compare the layouts + if: ${{ github.event.workflow_run.conclusion == 'success' }} + id: compare-layouts + run: | + node script/compareLayouts.js + # Even if this fails, the CI status should be updated. + continue-on-error: true + - name: Update parent commit status + if: always() + # if the outcome is not set, it will post failure + run: | + outcome=$( + [[ "${{ steps.compare-layouts.outcome }}" == "success" ]] && + echo "success" || + echo "failure" + ) + description=$( + [[ "${{ steps.compare-layouts.outcome }}" == "success" ]] && + echo "Storage layouts match" || + ([[ "${{ steps.compare-layouts.outcome }}" == "failure" ]] && + echo "Storage layouts do not match" || + echo "Job skipped since ${{ github.event.workflow_run.name }} failed.") + ) + curl -X POST \ + -H "Authorization: token $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + -d "{ + \"state\": \"${outcome}\", + \"context\": \"Compare Storage Layouts\", + \"description\": \"${description}\", + \"target_url\": \"https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}\" + }" \ + https://api.github.com/repos/${{ github.repository }}/statuses/${{ github.event.workflow_run.head_commit.id }} + - name: Set message again + id: set-message-again + env: + WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} + WORKFLOW_NAME: ${{ github.workflow }} + SHA: ${{ github.event.workflow_run.head_commit.id }} + run: | + if [ ${{ steps.compare-layouts.outcome }} == "success" ]; then + message="✅ The $WORKFLOW_NAME workflow has completed successfully." + elif [ ${{ steps.compare-layouts.outcome }} == "failure" ]; then + message="❌ The $WORKFLOW_NAME workflow has failed!" + else + message="⏭ The $WORKFLOW_NAME workflow was skipped." + fi + echo "message=$message Check the [workflow run]($WORKFLOW_URL) for details. ($SHA)" >> "${GITHUB_OUTPUT}" + - name: Update CI Status + uses: marocchino/sticky-pull-request-comment@v2 + if: ${{ github.event.workflow_run.event == 'pull_request' }} + with: + header: ${{ github.workflow }} + hide_details: true + number: ${{ needs.set-commit-status.outputs.number }} + message: ${{ steps.set-message-again.outputs.message }} + - name: Exit with the correct code + if: always() + # if the outcome is not set, it will exit 1. so, a failure in the parent job will + # result in a failure here. + run: | + if [[ "${{ steps.compare-layouts.outcome }}" == "success" ]]; then + exit 0 + else + exit 1 + fi diff --git a/.github/workflows/compare_deployed_storage_layout.py b/.github/workflows/compare_deployed_storage_layout.py deleted file mode 100644 index 3c3737aa..00000000 --- a/.github/workflows/compare_deployed_storage_layout.py +++ /dev/null @@ -1,87 +0,0 @@ -#!/usr/bin/env python - -import json -import subprocess -import pandas as pd -import os -from compare_storage_layout import parse_output, compare_layouts, get_current_layout - -def get_deployed_addresses(): - with open('script/deployedContracts.json', 'r') as f: - data = json.load(f) - return { - 'Bootstrap': data['clientChain'].get('bootstrapLogic'), - 'ClientChainGateway': data['clientChain'].get('clientGatewayLogic'), - 'Vault': data['clientChain'].get('vaultImplementation'), - 'RewardVault': data['clientChain'].get('rewardVaultImplementation'), - 'ExoCapsule': data['clientChain'].get('capsuleImplementation') - } - -def get_storage_layout(contract_name, address, rpc_url, etherscan_api_key): - if not address: - print(f"Skipping {contract_name} as it's not deployed.") - return pd.DataFrame() - - result = subprocess.run(['cast', 'storage', address, '--rpc-url', rpc_url, '--etherscan-api-key', etherscan_api_key], capture_output=True, text=True) - print(f"finish executing: cast storage {address} --rpc-url ...") - - if result.returncode != 0: - raise Exception(f"Error getting current layout for {contract_name}: {result.stderr}") - - return parse_output(contract_name, result.stdout.split('\n')) - -def load_and_parse_layout(contract_name, path): - with open(path, 'r') as f: - lines = f.readlines() - return parse_output(contract_name, lines) - -if __name__ == "__main__": - try: - api_key = os.getenv('ALCHEMY_API_KEY') - if not api_key: - raise ValueError("ALCHEMY_API_KEY environment variable is not set") - etherscan_api_key = os.getenv('ETHERSCAN_API_KEY') - if not etherscan_api_key: - raise ValueError("ETHERSCAN_API_KEY environment variable is not set") - - # Construct the RPC URL for Sepolia - rpc_url = f"https://eth-sepolia.g.alchemy.com/v2/{api_key}" - - addresses = get_deployed_addresses() - all_mismatches = {} - - for contract_name, address in addresses.items(): - print(f"Checking {contract_name}...") - deployed_layout = get_storage_layout(contract_name, address, rpc_url, etherscan_api_key) - if deployed_layout.empty: - print(f"No deployed layout found for {contract_name}.") - continue - - current_layout = get_current_layout(contract_name) - if current_layout.empty: - raise ValueError(f"Error: No valid entries of current layout found for {contract_name}.") - - mismatches = compare_layouts(deployed_layout, current_layout) - if mismatches: - all_mismatches[contract_name] = mismatches - - # then we load the layout file of ExocoreGateway on target branch and compare it with the current layout - print("Checking ExocoreGateway...") - target_branch_layout = load_and_parse_layout('ExocoreGateway', 'ExocoreGateway_target.txt') - current_layout = get_current_layout('ExocoreGateway') - mismatches = compare_layouts(target_branch_layout, current_layout) - if mismatches: - all_mismatches['ExocoreGateway'] = mismatches - - if all_mismatches: - print("Mismatches found for current contracts:") - for contract, mismatches in all_mismatches.items(): - print(f"{contract}:") - for mismatch in mismatches: - print(f" {mismatch}") - exit(1) - else: - print("Storage layout is compatible with all deployed contracts.") - except Exception as e: - print(f"Error: {e}") - exit(1) diff --git a/.github/workflows/compare_storage_layout.py b/.github/workflows/compare_storage_layout.py deleted file mode 100755 index 7ec6aee7..00000000 --- a/.github/workflows/compare_storage_layout.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python - -import pandas as pd -import subprocess - -def parse_output(contract_name, lines): - # Clean up the output and create a dataframe - data = [] - separator_line = len(lines) - for i, line in enumerate(lines): # start from the line next to the separator - if i > separator_line and line.startswith('|'): - parts = [part.strip() for part in line.split('|')[1:-1]] # Remove empty first and last elements - data.append(parts[:6]) # Keep Name, Type, Slot, Offset, Bytes, Contract - elif line.startswith('|') and 'Name' in line: - separator_line = i + 1 - - if not data: - raise Exception(f"No valid storage layout data found for {contract_name}") - - df = pd.DataFrame(data, columns=['Name', 'Type', 'Slot', 'Offset', 'Bytes', 'Contract']) - - # Convert numeric columns - for col in ['Slot', 'Offset', 'Bytes']: - df[col] = pd.to_numeric(df[col]) - - return df - -def get_current_layout(contract_name): - result = subprocess.run(['forge', 'inspect', f'src/core/{contract_name}.sol:{contract_name}', 'storage-layout', '--pretty'], capture_output=True, text=True) - print(f"finished executing forge inspect for {contract_name}") - - if result.returncode != 0: - raise Exception(f"Error getting current layout for {contract_name}: {result.stderr}") - - return parse_output(contract_name, result.stdout.split('\n')) - -def compare_layouts(old_layout, new_layout): - mismatches = [] - - # Ensure both dataframes have the same columns - columns = ['Name', 'Type', 'Slot', 'Offset', 'Bytes'] - old_layout = old_layout[columns].copy() - new_layout = new_layout[columns].copy() - - # Compare non-gap variables - for index, row in old_layout.iterrows(): - if row['Name'] != '__gap': - current_row = new_layout.loc[new_layout['Name'] == row['Name']] - if current_row.empty: - mismatches.append(f"Variable {row['Name']} is missing in the current layout") - elif not current_row.iloc[0].equals(row): - mismatches.append(f"Variable {row['Name']} has changed") - - if not mismatches: - print("No mismatches found") - - return mismatches - -if __name__ == "__main__": - try: - clientChainGateway_layout = get_current_layout("ClientChainGateway") - bootstrap_layout = get_current_layout("Bootstrap") - - if clientChainGateway_layout.empty: - raise ValueError("Error: No valid entries found for ClientChainGateway.") - - if bootstrap_layout.empty: - raise ValueError("Error: No valid entries found for Bootstrap.") - - mismatches = compare_layouts(bootstrap_layout, clientChainGateway_layout) - - if mismatches: - print(f"Mismatches found: {len(mismatches)}") - for mismatch in mismatches: - print(mismatch) - exit(1) - else: - print("All entries in Bootstrap match ClientChainGateway at the correct positions.") - except Exception as e: - print(f"Error: {e}") - exit(1) diff --git a/.github/workflows/forge-ci.yml b/.github/workflows/forge-ci.yml index a5e0e735..6d7d38ac 100644 --- a/.github/workflows/forge-ci.yml +++ b/.github/workflows/forge-ci.yml @@ -1,4 +1,4 @@ -name: Forge CI to build, test, format and compare storage layout +name: Forge CI on: merge_group: @@ -12,21 +12,25 @@ on: jobs: setup: - uses: ./.github/workflows/foundry-setup.yml + # A full job can be used as a reusable workflow but not a step. + uses: ./.github/workflows/reusable-foundry-setup.yml with: + # The below line does not accept environment variables, + # so it becomes the single source of truth for the version. foundry-version: nightly build: runs-on: ubuntu-latest needs: setup outputs: - installation-dir: ${{ needs.setup.outputs.installation-dir }} + # The cache-key only contains the version name. It is only used so that the name does not + # need to be repeated everywhere; instead setting the `foundry-version` above suffices. cache-key: ${{ needs.setup.outputs.cache-key }} + # Github's cache actions are a bit weird to deal with. It wouldn't let me restore the + # binaries to /usr/bin, so I restored them to the original location and added it to PATH. + # This output will let us carry it to other jobs. + installation-dir: ${{ needs.setup.outputs.installation-dir }} steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - submodules: recursive - name: Restore cached Foundry toolchain uses: actions/cache/restore@v3 with: @@ -34,20 +38,12 @@ jobs: key: ${{ needs.setup.outputs.cache-key }} - name: Add Foundry to PATH run: echo "${{ needs.setup.outputs.installation-dir }}" >> $GITHUB_PATH + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive - name: Build run: forge build - - name: Add comment for build failure - if: failure() - uses: actions/github-script@v6 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: 'The build has failed. Please check the logs.' - }) - name: Cache build artifacts uses: actions/cache/save@v3 with: @@ -56,14 +52,12 @@ jobs: ./out ./cache ./broadcast - key: ${{ runner.os }}-build-${{ github.sha }} + key: build-${{ github.event.pull_request.head.sha || github.event.after || github.sha }} test: runs-on: ubuntu-latest needs: build steps: - - name: Checkout repository - uses: actions/checkout@v4 - name: Restore cached Foundry toolchain uses: actions/cache/restore@v3 with: @@ -71,6 +65,8 @@ jobs: key: ${{ needs.build.outputs.cache-key }} - name: Add Foundry to PATH run: echo "${{ needs.build.outputs.installation-dir }}" >> $GITHUB_PATH + - name: Checkout repository + uses: actions/checkout@v4 - name: Restore build artifacts uses: actions/cache/restore@v3 with: @@ -79,30 +75,16 @@ jobs: ./out ./cache ./broadcast - key: ${{ runner.os }}-build-${{ github.sha }} + key: build-${{ github.event.pull_request.head.sha || github.event.after || github.sha }} - name: Test run: forge test -vvv - name: Set test snapshot as summary run: NO_COLOR=1 forge snapshot >> $GITHUB_STEP_SUMMARY - - name: Add comment for test failure - if: failure() - uses: actions/github-script@v6 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: 'The tests have failed. Please check the logs.' - }) format: runs-on: ubuntu-latest needs: build steps: - - name: Checkout repository - uses: actions/checkout@v4 - name: Restore cached Foundry toolchain uses: actions/cache/restore@v3 with: @@ -110,6 +92,8 @@ jobs: key: ${{ needs.build.outputs.cache-key }} - name: Add Foundry to PATH run: echo "${{ needs.build.outputs.installation-dir }}" >> $GITHUB_PATH + - name: Checkout repository + uses: actions/checkout@v4 - name: Restore build artifacts uses: actions/cache/restore@v3 with: @@ -118,31 +102,14 @@ jobs: ./out ./cache ./broadcast - key: ${{ runner.os }}-build-${{ github.sha }} + key: build-${{ github.event.pull_request.head.sha || github.event.after || github.sha }} - name: Check formatting run: forge fmt --check - - name: Add comment for format check failure - if: failure() - uses: actions/github-script@v6 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: 'The code is not formatted correctly. Please run `forge fmt` and push the changes.' - }) - compare-storage-layout: + extract-storage-layouts: runs-on: ubuntu-latest needs: build - env: - ALCHEMY_API_KEY: ${{ secrets.ALCHEMY_API_KEY }} - ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }} steps: - - name: Checkout repository - uses: actions/checkout@v4 - name: Restore cached Foundry toolchain uses: actions/cache/restore@v3 with: @@ -150,56 +117,55 @@ jobs: key: ${{ needs.build.outputs.cache-key }} - name: Add Foundry to PATH run: echo "${{ needs.build.outputs.installation-dir }}" >> $GITHUB_PATH - - name: Restore build artifacts - uses: actions/cache/restore@v3 - with: - path: | - ./lib - ./out - ./cache - ./broadcast - key: ${{ runner.os }}-build-${{ github.sha }} - - name: Checkout target branch + - name: Checkout base branch or previous commit uses: actions/checkout@v4 with: - ref: ${{ github.event.pull_request.base.ref }} + ref: ${{ github.event.pull_request.base.ref || github.event.before }} + # We don't have a `lib` folder to restore for this step, so we + # recursively checkout the submodules. In other steps, we use the + # `lib` folder from the `build` job. submodules: recursive - - name: Generate target branch layout files + - name: Generate base branch layout file + # Note that this `run` will do a `forge build` so we don't need to do it ourselves. + # The build artifacts of this step are not relevant to us either, so we don't need to + # cache them. run: | - forge inspect src/core/ExocoreGateway.sol:ExocoreGateway storage-layout --pretty > ExocoreGateway_target.txt - - name: Cache target branch layout file - uses: actions/cache/save@v3 - with: - path: ExocoreGateway_target.txt - key: ${{ runner.os }}-exocore-target-${{ github.sha }} + forge inspect src/core/ExocoreGateway.sol:ExocoreGateway storage-layout > ExocoreGateway.base.json + - name: Copy base branch layout file + run: cp ExocoreGateway.base.json /tmp/ + # Now we can generate the layout for the PR level. - name: Checkout back to PR uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} - - name: Restore target branch layout file + # Restoring these will help make `forge inspect` faster. + - name: Restore build artifacts uses: actions/cache/restore@v3 with: - path: ExocoreGateway_target.txt - key: ${{ runner.os }}-exocore-target-${{ github.sha }} - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.12.4' - - name: Install pandas - run: pip install --root-user-action=ignore pandas==2.2.2 - - name: Run the comparison script for Bootstrap and ClientChainGateway - run: python .github/workflows/compare_storage_layout.py - - name: Run the comparison script for deployed contracts - run: python .github/workflows/compare_deployed_storage_layout.py - - name: Add comment for storage layout mismatch failure - if: failure() - uses: actions/github-script@v6 + path: | + ./lib + ./out + ./cache + ./broadcast + key: build-${{ github.event.pull_request.head.sha || github.event.after || github.sha }} + # This step restores the base branch layout file, while the next step generates the PR + # branch layout files. + - name: Restore base branch layout file + uses: actions/cache/restore@v3 with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: 'Storage layout compatibility check failed. This could be due to a mismatch between Bootstrap and ClientChainGateway, or incompatibility with deployed contracts on Sepolia. Please check the logs for details.' - }) + path: ExocoreGateway.base.json + key: ExocoreGateway-base-layout-${{ github.event.pull_request.head.sha || github.event.after || github.sha }} + - name: Generate storage layout files for the PR + run: | + for file in Bootstrap ClientChainGateway RewardVault Vault ExocoreGateway ExoCapsule; do + forge inspect src/core/${file}.sol:${file} storage-layout > ${file}.compiled.json; + done + - name: Copy back base branch layout file + run: cp /tmp/ExocoreGateway.base.json ./ExocoreGateway.base.json + - name: Zip storage layout files + run: zip storage-layouts.zip *.compiled.json ExocoreGateway.base.json + - name: Upload storage layout files as an artifact + uses: actions/upload-artifact@v4 + with: + path: storage-layouts.zip + name: storage-layouts-${{ github.event.pull_request.head.sha || github.event.after || github.sha }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e1addf49..a67cde82 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,4 +1,4 @@ -name: Run `solhint` linter +name: Solhint on: merge_group: @@ -11,11 +11,10 @@ on: - "*" jobs: - check: + lint: strategy: fail-fast: true - name: Foundry project runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -23,11 +22,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v2 with: - # Node 22.5 has a bug - # https://github.com/nodejs/node/pull/53904 - # TODO: Once a version with the bug fix (above) is released, - # the version pin can be moved back to 22 - node-version: '22.4' + node-version: '22' - name: Clear npm cache run: npm cache clean --force @@ -38,16 +33,3 @@ jobs: - name: Run Solhint run: | npx solhint 'src/**/*.sol' -c ./src/.solhint.json - - - name: Add comment on failure - if: failure() - uses: actions/github-script@v6 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: 'Linting failed. Please check the logs.' - }) diff --git a/.github/workflows/foundry-setup.yml b/.github/workflows/reusable-foundry-setup.yml similarity index 82% rename from .github/workflows/foundry-setup.yml rename to .github/workflows/reusable-foundry-setup.yml index d4367318..36760729 100644 --- a/.github/workflows/foundry-setup.yml +++ b/.github/workflows/reusable-foundry-setup.yml @@ -10,13 +10,13 @@ on: outputs: installation-dir: description: "The installation directory of Foundry toolchain" - value: ${{ jobs.setup.outputs.installation-dir }} + value: ${{ jobs.install.outputs.installation-dir }} cache-key: description: "The cache key for Foundry toolchain" - value: ${{ jobs.setup.outputs.cache-key }} + value: ${{ jobs.install.outputs.cache-key }} jobs: - setup: + install: runs-on: ubuntu-latest outputs: cache-key: ${{ steps.set-cache-key.outputs.cache-key }} @@ -40,8 +40,8 @@ jobs: uses: actions/cache/save@v3 with: path: ${{ steps.find-path.outputs.installation-dir }} - key: ${{ runner.os }}-foundry-${{ inputs.foundry-version }} + key: foundry-${{ inputs.foundry-version }} - name: Set cache key id: set-cache-key run: | - echo "cache-key=${{ runner.os }}-foundry-${{ inputs.foundry-version }}" >> "$GITHUB_OUTPUT" + echo "cache-key=foundry-${{ inputs.foundry-version }}" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/status-comment.yml b/.github/workflows/status-comment.yml new file mode 100644 index 00000000..560bd78b --- /dev/null +++ b/.github/workflows/status-comment.yml @@ -0,0 +1,80 @@ +name: Comment CI status on PR + +on: + workflow_run: + workflows: + - "Forge CI" + - "Slither Analysis" + - "Solhint" + # Nested workflow_run is not supported, so this doesn't work. Instead + # that workflow should make comments by itself. + # - "Compare Storage Layouts" + types: + - completed + - requested + +permissions: + pull-requests: write + issues: read + +jobs: + comment_status: + runs-on: ubuntu-latest + steps: + # Log the workflow trigger details for debugging. + - name: Echo workflow trigger details + run: | + echo "Event action: ${{ github.event.action }}" + echo "Workflow run event: ${{ github.event.workflow_run.event }}" + echo "Workflow run conclusion: ${{ github.event.workflow_run.conclusion }}" + echo "Workflow run name: ${{ github.event.workflow_run.name }}" + echo "Workflow run URL: ${{ github.event.workflow_run.html_url }}" + echo "Commit SHA: ${{ github.event.workflow_run.head_commit.id }}" + echo "Workflow Run ID: ${{ github.event.workflow_run.id }}" + - name: Get PR number + id: pr-context + if: ${{ github.event.workflow_run.event == 'pull_request' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_TARGET_REPO: ${{ github.repository }} + PR_BRANCH: |- + ${{ + (github.event.workflow_run.head_repository.owner.login != github.event.workflow_run.repository.owner.login) + && format('{0}:{1}', github.event.workflow_run.head_repository.owner.login, github.event.workflow_run.head_branch) + || github.event.workflow_run.head_branch + }} + run: | + gh pr view --repo "${PR_TARGET_REPO}" "${PR_BRANCH}" \ + --json 'number' --jq '"number=\(.number)"' \ + >> "${GITHUB_OUTPUT}" + # Construct the message + - name: Set message + id: set-message + if: ${{ github.event.workflow_run.event == 'pull_request' }} + env: + WORKFLOW_NAME: ${{ github.event.workflow_run.name }} + WORKFLOW_URL: ${{ github.event.workflow_run.html_url }} + WORKFLOW_CONCLUSION: ${{ github.event.workflow_run.conclusion }} + SHA: ${{ github.event.workflow_run.head_commit.id }} + run: | + if [ "${{ github.event.action }}" == "requested" ]; then + message="🚀 The $WORKFLOW_NAME workflow has started." + elif [ "${{ github.event.workflow_run.conclusion }}" == "success" ]; then + message="✅ The $WORKFLOW_NAME workflow has completed successfully." + elif [ "${{ github.event.workflow_run.conclusion }}" == "failure" ]; then + message="❌ The $WORKFLOW_NAME workflow has failed!" + elif [ "${{ github.event.workflow_run.conclusion }}" == "cancelled" ]; then + message="⏚ī¸ The $WORKFLOW_NAME workflow was cancelled." + else + message="❓ The $WORKFLOW_NAME workflow has completed with an unknown status." + fi + echo "message=$message Check the [workflow run]($WORKFLOW_URL) for details. ($SHA)" >> "${GITHUB_OUTPUT}" + # Finally, post the status comment on the PR + - name: Update CI Status + uses: marocchino/sticky-pull-request-comment@v2 + if: ${{ github.event.workflow_run.event == 'pull_request' }} + with: + header: ${{ github.event.workflow_run.name }} + hide_details: true + number: ${{ steps.pr-context.outputs.number }} + message: ${{ steps.set-message.outputs.message }} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e66d06cc..732b39c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@openzeppelin/upgrades-core": "^1.40.0", "abbrev": "^1.0.9", "abstract-level": "^1.0.3", "acorn": "^8.11.2", @@ -1740,6 +1741,97 @@ "hardhat": "^2.0.4" } }, + "node_modules/@nomicfoundation/slang": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang/-/slang-0.17.0.tgz", + "integrity": "sha512-1GlkGRcGpVnjFw9Z1vvDKOKo2mzparFt7qrl2pDxWp+jrVtlvej98yCMX52pVyrYE7ZeOSZFnx/DtsSgoukStQ==", + "dependencies": { + "@nomicfoundation/slang-darwin-arm64": "0.17.0", + "@nomicfoundation/slang-darwin-x64": "0.17.0", + "@nomicfoundation/slang-linux-arm64-gnu": "0.17.0", + "@nomicfoundation/slang-linux-arm64-musl": "0.17.0", + "@nomicfoundation/slang-linux-x64-gnu": "0.17.0", + "@nomicfoundation/slang-linux-x64-musl": "0.17.0", + "@nomicfoundation/slang-win32-arm64-msvc": "0.17.0", + "@nomicfoundation/slang-win32-ia32-msvc": "0.17.0", + "@nomicfoundation/slang-win32-x64-msvc": "0.17.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nomicfoundation/slang-darwin-arm64": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-darwin-arm64/-/slang-darwin-arm64-0.17.0.tgz", + "integrity": "sha512-O0q94EUtoWy9A5kOTOa9/khtxXDYnLqmuda9pQELurSiwbQEVCPQL8kb34VbOW+ifdre66JM/05Xw9JWhIZ9sA==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nomicfoundation/slang-darwin-x64": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-darwin-x64/-/slang-darwin-x64-0.17.0.tgz", + "integrity": "sha512-IaDbHzvT08sBK2HyGzonWhq1uu8IxdjmTqAWHr25Oh/PYnamdi8u4qchZXXYKz/DHLoYN3vIpBXoqLQIomhD/g==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nomicfoundation/slang-linux-arm64-gnu": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-linux-arm64-gnu/-/slang-linux-arm64-gnu-0.17.0.tgz", + "integrity": "sha512-Lj4anvOsQZxs1SycG8VyT2Rl2oqIhyLSUCgGepTt3CiJ/bM+8r8bLJIgh8vKkki4BWz49YsYIgaJB2IPv8FFTw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nomicfoundation/slang-linux-arm64-musl": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-linux-arm64-musl/-/slang-linux-arm64-musl-0.17.0.tgz", + "integrity": "sha512-/xkTCa9d5SIWUBQE3BmLqDFfJRr4yUBwbl4ynPiGUpRXrD69cs6pWKkwjwz/FdBpXqVo36I+zY95qzoTj/YhOA==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nomicfoundation/slang-linux-x64-gnu": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-linux-x64-gnu/-/slang-linux-x64-gnu-0.17.0.tgz", + "integrity": "sha512-oe5IO5vntOqYvTd67deCHPIWuSuWm6aYtT2/0Kqz2/VLtGz4ClEulBSRwfnNzBVtw2nksWipE1w8BzhImI7Syg==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nomicfoundation/slang-linux-x64-musl": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-linux-x64-musl/-/slang-linux-x64-musl-0.17.0.tgz", + "integrity": "sha512-PpYCI5K/kgLAMXaPY0V4VST5gCDprEOh7z/47tbI8kJQumI5odjsj/Cs8MpTo7/uRH6flKYbVNgUzcocWVYrAQ==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nomicfoundation/slang-win32-arm64-msvc": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-win32-arm64-msvc/-/slang-win32-arm64-msvc-0.17.0.tgz", + "integrity": "sha512-u/Mkf7OjokdBilP7QOJj6QYJU4/mjkbKnTX21wLyCIzeVWS7yafRPYpBycKIBj2pRRZ6ceAY5EqRpb0aiCq+0Q==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nomicfoundation/slang-win32-ia32-msvc": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-win32-ia32-msvc/-/slang-win32-ia32-msvc-0.17.0.tgz", + "integrity": "sha512-XJBVQfNnZQUv0tP2JSJ573S+pmgrLWgqSZOGaMllnB/TL1gRci4Z7dYRJUF2s82GlRJE+FHSI2Ro6JISKmlXCg==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nomicfoundation/slang-win32-x64-msvc": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-win32-x64-msvc/-/slang-win32-x64-msvc-0.17.0.tgz", + "integrity": "sha512-zPGsAeiTfqfPNYHD8BfrahQmYzA78ZraoHKTGraq/1xwJwzBK4bu/NtvVA4pJjBV+B4L6DCxVhSbpn40q26JQA==", + "engines": { + "node": ">= 10" + } + }, "node_modules/@nomicfoundation/solidity-analyzer": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer/-/solidity-analyzer-0.1.1.tgz", @@ -1775,6 +1867,123 @@ "node": ">= 10" } }, + "node_modules/@openzeppelin/upgrades-core": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/@openzeppelin/upgrades-core/-/upgrades-core-1.40.0.tgz", + "integrity": "sha512-4bPSXdEqHsNRL5T1ybPLneWGYjzGl6XWGWkv7aUoFFgz8mOdarstRBX1Wi4XJFw6IeHPUI7mMSQr2jdz8Y2ypQ==", + "dependencies": { + "@nomicfoundation/slang": "^0.17.0", + "cbor": "^9.0.0", + "chalk": "^4.1.0", + "compare-versions": "^6.0.0", + "debug": "^4.1.1", + "ethereumjs-util": "^7.0.3", + "minimatch": "^9.0.5", + "minimist": "^1.2.7", + "proper-lockfile": "^4.1.1", + "solidity-ast": "^0.4.51" + }, + "bin": { + "openzeppelin-upgrades-core": "dist/cli/cli.js" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/cbor": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-9.0.2.tgz", + "integrity": "sha512-JPypkxsB10s9QOWwa6zwPzqE1Md3vqpPc+cai4sAecuCsRyAtAl/pMyhPlMbT/xtPnm2dznJZYRLui57qiRhaQ==", + "dependencies": { + "nofilter": "^3.1.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@pnpm/config.env-replace": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", @@ -3038,6 +3247,11 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-3.0.2.tgz", "integrity": "sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==" }, + "node_modules/compare-versions": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz", + "integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -5939,6 +6153,16 @@ "asap": "~2.0.6" } }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, "node_modules/proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", @@ -6208,6 +6432,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -6530,6 +6762,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -6834,6 +7071,11 @@ "node": ">=8" } }, + "node_modules/solidity-ast": { + "version": "0.4.59", + "resolved": "https://registry.npmjs.org/solidity-ast/-/solidity-ast-0.4.59.tgz", + "integrity": "sha512-I+CX0wrYUN9jDfYtcgWSe+OAowaXy8/1YQy7NS4ni5IBDmIYBq7ZzaP/7QqouLjzZapmQtvGLqCaYgoUWqBo5g==" + }, "node_modules/solidity-coverage": { "version": "0.8.5", "resolved": "https://registry.npmjs.org/solidity-coverage/-/solidity-coverage-0.8.5.tgz", diff --git a/package.json b/package.json index 87cd8749..d7c72afa 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "test": "test" }, "dependencies": { + "@openzeppelin/upgrades-core": "^1.40.0", "abbrev": "^1.0.9", "abstract-level": "^1.0.3", "acorn": "^8.11.2", diff --git a/script/compareLayouts.js b/script/compareLayouts.js new file mode 100644 index 00000000..9e0fab34 --- /dev/null +++ b/script/compareLayouts.js @@ -0,0 +1,47 @@ +const fs = require('fs'); +const { getStorageUpgradeReport } = require('@openzeppelin/upgrades-core/dist/storage'); + +// Mapping of deployed and compiled file names +const fileMappings = [ + { before: 'Bootstrap.deployed.json', after: 'Bootstrap.compiled.json' }, + { before: 'ClientChainGateway.deployed.json', after: 'ClientChainGateway.compiled.json' }, + { before: 'Vault.deployed.json', after: 'Vault.compiled.json' }, + { before: 'RewardVault.deployed.json', after: 'RewardVault.compiled.json' }, + { before: 'ExoCapsule.deployed.json', after: 'ExoCapsule.compiled.json' }, + { before: 'ExocoreGateway.base.json', after: 'ExocoreGateway.compiled.json' }, + { before: 'Bootstrap.compiled.json', after: 'ClientChainGateway.compiled.json' }, +]; + +// Loop through each mapping, load JSON files, and run the comparison +fileMappings.forEach(({ before, after }) => { + console.log(`🔍 Comparing ${before} and ${after}...`); + try { + // Check if both files exist and exit silently if not + if (!fs.existsSync(before)) { + console.log(`⚠ī¸ Skipping: ${before} does not exist.`); + return; + } + if (!fs.existsSync(after)) { + console.log(`⚠ī¸ Skipping: ${after} does not exist.`); + return; + } + // Load the JSON files + const deployedData = JSON.parse(fs.readFileSync(before, 'utf8')); + const compiledData = JSON.parse(fs.readFileSync(after, 'utf8')); + + // Run the storage upgrade comparison + const report = getStorageUpgradeReport(deployedData, compiledData, { unsafeAllowCustomTypes: true }); + + // Print the report if issues are found + if (!report.ok) { + console.log(`⚠ī¸ Issues found in ${before} and ${after}:`); + console.log(report.explain()); + process.exitCode = 1; + } else { + console.log(`✅ No issues detected between ${before} and ${after}.`); + } + } catch (error) { + console.error(`❌ Error processing ${before} or ${after}: ${error.message}`); + process.exitCode = 1; + } +}); diff --git a/src/storage/ClientChainGatewayStorage.sol b/src/storage/ClientChainGatewayStorage.sol index dc161ee2..47f0a235 100644 --- a/src/storage/ClientChainGatewayStorage.sol +++ b/src/storage/ClientChainGatewayStorage.sol @@ -41,7 +41,7 @@ contract ClientChainGatewayStorage is BootstrapStorage { IRewardVault public rewardVault; /// @dev Storage gap to allow for future upgrades. - uint256[40] private __gap; + uint256[39] private __gap; /* ----------------------------- restaking events ------------------------------ */ From c05fca072d074a4645366ec5d1357e83695c6905 Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Wed, 27 Nov 2024 05:03:41 +0000 Subject: [PATCH 04/23] respond to AI comments --- .github/workflows/compare-layouts.yml | 14 +++++++++----- .github/workflows/forge-ci.yml | 10 +++++----- .github/workflows/status-comment.yml | 12 ++++++++---- script/compareLayouts.js | 2 +- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/.github/workflows/compare-layouts.yml b/.github/workflows/compare-layouts.yml index efa64b11..3f2f5d02 100644 --- a/.github/workflows/compare-layouts.yml +++ b/.github/workflows/compare-layouts.yml @@ -50,9 +50,13 @@ jobs: || github.event.workflow_run.head_branch }} run: | - gh pr view --repo "${PR_TARGET_REPO}" "${PR_BRANCH}" \ - --json 'number' --jq '"number=\(.number)"' \ - >> "${GITHUB_OUTPUT}" + pr_number=$(gh pr view --repo "${PR_TARGET_REPO}" "${PR_BRANCH}" \ + --json 'number' --jq '.number') + if [ -z "$pr_number" ]; then + echo "Error: PR number not found for branch '${PR_BRANCH}' in repository '${PR_TARGET_REPO}'" >&2 + exit 1 + fi + echo "number=$pr_number" >> "${GITHUB_OUTPUT}" - name: Set message id: set-message env: @@ -117,7 +121,7 @@ jobs: key: ${{ needs.setup.outputs.cache-key }} - name: Add Foundry to PATH if: ${{ github.event.workflow_run.conclusion == 'success' }} - run: echo "${{ needs.setup.outputs.installation-dir }}" >> $GITHUB_PATH + run: echo "${{ needs.setup.outputs.installation-dir }}" >> "$GITHUB_PATH" - name: Fetch the deployed layouts if: ${{ github.event.workflow_run.conclusion == 'success' }} run: | @@ -163,7 +167,7 @@ jobs: run: unzip storage-layouts.zip - name: Set up Node.js if: ${{ github.event.workflow_run.conclusion == 'success' }} - uses: actions/setup-node@v2 + uses: actions/setup-node@v4 with: node-version: '18' - name: Clear npm cache diff --git a/.github/workflows/forge-ci.yml b/.github/workflows/forge-ci.yml index 6d7d38ac..cd5c4930 100644 --- a/.github/workflows/forge-ci.yml +++ b/.github/workflows/forge-ci.yml @@ -37,7 +37,7 @@ jobs: path: ${{ needs.setup.outputs.installation-dir }} key: ${{ needs.setup.outputs.cache-key }} - name: Add Foundry to PATH - run: echo "${{ needs.setup.outputs.installation-dir }}" >> $GITHUB_PATH + run: echo "${{ needs.setup.outputs.installation-dir }}" >> "$GITHUB_PATH" - name: Checkout repository uses: actions/checkout@v4 with: @@ -64,7 +64,7 @@ jobs: path: ${{ needs.build.outputs.installation-dir }} key: ${{ needs.build.outputs.cache-key }} - name: Add Foundry to PATH - run: echo "${{ needs.build.outputs.installation-dir }}" >> $GITHUB_PATH + run: echo "${{ needs.build.outputs.installation-dir }}" >> "$GITHUB_PATH" - name: Checkout repository uses: actions/checkout@v4 - name: Restore build artifacts @@ -91,7 +91,7 @@ jobs: path: ${{ needs.build.outputs.installation-dir }} key: ${{ needs.build.outputs.cache-key }} - name: Add Foundry to PATH - run: echo "${{ needs.build.outputs.installation-dir }}" >> $GITHUB_PATH + run: echo "${{ needs.build.outputs.installation-dir }}" >> "$GITHUB_PATH" - name: Checkout repository uses: actions/checkout@v4 - name: Restore build artifacts @@ -116,7 +116,7 @@ jobs: path: ${{ needs.build.outputs.installation-dir }} key: ${{ needs.build.outputs.cache-key }} - name: Add Foundry to PATH - run: echo "${{ needs.build.outputs.installation-dir }}" >> $GITHUB_PATH + run: echo "${{ needs.build.outputs.installation-dir }}" >> "$GITHUB_PATH" - name: Checkout base branch or previous commit uses: actions/checkout@v4 with: @@ -163,7 +163,7 @@ jobs: - name: Copy back base branch layout file run: cp /tmp/ExocoreGateway.base.json ./ExocoreGateway.base.json - name: Zip storage layout files - run: zip storage-layouts.zip *.compiled.json ExocoreGateway.base.json + run: zip "storage-layouts.zip" "./*.compiled.json" "./ExocoreGateway.base.json" - name: Upload storage layout files as an artifact uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/status-comment.yml b/.github/workflows/status-comment.yml index 560bd78b..aae79c98 100644 --- a/.github/workflows/status-comment.yml +++ b/.github/workflows/status-comment.yml @@ -44,9 +44,13 @@ jobs: || github.event.workflow_run.head_branch }} run: | - gh pr view --repo "${PR_TARGET_REPO}" "${PR_BRANCH}" \ - --json 'number' --jq '"number=\(.number)"' \ - >> "${GITHUB_OUTPUT}" + pr_number=$(gh pr view --repo "${PR_TARGET_REPO}" "${PR_BRANCH}" \ + --json 'number' --jq '.number') + if [ -z "$pr_number" ]; then + echo "Error: PR number not found for branch '${PR_BRANCH}' in repository '${PR_TARGET_REPO}'" >&2 + exit 1 + fi + echo "number=$pr_number" >> "${GITHUB_OUTPUT}" # Construct the message - name: Set message id: set-message @@ -77,4 +81,4 @@ jobs: header: ${{ github.event.workflow_run.name }} hide_details: true number: ${{ steps.pr-context.outputs.number }} - message: ${{ steps.set-message.outputs.message }} \ No newline at end of file + message: ${{ steps.set-message.outputs.message }} diff --git a/script/compareLayouts.js b/script/compareLayouts.js index 9e0fab34..cc9595dc 100644 --- a/script/compareLayouts.js +++ b/script/compareLayouts.js @@ -1,5 +1,5 @@ const fs = require('fs'); -const { getStorageUpgradeReport } = require('@openzeppelin/upgrades-core/dist/storage'); +const { getStorageUpgradeReport } = require('@openzeppelin/upgrades-core'); // Mapping of deployed and compiled file names const fileMappings = [ From dd628dbd098bbf2566b9f4f4b52807e483f371b9 Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Wed, 27 Nov 2024 05:20:41 +0000 Subject: [PATCH 05/23] respond to more AI comments --- .github/workflows/compare-layouts.yml | 64 ++++++++++++--------------- .github/workflows/forge-ci.yml | 9 +--- 2 files changed, 30 insertions(+), 43 deletions(-) diff --git a/.github/workflows/compare-layouts.yml b/.github/workflows/compare-layouts.yml index 3f2f5d02..b23dd223 100644 --- a/.github/workflows/compare-layouts.yml +++ b/.github/workflows/compare-layouts.yml @@ -23,20 +23,17 @@ jobs: # trigger is no matter what, because the status should be updated if: always() env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # this step would have been better located in forge-ci.yml, but since it needs the secret # it is placed here. first, it is updated to pending here and failure/success is updated later. run: | - curl -X POST \ - -H "Authorization: token $GITHUB_TOKEN" \ - -H "Accept: application/vnd.github+json" \ - -d '{ - "state": "pending", - "context": "Compare Storage Layouts", - "description": "In progress...", - "target_url": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" - }' \ - https://api.github.com/repos/${{ github.repository }}/statuses/${{ github.event.workflow_run.head_commit.id }} + gh api \ + --method POST \ + /repos/${{ github.repository }}/statuses/${{ github.event.workflow_run.head_commit.id }} \ + -f state=pending \ + -f context='${{ github.workflow.name }}' \ + -f description='In progress...' \ + -f target_url="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" - name: Get PR number id: pr-context if: ${{ github.event.workflow_run.event == 'pull_request' }} @@ -94,7 +91,6 @@ jobs: env: ALCHEMY_API_KEY: ${{ secrets.ALCHEMY_API_KEY }} ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: # Log the workflow trigger details for debugging. @@ -143,11 +139,12 @@ jobs: ["ExoCapsule"]="$capsule" ) # Iterate over the array and run `cast storage` for each contract + RPC_URL="https://eth-sepolia.g.alchemy.com/v2/$ALCHEMY_API_KEY" for contract in "${!contracts[@]}"; do address=${contracts[$contract]} if [[ -n $address ]]; then echo "Processing $contract at address $address" - cast storage --json "$address" --rpc-url "https://eth-sepolia.g.alchemy.com/v2/$ALCHEMY_API_KEY" \ + cast storage --json "$address" --rpc-url "$RPC_URL" \ --etherscan-api-key "$ETHERSCAN_API_KEY" > "$contract.deployed.json" mv "$contract.deployed.json" "$pwd" else @@ -185,30 +182,27 @@ jobs: continue-on-error: true - name: Update parent commit status if: always() + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # if the outcome is not set, it will post failure run: | - outcome=$( - [[ "${{ steps.compare-layouts.outcome }}" == "success" ]] && - echo "success" || - echo "failure" - ) - description=$( - [[ "${{ steps.compare-layouts.outcome }}" == "success" ]] && - echo "Storage layouts match" || - ([[ "${{ steps.compare-layouts.outcome }}" == "failure" ]] && - echo "Storage layouts do not match" || - echo "Job skipped since ${{ github.event.workflow_run.name }} failed.") - ) - curl -X POST \ - -H "Authorization: token $GITHUB_TOKEN" \ - -H "Accept: application/vnd.github+json" \ - -d "{ - \"state\": \"${outcome}\", - \"context\": \"Compare Storage Layouts\", - \"description\": \"${description}\", - \"target_url\": \"https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}\" - }" \ - https://api.github.com/repos/${{ github.repository }}/statuses/${{ github.event.workflow_run.head_commit.id }} + if [[ "${{ steps.compare-layouts.outcome }}" == "success" ]]; then + outcome="success" + description="Storage layouts match" + elif [[ "${{ steps.compare-layouts.outcome }}" == "failure" ]]; then + outcome="failure" + description="Storage layouts do not match" + else + outcome="failure" + description="Job skipped since ${{ github.event.workflow_run.name }} failed." + fi + gh api \ + --method POST \ + /repos/${{ github.repository }}/statuses/${{ github.event.workflow_run.head_commit.id }} \ + -f state="$outcome" \ + -f context='${{ github.workflow.name }}' \ + -f description="$description" \ + -f target_url="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" - name: Set message again id: set-message-again env: diff --git a/.github/workflows/forge-ci.yml b/.github/workflows/forge-ci.yml index cd5c4930..26486116 100644 --- a/.github/workflows/forge-ci.yml +++ b/.github/workflows/forge-ci.yml @@ -148,13 +148,6 @@ jobs: ./cache ./broadcast key: build-${{ github.event.pull_request.head.sha || github.event.after || github.sha }} - # This step restores the base branch layout file, while the next step generates the PR - # branch layout files. - - name: Restore base branch layout file - uses: actions/cache/restore@v3 - with: - path: ExocoreGateway.base.json - key: ExocoreGateway-base-layout-${{ github.event.pull_request.head.sha || github.event.after || github.sha }} - name: Generate storage layout files for the PR run: | for file in Bootstrap ClientChainGateway RewardVault Vault ExocoreGateway ExoCapsule; do @@ -163,7 +156,7 @@ jobs: - name: Copy back base branch layout file run: cp /tmp/ExocoreGateway.base.json ./ExocoreGateway.base.json - name: Zip storage layout files - run: zip "storage-layouts.zip" "./*.compiled.json" "./ExocoreGateway.base.json" + run: zip storage-layouts.zip ./*.compiled.json ./ExocoreGateway.base.json - name: Upload storage layout files as an artifact uses: actions/upload-artifact@v4 with: From 1ff0aa25303f22970d58b34de1101a603cf46d3d Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Wed, 27 Nov 2024 05:29:37 +0000 Subject: [PATCH 06/23] error out if expected files do not exist --- script/compareLayouts.js | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/script/compareLayouts.js b/script/compareLayouts.js index cc9595dc..11c57891 100644 --- a/script/compareLayouts.js +++ b/script/compareLayouts.js @@ -3,28 +3,33 @@ const { getStorageUpgradeReport } = require('@openzeppelin/upgrades-core'); // Mapping of deployed and compiled file names const fileMappings = [ - { before: 'Bootstrap.deployed.json', after: 'Bootstrap.compiled.json' }, - { before: 'ClientChainGateway.deployed.json', after: 'ClientChainGateway.compiled.json' }, - { before: 'Vault.deployed.json', after: 'Vault.compiled.json' }, - { before: 'RewardVault.deployed.json', after: 'RewardVault.compiled.json' }, - { before: 'ExoCapsule.deployed.json', after: 'ExoCapsule.compiled.json' }, - { before: 'ExocoreGateway.base.json', after: 'ExocoreGateway.compiled.json' }, - { before: 'Bootstrap.compiled.json', after: 'ClientChainGateway.compiled.json' }, + { before: 'Bootstrap.deployed.json', after: 'Bootstrap.compiled.json', mustExist: true }, + { before: 'ClientChainGateway.deployed.json', after: 'ClientChainGateway.compiled.json', mustExist: true }, + { before: 'Vault.deployed.json', after: 'Vault.compiled.json', mustExist: true }, + // TODO: once RewardVault is deployed, change mustExist to true + { before: 'RewardVault.deployed.json', after: 'RewardVault.compiled.json', mustExist: false }, + { before: 'ExoCapsule.deployed.json', after: 'ExoCapsule.compiled.json', mustExist: true }, + { before: 'ExocoreGateway.base.json', after: 'ExocoreGateway.compiled.json', mustExist: true }, + { before: 'Bootstrap.compiled.json', after: 'ClientChainGateway.compiled.json', mustExist: true }, ]; // Loop through each mapping, load JSON files, and run the comparison -fileMappings.forEach(({ before, after }) => { +fileMappings.forEach(({ before, after, mustExist }) => { console.log(`🔍 Comparing ${before} and ${after}...`); + try { - // Check if both files exist and exit silently if not - if (!fs.existsSync(before)) { - console.log(`⚠ī¸ Skipping: ${before} does not exist.`); - return; - } - if (!fs.existsSync(after)) { - console.log(`⚠ī¸ Skipping: ${after} does not exist.`); + // Ensure files exist + const beforeExists = fs.existsSync(before); + const afterExists = fs.existsSync(after); + + if (!beforeExists || !afterExists) { + if (mustExist) { + throw new Error(`❌ Required file(s) missing: ${beforeExists ? '' : before} ${afterExists ? '' : after}`); + } + console.log(`⚠ī¸ Skipping: Missing file(s): ${beforeExists ? '' : before} ${afterExists ? '' : after}`); return; } + // Load the JSON files const deployedData = JSON.parse(fs.readFileSync(before, 'utf8')); const compiledData = JSON.parse(fs.readFileSync(after, 'utf8')); @@ -41,7 +46,7 @@ fileMappings.forEach(({ before, after }) => { console.log(`✅ No issues detected between ${before} and ${after}.`); } } catch (error) { - console.error(`❌ Error processing ${before} or ${after}: ${error.message}`); + console.error(`❌ Error processing ${before} and ${after}: ${error.message}`); process.exitCode = 1; } }); From 3b788034165f938aec2445f66e8662b7de59cbee Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Wed, 27 Nov 2024 05:36:38 +0000 Subject: [PATCH 07/23] fix(ci): escape the params --- .github/workflows/compare-layouts.yml | 10 +++++----- .github/workflows/status-comment.yml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/compare-layouts.yml b/.github/workflows/compare-layouts.yml index b23dd223..b580293f 100644 --- a/.github/workflows/compare-layouts.yml +++ b/.github/workflows/compare-layouts.yml @@ -31,8 +31,8 @@ jobs: --method POST \ /repos/${{ github.repository }}/statuses/${{ github.event.workflow_run.head_commit.id }} \ -f state=pending \ - -f context='${{ github.workflow.name }}' \ - -f description='In progress...' \ + -f context="${{ github.workflow }}" \ + -f description="In progress..." \ -f target_url="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" - name: Get PR number id: pr-context @@ -63,7 +63,7 @@ jobs: run: | message="🚀 The $WORKFLOW_NAME workflow has started." echo "message=$message Check the [workflow run]($WORKFLOW_URL) for progress. ($SHA)" >> "${GITHUB_OUTPUT}" - - name: Update CI Status + - name: Comment CI Status uses: marocchino/sticky-pull-request-comment@v2 if: ${{ github.event.workflow_run.event == 'pull_request' }} with: @@ -200,7 +200,7 @@ jobs: --method POST \ /repos/${{ github.repository }}/statuses/${{ github.event.workflow_run.head_commit.id }} \ -f state="$outcome" \ - -f context='${{ github.workflow.name }}' \ + -f context="${{ github.workflow }}" \ -f description="$description" \ -f target_url="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" - name: Set message again @@ -218,7 +218,7 @@ jobs: message="⏭ The $WORKFLOW_NAME workflow was skipped." fi echo "message=$message Check the [workflow run]($WORKFLOW_URL) for details. ($SHA)" >> "${GITHUB_OUTPUT}" - - name: Update CI Status + - name: Comment CI Status uses: marocchino/sticky-pull-request-comment@v2 if: ${{ github.event.workflow_run.event == 'pull_request' }} with: diff --git a/.github/workflows/status-comment.yml b/.github/workflows/status-comment.yml index aae79c98..ecfbf9f5 100644 --- a/.github/workflows/status-comment.yml +++ b/.github/workflows/status-comment.yml @@ -74,7 +74,7 @@ jobs: fi echo "message=$message Check the [workflow run]($WORKFLOW_URL) for details. ($SHA)" >> "${GITHUB_OUTPUT}" # Finally, post the status comment on the PR - - name: Update CI Status + - name: Comment parent CI Status uses: marocchino/sticky-pull-request-comment@v2 if: ${{ github.event.workflow_run.event == 'pull_request' }} with: From 0510384585e142341691b8ccc0180206dd3e031f Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Wed, 27 Nov 2024 05:51:55 +0000 Subject: [PATCH 08/23] fix(ci): fail faster if the parent job failed --- .github/workflows/compare-layouts.yml | 2 ++ .github/workflows/reusable-foundry-setup.yml | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/.github/workflows/compare-layouts.yml b/.github/workflows/compare-layouts.yml index b580293f..b8572e27 100644 --- a/.github/workflows/compare-layouts.yml +++ b/.github/workflows/compare-layouts.yml @@ -80,6 +80,8 @@ jobs: # so it becomes the single source of truth for the version, within this workflow. # Any `pinning` of the version should be done here and forge-ci.yml. foundry-version: nightly + # Skip the setup job if the parent job failed. + do: ${{ github.event.workflow_run.conclusion == 'success' }} # The actual job to compare the storage layouts. compare-storage-layouts: diff --git a/.github/workflows/reusable-foundry-setup.yml b/.github/workflows/reusable-foundry-setup.yml index 36760729..3b7da4bb 100644 --- a/.github/workflows/reusable-foundry-setup.yml +++ b/.github/workflows/reusable-foundry-setup.yml @@ -7,6 +7,10 @@ on: foundry-version: required: true type: string + do: + required: false + type: boolean + default: true outputs: installation-dir: description: "The installation directory of Foundry toolchain" @@ -26,8 +30,10 @@ jobs: uses: foundry-rs/foundry-toolchain@v1 with: version: ${{ inputs.foundry-version }} + if: ${{ inputs.do }} - name: Print forge version run: forge --version + if: ${{ inputs.do }} # Unfortunately, the `foundry-toolchain` action installs it in a # randomly generated location, so we must determine it ourselves - name: Determine Foundry installation path @@ -36,11 +42,15 @@ jobs: installation_path=$(which forge) installation_dir=$(dirname $installation_path) echo "installation-dir=$installation_dir" >> "$GITHUB_OUTPUT" + if: ${{ inputs.do }} - name: Cache the Foundry toolchain uses: actions/cache/save@v3 with: path: ${{ steps.find-path.outputs.installation-dir }} key: foundry-${{ inputs.foundry-version }} + if: ${{ inputs.do }} + # No if in the below step to avoid the notification that goes + # out when the action is skipped - name: Set cache key id: set-cache-key run: | From b0e4314b1e3d04be5cf0363c5ad8f9f4e3b3b606 Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Wed, 27 Nov 2024 06:00:35 +0000 Subject: [PATCH 09/23] rename input for clarity --- .github/workflows/compare-layouts.yml | 2 +- .github/workflows/reusable-foundry-setup.yml | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/compare-layouts.yml b/.github/workflows/compare-layouts.yml index b8572e27..1162c7fe 100644 --- a/.github/workflows/compare-layouts.yml +++ b/.github/workflows/compare-layouts.yml @@ -81,7 +81,7 @@ jobs: # Any `pinning` of the version should be done here and forge-ci.yml. foundry-version: nightly # Skip the setup job if the parent job failed. - do: ${{ github.event.workflow_run.conclusion == 'success' }} + skip-install: ${{ github.event.workflow_run.conclusion != 'success' }} # The actual job to compare the storage layouts. compare-storage-layouts: diff --git a/.github/workflows/reusable-foundry-setup.yml b/.github/workflows/reusable-foundry-setup.yml index 3b7da4bb..6bb133e3 100644 --- a/.github/workflows/reusable-foundry-setup.yml +++ b/.github/workflows/reusable-foundry-setup.yml @@ -6,11 +6,13 @@ on: inputs: foundry-version: required: true + description: "The version of Foundry to install" type: string - do: + skip-install: required: false + description: "Skip the installation but set the cache key. Useful to avoid installation and the extremely time consuming caching but still run this job to avoid notifications." type: boolean - default: true + default: false outputs: installation-dir: description: "The installation directory of Foundry toolchain" @@ -30,10 +32,10 @@ jobs: uses: foundry-rs/foundry-toolchain@v1 with: version: ${{ inputs.foundry-version }} - if: ${{ inputs.do }} + if: ${{ inputs.skip-install == false }} - name: Print forge version run: forge --version - if: ${{ inputs.do }} + if: ${{ inputs.skip-install == false }} # Unfortunately, the `foundry-toolchain` action installs it in a # randomly generated location, so we must determine it ourselves - name: Determine Foundry installation path @@ -42,13 +44,13 @@ jobs: installation_path=$(which forge) installation_dir=$(dirname $installation_path) echo "installation-dir=$installation_dir" >> "$GITHUB_OUTPUT" - if: ${{ inputs.do }} + if: ${{ inputs.skip-install == false }} - name: Cache the Foundry toolchain uses: actions/cache/save@v3 with: path: ${{ steps.find-path.outputs.installation-dir }} key: foundry-${{ inputs.foundry-version }} - if: ${{ inputs.do }} + if: ${{ inputs.skip-install == false }} # No if in the below step to avoid the notification that goes # out when the action is skipped - name: Set cache key From 8e26760c1c3eeaad053ce783ac6d081abcc7363f Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Wed, 27 Nov 2024 06:02:41 +0000 Subject: [PATCH 10/23] do not set the key unless required --- .github/workflows/reusable-foundry-setup.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/reusable-foundry-setup.yml b/.github/workflows/reusable-foundry-setup.yml index 6bb133e3..c48244e8 100644 --- a/.github/workflows/reusable-foundry-setup.yml +++ b/.github/workflows/reusable-foundry-setup.yml @@ -10,7 +10,7 @@ on: type: string skip-install: required: false - description: "Skip the installation but set the cache key. Useful to avoid installation and the extremely time consuming caching but still run this job to avoid notifications." + description: "Skip the installation. Useful to avoid installation and the extremely time consuming caching but still run this job to avoid notifications." type: boolean default: false outputs: @@ -28,6 +28,9 @@ jobs: cache-key: ${{ steps.set-cache-key.outputs.cache-key }} installation-dir: ${{ steps.find-path.outputs.installation-dir }} steps: + - name: Echo skipping status + if: ${{ inputs.skip-install == true }} + run: echo "Skipping Foundry installation" - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: @@ -57,3 +60,4 @@ jobs: id: set-cache-key run: | echo "cache-key=foundry-${{ inputs.foundry-version }}" >> "$GITHUB_OUTPUT" + if: ${{ inputs.skip-install == false }} From 65a46565fd9621f65bd3b06cd542e2abee0c9a6c Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Wed, 27 Nov 2024 06:05:09 +0000 Subject: [PATCH 11/23] use not condition instead of == false --- .github/workflows/reusable-foundry-setup.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/reusable-foundry-setup.yml b/.github/workflows/reusable-foundry-setup.yml index c48244e8..0a80fcf3 100644 --- a/.github/workflows/reusable-foundry-setup.yml +++ b/.github/workflows/reusable-foundry-setup.yml @@ -29,16 +29,16 @@ jobs: installation-dir: ${{ steps.find-path.outputs.installation-dir }} steps: - name: Echo skipping status - if: ${{ inputs.skip-install == true }} + if: ${{ inputs.skip-install }} run: echo "Skipping Foundry installation" - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: version: ${{ inputs.foundry-version }} - if: ${{ inputs.skip-install == false }} + if: ${{ !inputs.skip-install }} - name: Print forge version run: forge --version - if: ${{ inputs.skip-install == false }} + if: ${{ !inputs.skip-install }} # Unfortunately, the `foundry-toolchain` action installs it in a # randomly generated location, so we must determine it ourselves - name: Determine Foundry installation path @@ -47,17 +47,17 @@ jobs: installation_path=$(which forge) installation_dir=$(dirname $installation_path) echo "installation-dir=$installation_dir" >> "$GITHUB_OUTPUT" - if: ${{ inputs.skip-install == false }} + if: ${{ !inputs.skip-install }} - name: Cache the Foundry toolchain uses: actions/cache/save@v3 with: path: ${{ steps.find-path.outputs.installation-dir }} key: foundry-${{ inputs.foundry-version }} - if: ${{ inputs.skip-install == false }} + if: ${{ !inputs.skip-install }} # No if in the below step to avoid the notification that goes # out when the action is skipped - name: Set cache key id: set-cache-key run: | echo "cache-key=foundry-${{ inputs.foundry-version }}" >> "$GITHUB_OUTPUT" - if: ${{ inputs.skip-install == false }} + if: ${{ !inputs.skip-install }} From 1b9c58743f9e834750b8d568b288a8e70fdc9019 Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Wed, 27 Nov 2024 09:05:26 +0000 Subject: [PATCH 12/23] remove superfluous comment --- .github/workflows/reusable-foundry-setup.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/reusable-foundry-setup.yml b/.github/workflows/reusable-foundry-setup.yml index 0a80fcf3..c4ea8fd0 100644 --- a/.github/workflows/reusable-foundry-setup.yml +++ b/.github/workflows/reusable-foundry-setup.yml @@ -54,8 +54,6 @@ jobs: path: ${{ steps.find-path.outputs.installation-dir }} key: foundry-${{ inputs.foundry-version }} if: ${{ !inputs.skip-install }} - # No if in the below step to avoid the notification that goes - # out when the action is skipped - name: Set cache key id: set-cache-key run: | From aadac30b457063d5c55b0055349b15f30742c9dc Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Wed, 27 Nov 2024 10:00:15 +0000 Subject: [PATCH 13/23] fix(ci): add timeout --- .github/workflows/compare-layouts.yml | 5 +++++ .github/workflows/forge-ci.yml | 8 ++++++++ .github/workflows/lint.yml | 2 ++ .github/workflows/reusable-foundry-setup.yml | 2 ++ .github/workflows/slither.yml | 2 ++ .github/workflows/status-comment.yml | 2 ++ 6 files changed, 21 insertions(+) diff --git a/.github/workflows/compare-layouts.yml b/.github/workflows/compare-layouts.yml index 1162c7fe..555b844d 100644 --- a/.github/workflows/compare-layouts.yml +++ b/.github/workflows/compare-layouts.yml @@ -15,6 +15,8 @@ jobs: # The cache storage in the reusable foundry setup takes far too long. # Do this job first to update the commit status and comment ASAP. set-commit-status: + # Typically takes no more than 30s + timeout-minutes: 10 runs-on: ubuntu-latest outputs: number: ${{ steps.pr-context.outputs.number }} @@ -85,6 +87,8 @@ jobs: # The actual job to compare the storage layouts. compare-storage-layouts: + # Typically takes no more than 7 minutes + timeout-minutes: 30 needs: - setup - set-commit-status @@ -206,6 +210,7 @@ jobs: -f description="$description" \ -f target_url="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" - name: Set message again + # Even though the job is different, specify a unique ID. id: set-message-again env: WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} diff --git a/.github/workflows/forge-ci.yml b/.github/workflows/forge-ci.yml index 26486116..e0010ef6 100644 --- a/.github/workflows/forge-ci.yml +++ b/.github/workflows/forge-ci.yml @@ -22,6 +22,8 @@ jobs: build: runs-on: ubuntu-latest needs: setup + # Takes about 3 minutes + timeout-minutes: 15 outputs: # The cache-key only contains the version name. It is only used so that the name does not # need to be repeated everywhere; instead setting the `foundry-version` above suffices. @@ -57,6 +59,8 @@ jobs: test: runs-on: ubuntu-latest needs: build + # No more than a few minutes + timeout-minutes: 5 steps: - name: Restore cached Foundry toolchain uses: actions/cache/restore@v3 @@ -84,6 +88,8 @@ jobs: format: runs-on: ubuntu-latest needs: build + # No more than a few minutes + timeout-minutes: 5 steps: - name: Restore cached Foundry toolchain uses: actions/cache/restore@v3 @@ -109,6 +115,8 @@ jobs: extract-storage-layouts: runs-on: ubuntu-latest needs: build + # A few minutes + timeout-minutes: 10 steps: - name: Restore cached Foundry toolchain uses: actions/cache/restore@v3 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index a67cde82..21e1d23c 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -14,6 +14,8 @@ jobs: lint: strategy: fail-fast: true + # No more than a few minutes + timeout-minutes: 5 runs-on: ubuntu-latest steps: diff --git a/.github/workflows/reusable-foundry-setup.yml b/.github/workflows/reusable-foundry-setup.yml index c4ea8fd0..a0284c8f 100644 --- a/.github/workflows/reusable-foundry-setup.yml +++ b/.github/workflows/reusable-foundry-setup.yml @@ -23,6 +23,8 @@ on: jobs: install: + # Caching is slow, takes about 3 minutes total + timeout-minutes: 15 runs-on: ubuntu-latest outputs: cache-key: ${{ steps.set-cache-key.outputs.cache-key }} diff --git a/.github/workflows/slither.yml b/.github/workflows/slither.yml index 15a096a3..2ecc8b37 100644 --- a/.github/workflows/slither.yml +++ b/.github/workflows/slither.yml @@ -12,6 +12,8 @@ on: jobs: analyze: + # Takes about 2 minutes + timeout-minutes: 10 runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/status-comment.yml b/.github/workflows/status-comment.yml index ecfbf9f5..dd41b927 100644 --- a/.github/workflows/status-comment.yml +++ b/.github/workflows/status-comment.yml @@ -20,6 +20,8 @@ permissions: jobs: comment_status: runs-on: ubuntu-latest + # Typically takes no more than 30s + timeout-minutes: 5 steps: # Log the workflow trigger details for debugging. - name: Echo workflow trigger details From 081a143f68ee5a2d7d7774885f376cc0b3784058 Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Wed, 27 Nov 2024 16:45:22 +0000 Subject: [PATCH 14/23] feat(forge-ci): parallelize layout extraction --- .github/workflows/forge-ci.yml | 46 +++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/.github/workflows/forge-ci.yml b/.github/workflows/forge-ci.yml index e0010ef6..52887f2f 100644 --- a/.github/workflows/forge-ci.yml +++ b/.github/workflows/forge-ci.yml @@ -112,7 +112,7 @@ jobs: - name: Check formatting run: forge fmt --check - extract-storage-layouts: + extract-base-storage-layout-exocore-gateway: runs-on: ubuntu-latest needs: build # A few minutes @@ -139,14 +139,30 @@ jobs: # cache them. run: | forge inspect src/core/ExocoreGateway.sol:ExocoreGateway storage-layout > ExocoreGateway.base.json - - name: Copy base branch layout file - run: cp ExocoreGateway.base.json /tmp/ - # Now we can generate the layout for the PR level. - - name: Checkout back to PR - uses: actions/checkout@v4 + - name: Upload storage layout file as an artifact + uses: actions/upload-artifact@v4 with: - ref: ${{ github.event.pull_request.head.sha }} - # Restoring these will help make `forge inspect` faster. + path: ExocoreGateway.base.json + name: storage-layouts-${{ github.event.pull_request.base.sha || github.event.after || github.sha }} + + extract-storage-layouts: + runs-on: ubuntu-latest + needs: build + strategy: + matrix: + contract: [Bootstrap, ClientChainGateway, RewardVault, Vault, ExocoreGateway, ExoCapsule] + # A few minutes + timeout-minutes: 10 + steps: + - name: Restore cached Foundry toolchain + uses: actions/cache/restore@v3 + with: + path: ${{ needs.build.outputs.installation-dir }} + key: ${{ needs.build.outputs.cache-key }} + - name: Add Foundry to PATH + run: echo "${{ needs.build.outputs.installation-dir }}" >> "$GITHUB_PATH" + - name: Checkout repository + uses: actions/checkout@v4 - name: Restore build artifacts uses: actions/cache/restore@v3 with: @@ -156,17 +172,11 @@ jobs: ./cache ./broadcast key: build-${{ github.event.pull_request.head.sha || github.event.after || github.sha }} - - name: Generate storage layout files for the PR + - name: Generate storage layout file for ${{ matrix.contract }} run: | - for file in Bootstrap ClientChainGateway RewardVault Vault ExocoreGateway ExoCapsule; do - forge inspect src/core/${file}.sol:${file} storage-layout > ${file}.compiled.json; - done - - name: Copy back base branch layout file - run: cp /tmp/ExocoreGateway.base.json ./ExocoreGateway.base.json - - name: Zip storage layout files - run: zip storage-layouts.zip ./*.compiled.json ./ExocoreGateway.base.json - - name: Upload storage layout files as an artifact + forge inspect src/core/${{ matrix.contract }}.sol:${{ matrix.contract }} storage-layout > ${{ matrix.contract }}.compiled.json; + - name: Upload storage layout file as an artifact uses: actions/upload-artifact@v4 with: - path: storage-layouts.zip + path: ${{ matrix.contract }}.compiled.json name: storage-layouts-${{ github.event.pull_request.head.sha || github.event.after || github.sha }} From 56c71cd8f5020160a4801fff5822866249815565 Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Wed, 27 Nov 2024 16:57:37 +0000 Subject: [PATCH 15/23] fix(ci): use artifacts with different names --- .github/workflows/forge-ci.yml | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/.github/workflows/forge-ci.yml b/.github/workflows/forge-ci.yml index 52887f2f..595b8e13 100644 --- a/.github/workflows/forge-ci.yml +++ b/.github/workflows/forge-ci.yml @@ -143,7 +143,7 @@ jobs: uses: actions/upload-artifact@v4 with: path: ExocoreGateway.base.json - name: storage-layouts-${{ github.event.pull_request.base.sha || github.event.after || github.sha }} + name: storage-layouts-ExocoreGateway-base-${{ github.event.pull_request.base.sha || github.event.after || github.sha }} extract-storage-layouts: runs-on: ubuntu-latest @@ -179,4 +179,23 @@ jobs: uses: actions/upload-artifact@v4 with: path: ${{ matrix.contract }}.compiled.json - name: storage-layouts-${{ github.event.pull_request.head.sha || github.event.after || github.sha }} + name: storage-layouts-${{ matrix.contract}}-${{ github.event.pull_request.head.sha || github.event.after || github.sha }} + + combine-storage-layouts: + runs-on: ubuntu-latest + needs: + - extract-base-storage-layout-exocore-gateway + - extract-storage-layouts + # A few minutes + timeout-minutes: 10 + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + # No name and no path means all artifacts are downloaded and saved to $GITHUB_WORKSPACE + - name: Zip up the storage layouts + run: zip storage-layouts.zip *.json + - name: Upload storage layout file as an artifact + uses: actions/upload-artifact@v4 + with: + path: storage-layouts.zip + name: storage-layouts-${{ github.event.pull_request.head.sha || github.event.after || github.sha }} \ No newline at end of file From df267d61710e5b7976697f739d5937fb0aca2625 Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Wed, 27 Nov 2024 17:09:36 +0000 Subject: [PATCH 16/23] fix(ci): flatten combination correctly --- .github/workflows/forge-ci.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/forge-ci.yml b/.github/workflows/forge-ci.yml index 595b8e13..d929eaaa 100644 --- a/.github/workflows/forge-ci.yml +++ b/.github/workflows/forge-ci.yml @@ -191,9 +191,12 @@ jobs: steps: - name: Download artifacts uses: actions/download-artifact@v4 - # No name and no path means all artifacts are downloaded and saved to $GITHUB_WORKSPACE + # No name means all artifacts are downloaded within their respective subfolders + # inside the provided path. + with: + path: combined - name: Zip up the storage layouts - run: zip storage-layouts.zip *.json + run: zip -j storage-layouts.zip combined/*/*.json - name: Upload storage layout file as an artifact uses: actions/upload-artifact@v4 with: From 8bf4f1c5357884200aac18c054d262b4e6969c3e Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Wed, 27 Nov 2024 17:39:56 +0000 Subject: [PATCH 17/23] split deployed layout job to parallel --- .github/workflows/compare-layouts.yml | 181 +++++++++++++++++--------- .github/workflows/forge-ci.yml | 14 +- 2 files changed, 125 insertions(+), 70 deletions(-) diff --git a/.github/workflows/compare-layouts.yml b/.github/workflows/compare-layouts.yml index 555b844d..85974619 100644 --- a/.github/workflows/compare-layouts.yml +++ b/.github/workflows/compare-layouts.yml @@ -21,6 +21,15 @@ jobs: outputs: number: ${{ steps.pr-context.outputs.number }} steps: + # Log the workflow trigger details for debugging. + - name: Echo workflow trigger details + run: | + echo "Workflow run event: ${{ github.event.workflow_run.event }}" + echo "Workflow run conclusion: ${{ github.event.workflow_run.conclusion }}" + echo "Workflow run name: ${{ github.event.workflow_run.name }}" + echo "Workflow run URL: ${{ github.event.workflow_run.html_url }}" + echo "Commit SHA: ${{ github.event.workflow_run.head_commit.id }}" + echo "Workflow Run ID: ${{ github.event.workflow_run.id }}" - name: Set commit status # trigger is no matter what, because the status should be updated if: always() @@ -85,6 +94,101 @@ jobs: # Skip the setup job if the parent job failed. skip-install: ${{ github.event.workflow_run.conclusion != 'success' }} + create-deployed-layouts-matrix: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.generate-matrix.outputs.matrix }} + steps: + - name: Echo a message + run: echo "Creating the matrix for deployed layouts." + - name: Checkout code + uses: actions/checkout@v4 + if: ${{ github.event.workflow_run.conclusion == 'success' }} + - name: Generate matrix from deployedContracts.json + id: generate-matrix + if: ${{ github.event.workflow_run.conclusion == 'success' }} + run: | + set -e + data=$(cat script/deployedContracts.json) + + bootstrap=$(echo "$data" | jq -r '.clientChain.bootstrapLogic // empty') + clientGateway=$(echo "$data" | jq -r '.clientChain.clientGatewayLogic // empty') + vault=$(echo "$data" | jq -r '.clientChain.vaultImplementation // empty') + rewardVault=$(echo "$data" | jq -r '.clientChain.rewardVaultImplementation // empty') + capsule=$(echo "$data" | jq -r '.clientChain.capsuleImplementation // empty') + + # Create the matrix as a JSON array + matrix=$(jq -n \ + --arg bootstrap "$bootstrap" \ + --arg clientGateway "$clientGateway" \ + --arg vault "$vault" \ + --arg rewardVault "$rewardVault" \ + --arg capsule "$capsule" \ + '[{name: "Bootstrap", address: $bootstrap}, + {name: "ClientChainGateway", address: $clientGateway}, + {name: "Vault", address: $vault}, + {name: "RewardVault", address: $rewardVault}, + {name: "ExoCapsule", address: $capsule}] | map(select(.address != ""))') + + echo "Matrix: $matrix" + echo "matrix=$matrix" >> "${GITHUB_OUTPUT}" + + fetch-deployed-layouts: + timeout-minutes: 30 + strategy: + matrix: + # if the parent workflow failed, the matrix will be empty. hence, no jobs will run. + contract: ${{ fromJSON(needs.create-deployed-layouts-matrix.outputs.matrix) }} + needs: + - setup + - create-deployed-layouts-matrix + runs-on: ubuntu-latest + steps: + - name: Restore cached Foundry toolchain + uses: actions/cache/restore@v3 + with: + path: ${{ needs.setup.outputs.installation-dir }} + key: ${{ needs.setup.outputs.cache-key }} + - name: Add Foundry to PATH + run: echo "${{ needs.setup.outputs.installation-dir }}" >> "$GITHUB_PATH" + - name: Fetch the deployed layout + env: + ALCHEMY_API_KEY: ${{ secrets.ALCHEMY_API_KEY }} + ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }} + run: | + echo "Processing ${{ matrix.contract.name }} at address ${{ matrix.contract.address }}" + RPC_URL="https://eth-sepolia.g.alchemy.com/v2/$ALCHEMY_API_KEY" + cast storage --json "${{ matrix.contract.address }}" \ + --rpc-url "$RPC_URL" \ + --etherscan-api-key "$ETHERSCAN_API_KEY" > "${{ matrix.contract.name }}.deployed.json" + - name: Upload the deployed layout file as an artifact + uses: actions/upload-artifact@v4 + with: + path: ${{ matrix.contract.name }}.deployed.json + name: deployed-layout-${{ matrix.contract.name }}-${{ github.event.workflow_run.head_commit.id }} + + combine-deployed-layouts: + needs: fetch-deployed-layouts + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Echo a message + run: echo "Combining the deployed layouts." + - name: Download artifacts + if: ${{ github.event.workflow_run.conclusion == 'success' }} + uses: actions/download-artifact@v4 + with: + path: combined + - name: Zip up the deployed layouts + if: ${{ github.event.workflow_run.conclusion == 'success' }} + run: zip -j deployed-layouts.zip combined/*/*.json + - name: Upload the deployed layout files as an artifact + if: ${{ github.event.workflow_run.conclusion == 'success' }} + uses: actions/upload-artifact@v4 + with: + path: deployed-layouts.zip + name: deployed-layouts-${{ github.event.workflow_run.head_commit.id }} + # The actual job to compare the storage layouts. compare-storage-layouts: # Typically takes no more than 7 minutes @@ -92,82 +196,33 @@ jobs: needs: - setup - set-commit-status + - combine-deployed-layouts runs-on: ubuntu-latest - env: - ALCHEMY_API_KEY: ${{ secrets.ALCHEMY_API_KEY }} - ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }} - steps: - # Log the workflow trigger details for debugging. - - name: Echo workflow trigger details - run: | - echo "Workflow run event: ${{ github.event.workflow_run.event }}" - echo "Workflow run conclusion: ${{ github.event.workflow_run.conclusion }}" - echo "Workflow run name: ${{ github.event.workflow_run.name }}" - echo "Workflow run URL: ${{ github.event.workflow_run.html_url }}" - echo "Commit SHA: ${{ github.event.workflow_run.head_commit.id }}" - echo "Workflow Run ID: ${{ github.event.workflow_run.id }}" # The repository needs to be available for script/deployedContracts.json - # and script/compareLayouts.js. We do not restore the build data because - # the compiled layouts are restored from the artifact and not rebuilt. + # and script/compareLayouts.js. - name: Checkout the repository if: ${{ github.event.workflow_run.conclusion == 'success' }} uses: actions/checkout@v4 - # The toolchain is needed to run `cast storage` - - name: Restore cached Foundry toolchain + - name: Restore the compiled layout files from the artifact if: ${{ github.event.workflow_run.conclusion == 'success' }} - uses: actions/cache/restore@v3 + uses: dawidd6/action-download-artifact@v6 with: - path: ${{ needs.setup.outputs.installation-dir }} - key: ${{ needs.setup.outputs.cache-key }} - - name: Add Foundry to PATH - if: ${{ github.event.workflow_run.conclusion == 'success' }} - run: echo "${{ needs.setup.outputs.installation-dir }}" >> "$GITHUB_PATH" - - name: Fetch the deployed layouts - if: ${{ github.event.workflow_run.conclusion == 'success' }} - run: | - set -e - data=$(cat script/deployedContracts.json) - bootstrap=$(echo "$data" | jq -r '.clientChain.bootstrapLogic // empty') - clientGateway=$(echo "$data" | jq -r '.clientChain.clientGatewayLogic // empty') - vault=$(echo "$data" | jq -r '.clientChain.vaultImplementation // empty') - rewardVault=$(echo "$data" | jq -r '.clientChain.rewardVaultImplementation // empty') - capsule=$(echo "$data" | jq -r '.clientChain.capsuleImplementation // empty') - pwd=$(pwd) - cd /tmp - # Create an array of contract names and addresses - declare -A contracts=( - ["Bootstrap"]="$bootstrap" - ["ClientChainGateway"]="$clientGateway" - ["Vault"]="$vault" - ["RewardVault"]="$rewardVault" - ["ExoCapsule"]="$capsule" - ) - # Iterate over the array and run `cast storage` for each contract - RPC_URL="https://eth-sepolia.g.alchemy.com/v2/$ALCHEMY_API_KEY" - for contract in "${!contracts[@]}"; do - address=${contracts[$contract]} - if [[ -n $address ]]; then - echo "Processing $contract at address $address" - cast storage --json "$address" --rpc-url "$RPC_URL" \ - --etherscan-api-key "$ETHERSCAN_API_KEY" > "$contract.deployed.json" - mv "$contract.deployed.json" "$pwd" - else - echo "Skipping $contract as no address is provided" - fi - done - cd "$pwd" - # Restore the layouts from the previous job - - name: Restore the layout files from the artifact + name: compiled-layouts-${{ github.event.workflow_run.head_commit.id }} + run_id: ${{ github.event.workflow_run.id }} + - name: Restore the deployed layout files from the artifact if: ${{ github.event.workflow_run.conclusion == 'success' }} uses: dawidd6/action-download-artifact@v6 with: - name: storage-layouts-${{ github.event.workflow_run.head_commit.id }} - run_id: ${{ github.event.workflow_run.id }} - - name: Extract the restored layouts + name: deployed-layouts-${{ github.event.workflow_run.head_commit.id }} + # no run_id is needed because the artifacts are from the same run + - name: Extract the restored compiled layouts + if: ${{ github.event.workflow_run.conclusion == 'success' }} + run: unzip compiled-layouts.zip + - name: Extract the restored deployed layouts if: ${{ github.event.workflow_run.conclusion == 'success' }} - run: unzip storage-layouts.zip + run: unzip deployed-layouts.zip - name: Set up Node.js if: ${{ github.event.workflow_run.conclusion == 'success' }} uses: actions/setup-node@v4 diff --git a/.github/workflows/forge-ci.yml b/.github/workflows/forge-ci.yml index d929eaaa..a8ae91bd 100644 --- a/.github/workflows/forge-ci.yml +++ b/.github/workflows/forge-ci.yml @@ -143,7 +143,7 @@ jobs: uses: actions/upload-artifact@v4 with: path: ExocoreGateway.base.json - name: storage-layouts-ExocoreGateway-base-${{ github.event.pull_request.base.sha || github.event.after || github.sha }} + name: compiled-layout-ExocoreGateway-base-${{ github.event.pull_request.base.sha || github.event.after || github.sha }} extract-storage-layouts: runs-on: ubuntu-latest @@ -179,7 +179,7 @@ jobs: uses: actions/upload-artifact@v4 with: path: ${{ matrix.contract }}.compiled.json - name: storage-layouts-${{ matrix.contract}}-${{ github.event.pull_request.head.sha || github.event.after || github.sha }} + name: compiled-layout-${{ matrix.contract}}-${{ github.event.pull_request.head.sha || github.event.after || github.sha }} combine-storage-layouts: runs-on: ubuntu-latest @@ -195,10 +195,10 @@ jobs: # inside the provided path. with: path: combined - - name: Zip up the storage layouts - run: zip -j storage-layouts.zip combined/*/*.json - - name: Upload storage layout file as an artifact + - name: Zip up the compiled layouts + run: zip -j compiled-layouts.zip combined/*/*.json + - name: Upload the compiled layouts file as an artifact uses: actions/upload-artifact@v4 with: - path: storage-layouts.zip - name: storage-layouts-${{ github.event.pull_request.head.sha || github.event.after || github.sha }} \ No newline at end of file + path: compiled-layouts.zip + name: compiled-layouts-${{ github.event.pull_request.head.sha || github.event.after || github.sha }} From 5ed62a2bcf8ae068514a2cf3473a1115c390a1f2 Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Wed, 27 Nov 2024 17:49:57 +0000 Subject: [PATCH 18/23] fix(ci): make matrix single line output --- .github/workflows/compare-layouts.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/compare-layouts.yml b/.github/workflows/compare-layouts.yml index 85974619..75bbe739 100644 --- a/.github/workflows/compare-layouts.yml +++ b/.github/workflows/compare-layouts.yml @@ -131,7 +131,7 @@ jobs: {name: "ExoCapsule", address: $capsule}] | map(select(.address != ""))') echo "Matrix: $matrix" - echo "matrix=$matrix" >> "${GITHUB_OUTPUT}" + echo "matrix=$(echo $matrix | jq -c .)" >> "${GITHUB_OUTPUT}" fetch-deployed-layouts: timeout-minutes: 30 From 7164340bff6b8ae196099db931ebcdda256f7ffe Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Wed, 27 Nov 2024 18:03:44 +0000 Subject: [PATCH 19/23] fix(ci): download current run artifact --- .github/workflows/compare-layouts.yml | 7 +++++-- .github/workflows/lint.yml | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/compare-layouts.yml b/.github/workflows/compare-layouts.yml index 75bbe739..c0ff00b2 100644 --- a/.github/workflows/compare-layouts.yml +++ b/.github/workflows/compare-layouts.yml @@ -213,10 +213,13 @@ jobs: run_id: ${{ github.event.workflow_run.id }} - name: Restore the deployed layout files from the artifact if: ${{ github.event.workflow_run.conclusion == 'success' }} - uses: dawidd6/action-download-artifact@v6 + uses: actions/download-artifact@v4 with: name: deployed-layouts-${{ github.event.workflow_run.head_commit.id }} - # no run_id is needed because the artifacts are from the same run + path: ./ + - name: Debug current directory + if: ${{ github.event.workflow_run.conclusion == 'success' }} + run: ls -la - name: Extract the restored compiled layouts if: ${{ github.event.workflow_run.conclusion == 'success' }} run: unzip compiled-layouts.zip diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 21e1d23c..deaeea08 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -22,9 +22,9 @@ jobs: - uses: actions/checkout@v4 - name: Set up Node.js - uses: actions/setup-node@v2 + uses: actions/setup-node@v4 with: - node-version: '22' + node-version: '18' # Latest LTS - name: Clear npm cache run: npm cache clean --force From 93a0a13fc2bb7ddae4aab88f146d8f484ff50207 Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Wed, 27 Nov 2024 18:14:36 +0000 Subject: [PATCH 20/23] fix(ci): remove debug --- .github/workflows/compare-layouts.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/compare-layouts.yml b/.github/workflows/compare-layouts.yml index c0ff00b2..2c39b699 100644 --- a/.github/workflows/compare-layouts.yml +++ b/.github/workflows/compare-layouts.yml @@ -84,12 +84,13 @@ jobs: message: ${{ steps.set-message.outputs.message }} setup: - # A full job can be used as a reusable workflow but not a step. + # The caching of the binaries is necessary because we run the job to fetch + # the deployed layouts via a matrix strategy. This job is the parent of that job. uses: ./.github/workflows/reusable-foundry-setup.yml with: # The below line does not accept environment variables, # so it becomes the single source of truth for the version, within this workflow. - # Any `pinning` of the version should be done here and forge-ci.yml. + # Any `pinning` of the version should be done here and in forge-ci.yml. foundry-version: nightly # Skip the setup job if the parent job failed. skip-install: ${{ github.event.workflow_run.conclusion != 'success' }} @@ -99,7 +100,7 @@ jobs: outputs: matrix: ${{ steps.generate-matrix.outputs.matrix }} steps: - - name: Echo a message + - name: Echo a message to avoid "no action run" warning. run: echo "Creating the matrix for deployed layouts." - name: Checkout code uses: actions/checkout@v4 @@ -217,9 +218,6 @@ jobs: with: name: deployed-layouts-${{ github.event.workflow_run.head_commit.id }} path: ./ - - name: Debug current directory - if: ${{ github.event.workflow_run.conclusion == 'success' }} - run: ls -la - name: Extract the restored compiled layouts if: ${{ github.event.workflow_run.conclusion == 'success' }} run: unzip compiled-layouts.zip From 3866fc95984129daa9b4b3ba8bece6a1a2f34719 Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Wed, 27 Nov 2024 18:31:33 +0000 Subject: [PATCH 21/23] fix(ci): always create matrix --- .github/workflows/compare-layouts.yml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/compare-layouts.yml b/.github/workflows/compare-layouts.yml index 2c39b699..c49804b2 100644 --- a/.github/workflows/compare-layouts.yml +++ b/.github/workflows/compare-layouts.yml @@ -96,18 +96,17 @@ jobs: skip-install: ${{ github.event.workflow_run.conclusion != 'success' }} create-deployed-layouts-matrix: + # Generating the matrix is very quick. It should be done regardless of the parent + # workflow status, because an empty matrix will result in no `fetch-deployed-layouts` + # jobs, which will cascade to no `compare-storage-layouts` job. runs-on: ubuntu-latest outputs: matrix: ${{ steps.generate-matrix.outputs.matrix }} steps: - - name: Echo a message to avoid "no action run" warning. - run: echo "Creating the matrix for deployed layouts." - name: Checkout code uses: actions/checkout@v4 - if: ${{ github.event.workflow_run.conclusion == 'success' }} - name: Generate matrix from deployedContracts.json id: generate-matrix - if: ${{ github.event.workflow_run.conclusion == 'success' }} run: | set -e data=$(cat script/deployedContracts.json) @@ -145,14 +144,19 @@ jobs: - create-deployed-layouts-matrix runs-on: ubuntu-latest steps: + - name: Echo a message to prevent "no steps" warning. + run: echo "Fetching the deployed layouts." - name: Restore cached Foundry toolchain + if: ${{ github.event.workflow_run.conclusion == 'success' }} uses: actions/cache/restore@v3 with: path: ${{ needs.setup.outputs.installation-dir }} key: ${{ needs.setup.outputs.cache-key }} - name: Add Foundry to PATH + if: ${{ github.event.workflow_run.conclusion == 'success' }} run: echo "${{ needs.setup.outputs.installation-dir }}" >> "$GITHUB_PATH" - name: Fetch the deployed layout + if: ${{ github.event.workflow_run.conclusion == 'success' }} env: ALCHEMY_API_KEY: ${{ secrets.ALCHEMY_API_KEY }} ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }} @@ -163,6 +167,7 @@ jobs: --rpc-url "$RPC_URL" \ --etherscan-api-key "$ETHERSCAN_API_KEY" > "${{ matrix.contract.name }}.deployed.json" - name: Upload the deployed layout file as an artifact + if: ${{ github.event.workflow_run.conclusion == 'success' }} uses: actions/upload-artifact@v4 with: path: ${{ matrix.contract.name }}.deployed.json @@ -173,7 +178,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 steps: - - name: Echo a message + - name: Echo a message to prevent "no steps" warning. run: echo "Combining the deployed layouts." - name: Download artifacts if: ${{ github.event.workflow_run.conclusion == 'success' }} From 491ecbab4a65166b64a504badf85a57fb0e405c6 Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Wed, 27 Nov 2024 18:50:58 +0000 Subject: [PATCH 22/23] fix(ci): update job timeouts --- .github/workflows/compare-layouts.yml | 14 +++++++++----- .github/workflows/forge-ci.yml | 28 +++++++++++++-------------- .github/workflows/lint.yml | 4 ++-- 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/.github/workflows/compare-layouts.yml b/.github/workflows/compare-layouts.yml index c49804b2..e3030768 100644 --- a/.github/workflows/compare-layouts.yml +++ b/.github/workflows/compare-layouts.yml @@ -16,7 +16,7 @@ jobs: # Do this job first to update the commit status and comment ASAP. set-commit-status: # Typically takes no more than 30s - timeout-minutes: 10 + timeout-minutes: 5 runs-on: ubuntu-latest outputs: number: ${{ steps.pr-context.outputs.number }} @@ -96,6 +96,8 @@ jobs: skip-install: ${{ github.event.workflow_run.conclusion != 'success' }} create-deployed-layouts-matrix: + # Takes about 2 seconds + timeout-minutes: 5 # Generating the matrix is very quick. It should be done regardless of the parent # workflow status, because an empty matrix will result in no `fetch-deployed-layouts` # jobs, which will cascade to no `compare-storage-layouts` job. @@ -134,7 +136,8 @@ jobs: echo "matrix=$(echo $matrix | jq -c .)" >> "${GITHUB_OUTPUT}" fetch-deployed-layouts: - timeout-minutes: 30 + # Takes about 15 seconds + timeout-minutes: 5 strategy: matrix: # if the parent workflow failed, the matrix will be empty. hence, no jobs will run. @@ -174,9 +177,10 @@ jobs: name: deployed-layout-${{ matrix.contract.name }}-${{ github.event.workflow_run.head_commit.id }} combine-deployed-layouts: + # Takes about 4 seconds + timeout-minutes: 5 needs: fetch-deployed-layouts runs-on: ubuntu-latest - timeout-minutes: 5 steps: - name: Echo a message to prevent "no steps" warning. run: echo "Combining the deployed layouts." @@ -197,8 +201,8 @@ jobs: # The actual job to compare the storage layouts. compare-storage-layouts: - # Typically takes no more than 7 minutes - timeout-minutes: 30 + # Takes no more than a minute + timeout-minutes: 5 needs: - setup - set-commit-status diff --git a/.github/workflows/forge-ci.yml b/.github/workflows/forge-ci.yml index a8ae91bd..926f3544 100644 --- a/.github/workflows/forge-ci.yml +++ b/.github/workflows/forge-ci.yml @@ -20,10 +20,10 @@ jobs: foundry-version: nightly build: + # Caching is slow; takes about 3 minutes. + timeout-minutes: 15 runs-on: ubuntu-latest needs: setup - # Takes about 3 minutes - timeout-minutes: 15 outputs: # The cache-key only contains the version name. It is only used so that the name does not # need to be repeated everywhere; instead setting the `foundry-version` above suffices. @@ -57,10 +57,10 @@ jobs: key: build-${{ github.event.pull_request.head.sha || github.event.after || github.sha }} test: + # Takes less than 30s + timeout-minutes: 5 runs-on: ubuntu-latest needs: build - # No more than a few minutes - timeout-minutes: 5 steps: - name: Restore cached Foundry toolchain uses: actions/cache/restore@v3 @@ -86,10 +86,10 @@ jobs: run: NO_COLOR=1 forge snapshot >> $GITHUB_STEP_SUMMARY format: + # Takes less than 30s + timeout-minutes: 5 runs-on: ubuntu-latest needs: build - # No more than a few minutes - timeout-minutes: 5 steps: - name: Restore cached Foundry toolchain uses: actions/cache/restore@v3 @@ -113,10 +113,10 @@ jobs: run: forge fmt --check extract-base-storage-layout-exocore-gateway: + # Takes less than 30 seconds, but add some margin for git clone + timeout-minutes: 10 runs-on: ubuntu-latest needs: build - # A few minutes - timeout-minutes: 10 steps: - name: Restore cached Foundry toolchain uses: actions/cache/restore@v3 @@ -145,14 +145,14 @@ jobs: path: ExocoreGateway.base.json name: compiled-layout-ExocoreGateway-base-${{ github.event.pull_request.base.sha || github.event.after || github.sha }} - extract-storage-layouts: + extract-storage-layout: + # Takes less than 30 seconds + timeout-minutes: 5 runs-on: ubuntu-latest needs: build strategy: matrix: contract: [Bootstrap, ClientChainGateway, RewardVault, Vault, ExocoreGateway, ExoCapsule] - # A few minutes - timeout-minutes: 10 steps: - name: Restore cached Foundry toolchain uses: actions/cache/restore@v3 @@ -182,12 +182,12 @@ jobs: name: compiled-layout-${{ matrix.contract}}-${{ github.event.pull_request.head.sha || github.event.after || github.sha }} combine-storage-layouts: + # Takes less than 10 seconds + timeout-minutes: 5 runs-on: ubuntu-latest needs: - extract-base-storage-layout-exocore-gateway - - extract-storage-layouts - # A few minutes - timeout-minutes: 10 + - extract-storage-layout steps: - name: Download artifacts uses: actions/download-artifact@v4 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index deaeea08..0fd2978c 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,10 +12,10 @@ on: jobs: lint: + # Usually done in 30 seconds + timeout-minutes: 5 strategy: fail-fast: true - # No more than a few minutes - timeout-minutes: 5 runs-on: ubuntu-latest steps: From 99bb116ecd3ddfc4cedcccff9dfc68b102fbccee Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Wed, 27 Nov 2024 18:59:44 +0000 Subject: [PATCH 23/23] fix(ci): respond to AI comments --- .github/workflows/compare-layouts.yml | 2 +- .github/workflows/lint.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/compare-layouts.yml b/.github/workflows/compare-layouts.yml index e3030768..da4408b9 100644 --- a/.github/workflows/compare-layouts.yml +++ b/.github/workflows/compare-layouts.yml @@ -133,7 +133,7 @@ jobs: {name: "ExoCapsule", address: $capsule}] | map(select(.address != ""))') echo "Matrix: $matrix" - echo "matrix=$(echo $matrix | jq -c .)" >> "${GITHUB_OUTPUT}" + echo "matrix=$(echo "$matrix" | jq -c .)" >> "${GITHUB_OUTPUT}" fetch-deployed-layouts: # Takes about 15 seconds diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 0fd2978c..b4e82c73 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -24,7 +24,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: '18' # Latest LTS + node-version: '20' # LTS till Oct-25 - name: Clear npm cache run: npm cache clean --force