Compare Storage Layouts #106
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: 5 | |
runs-on: ubuntu-latest | |
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 | |
# If the parent workflow is cancelled, this one should implicitly cancel itself. So, avoid | |
# interacting with the status or the comments. However, if the parent workflow failed, | |
# record a failure in this workflow as well. | |
if: ${{ github.event.workflow_run.conclusion != 'cancelled' }} | |
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' && github.event.workflow_run.conclusion != 'cancelled' }} | |
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 | |
if: ${{ github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion != 'cancelled' }} | |
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' && github.event.workflow_run.conclusion != 'cancelled' }} | |
with: | |
header: ${{ github.workflow }} | |
hide_details: true | |
number: ${{ steps.pr-context.outputs.number }} | |
message: ${{ steps.set-message.outputs.message }} | |
setup: | |
# 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 in forge-ci.yml. | |
foundry-version: nightly | |
# Skip the setup job if the parent job failed. | |
# Instead of using an if condition, use this to avoid job skipped emails. | |
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 and report an unnecessary | |
# failure. | |
if: always() | |
runs-on: ubuntu-latest | |
outputs: | |
matrix: ${{ steps.generate-matrix.outputs.matrix }} | |
steps: | |
- name: Download validated contracts json | |
uses: dawidd6/action-download-artifact@v6 | |
with: | |
name: validated-contracts-${{ github.event.workflow_run.head_commit.id }} | |
run_id: ${{ github.event.workflow_run.id }} | |
- name: Generate matrix from deployedContracts.json | |
id: generate-matrix | |
run: | | |
set -e | |
# Read the JSON file | |
data=$(cat validatedContracts.json) | |
# Generate the matrix dynamically from the JSON content | |
matrix=$(echo "$data" | jq -c 'to_entries | map({name: .key, address: .value})') | |
echo "Matrix: $matrix" | |
echo "matrix=$(echo "$matrix" | jq -c .)" >> "${GITHUB_OUTPUT}" | |
fetch-deployed-layouts: | |
# Takes about 15 seconds | |
timeout-minutes: 5 | |
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: Echo a message to prevent "no steps" warning. | |
run: echo "Fetching the deployed layouts." | |
- name: Restore cached Foundry toolchain | |
# skips cancelled as well | |
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 }} | |
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 | |
if: ${{ github.event.workflow_run.conclusion == 'success' }} | |
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: | |
# Takes about 4 seconds | |
timeout-minutes: 5 | |
needs: fetch-deployed-layouts | |
runs-on: ubuntu-latest | |
steps: | |
- 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' }} | |
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: | |
# Takes no more than a minute | |
timeout-minutes: 5 | |
needs: | |
- setup | |
- set-commit-status | |
- combine-deployed-layouts | |
runs-on: ubuntu-latest | |
steps: | |
# The repository needs to be available for script/deployments/deployedContracts.json | |
# and script/compareLayouts.js. | |
- name: Checkout the repository | |
if: ${{ github.event.workflow_run.conclusion == 'success' }} | |
uses: actions/checkout@v4 | |
- name: Restore the compiled layout files from the artifact | |
if: ${{ github.event.workflow_run.conclusion == 'success' }} | |
# Use this workflow if the artifact was generated by another workflow. | |
uses: dawidd6/action-download-artifact@v6 | |
with: | |
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' }} | |
# Use this workflow if the artifact was generated by the same workflow. | |
# It is faster than the other one and conversely, a bit limited in skill. | |
uses: actions/download-artifact@v4 | |
with: | |
name: deployed-layouts-${{ github.event.workflow_run.head_commit.id }} | |
path: ./ | |
- 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 deployed-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 | |
# Same as above, do not interact if we cancelled. However, do report the failure of parent workflow. | |
if: ${{ github.event.workflow_run.conclusion != 'cancelled' }} | |
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 | |
if: ${{ github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion != 'cancelled' }} | |
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' && github.event.workflow_run.conclusion != 'cancelled' }} | |
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 |