Skip to content

Compare Storage Layouts #125

Compare Storage Layouts

Compare Storage Layouts #125

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:
# Typically takes no more than 30s
timeout-minutes: 10
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:
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: |
gh api \
--method POST \
/repos/${{ github.repository }}/statuses/${{ github.event.workflow_run.head_commit.id }} \
-f state=pending \
-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
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: |
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:
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: Comment 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
# Skip the setup job if the parent job failed.
skip-install: ${{ github.event.workflow_run.conclusion != 'success' }}
# 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
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.
- 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
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
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@v4
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()
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# if the outcome is not set, it will post failure
run: |
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 }}" \
-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 }}
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: Comment 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