diff --git a/.github/workflows/_ci.yml b/.github/workflows/_ci.yml index 1cc4817..145b2b4 100644 --- a/.github/workflows/_ci.yml +++ b/.github/workflows/_ci.yml @@ -1,5 +1,5 @@ # Reusable CI workflow for Foundry projects -# Usage: jobs..uses: ./.github/workflows/_ci.yml +# Usage: jobs..uses: BreadchainCoop/etherform/.github/workflows/_ci.yml@main name: CI (Reusable) on: @@ -57,6 +57,10 @@ on: description: 'Run Halmos symbolic execution' type: boolean default: false + etherform-ref: + description: 'Git ref for etherform scripts checkout (default: main)' + type: string + default: 'main' secrets: RPC_URL: description: 'RPC endpoint for fork-based tests' @@ -155,6 +159,14 @@ jobs: with: submodules: recursive + - name: Checkout etherform scripts + uses: actions/checkout@v4 + with: + repository: BreadchainCoop/etherform + ref: ${{ inputs.etherform-ref }} + path: .etherform + sparse-checkout: scripts + - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 @@ -189,66 +201,9 @@ jobs: - name: Extract summary table id: summary - run: | - # Extract the final summary table (box-drawing characters) - awk '/^╭/,/^╰/' coverage-raw.txt > coverage-table.txt - - # Filter to only source files and compute totals from raw counts - grep '^|' coverage-table.txt | grep '${{ inputs.coverage-source-filter }}' > src-rows.txt || true - - # Sum up hit/total for each metric across source files - total_lines_hit=0; total_lines_all=0 - total_stmts_hit=0; total_stmts_all=0 - total_branch_hit=0; total_branch_all=0 - total_funcs_hit=0; total_funcs_all=0 - - while IFS='|' read -r _ file lines stmts branches funcs _; do - extract() { echo "$1" | grep -oP '\(\K[0-9]+/[0-9]+' | tr '/' ' '; } - read lh lt <<< "$(extract "$lines")" - read sh st <<< "$(extract "$stmts")" - read bh bt <<< "$(extract "$branches")" - read fh ft <<< "$(extract "$funcs")" - total_lines_hit=$((total_lines_hit + lh)); total_lines_all=$((total_lines_all + lt)) - total_stmts_hit=$((total_stmts_hit + sh)); total_stmts_all=$((total_stmts_all + st)) - total_branch_hit=$((total_branch_hit + bh)); total_branch_all=$((total_branch_all + bt)) - total_funcs_hit=$((total_funcs_hit + fh)); total_funcs_all=$((total_funcs_all + ft)) - done < src-rows.txt - - pct() { [ "$2" -eq 0 ] && echo "100.00% (0/0)" || printf "%.2f%% (%d/%d)" "$(echo "scale=4; $1 * 100 / $2" | bc)" "$1" "$2"; } - - # Compute numeric percentages for threshold checking - num_pct() { [ "$2" -eq 0 ] && echo "100.00" || echo "scale=2; $1 * 100 / $2" | bc; } - echo "lines_pct=$(num_pct $total_lines_hit $total_lines_all)" >> $GITHUB_OUTPUT - echo "stmts_pct=$(num_pct $total_stmts_hit $total_stmts_all)" >> $GITHUB_OUTPUT - echo "branch_pct=$(num_pct $total_branch_hit $total_branch_all)" >> $GITHUB_OUTPUT - echo "funcs_pct=$(num_pct $total_funcs_hit $total_funcs_all)" >> $GITHUB_OUTPUT - - # Build the markdown comment - { - echo "## Coverage Report" - echo "" - echo "| Metric | Coverage |" - echo "|--------|----------|" - echo "| Lines | $(pct $total_lines_hit $total_lines_all) |" - echo "| Statements | $(pct $total_stmts_hit $total_stmts_all) |" - echo "| Branches | $(pct $total_branch_hit $total_branch_all) |" - echo "| Functions | $(pct $total_funcs_hit $total_funcs_all) |" - echo "" - echo "
Coverage by file" - echo "" - echo "| File | Lines | Statements | Branches | Functions |" - echo "|------|-------|------------|----------|-----------|" - while IFS='|' read -r _ file lines stmts branches funcs _; do - file=$(echo "$file" | xargs) - lines=$(echo "$lines" | xargs) - stmts=$(echo "$stmts" | xargs) - branches=$(echo "$branches" | xargs) - funcs=$(echo "$funcs" | xargs) - echo "| \`$file\` | $lines | $stmts | $branches | $funcs |" - done < src-rows.txt - echo "" - echo "
" - } > coverage-comment.md + env: + COVERAGE_SOURCE_FILTER: ${{ inputs.coverage-source-filter }} + run: bash .etherform/scripts/coverage/extract-summary.sh - name: Post coverage comment if: inputs.coverage-post-comment && github.event_name == 'pull_request' @@ -258,24 +213,13 @@ jobs: - name: Check coverage threshold if: inputs.coverage-min-threshold > 0 - run: | - THRESHOLD=${{ inputs.coverage-min-threshold }} - FAILED=0 - for metric in "Lines:${{ steps.summary.outputs.lines_pct }}" \ - "Statements:${{ steps.summary.outputs.stmts_pct }}" \ - "Branches:${{ steps.summary.outputs.branch_pct }}" \ - "Functions:${{ steps.summary.outputs.funcs_pct }}"; do - NAME="${metric%%:*}" - VALUE="${metric#*:}" - if (( $(echo "$VALUE < $THRESHOLD" | bc -l) )); then - echo "::error::$NAME coverage ($VALUE%) is below threshold ($THRESHOLD%)" - FAILED=1 - fi - done - if [[ $FAILED -eq 1 ]]; then - exit 1 - fi - echo "All coverage metrics meet the $THRESHOLD% threshold" + env: + THRESHOLD: ${{ inputs.coverage-min-threshold }} + LINES_PCT: ${{ steps.summary.outputs.lines_pct }} + STMTS_PCT: ${{ steps.summary.outputs.stmts_pct }} + BRANCH_PCT: ${{ steps.summary.outputs.branch_pct }} + FUNCS_PCT: ${{ steps.summary.outputs.funcs_pct }} + run: bash .etherform/scripts/coverage/check-threshold.sh halmos: name: Symbolic Execution @@ -318,4 +262,3 @@ jobs: env: RPC_URL: ${{ secrets.RPC_URL }} run: halmos - diff --git a/.github/workflows/_deploy-testnet.yml b/.github/workflows/_deploy-testnet.yml index 282115b..5277012 100644 --- a/.github/workflows/_deploy-testnet.yml +++ b/.github/workflows/_deploy-testnet.yml @@ -1,5 +1,5 @@ # Reusable testnet deployment workflow -# Usage: jobs..uses: ./.github/workflows/_deploy-testnet.yml +# Usage: jobs..uses: BreadchainCoop/etherform/.github/workflows/_deploy-testnet.yml@main name: Deploy Testnet (Reusable) on: @@ -33,6 +33,10 @@ on: description: 'Node.js version for package installation' type: string default: '20' + etherform-ref: + description: 'Git ref for etherform scripts checkout (default: main)' + type: string + default: 'main' secrets: PRIVATE_KEY: required: true @@ -55,6 +59,14 @@ jobs: with: submodules: recursive + - name: Checkout etherform scripts + uses: actions/checkout@v4 + with: + repository: BreadchainCoop/etherform + ref: ${{ inputs.etherform-ref }} + path: .etherform + sparse-checkout: scripts + - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 @@ -88,19 +100,7 @@ jobs: RPC_URL: ${{ secrets.RPC_URL }} DEPLOY_ENV_VARS: ${{ secrets.DEPLOY_ENV_VARS }} run: | - # Ensure PRIVATE_KEY has 0x prefix - if [[ "$PRIVATE_KEY" != 0x* ]]; then - export PRIVATE_KEY="0x$PRIVATE_KEY" - fi - - # Export user-provided environment variables - if [[ -n "$DEPLOY_ENV_VARS" ]]; then - while IFS= read -r line; do - [[ -z "$line" || "$line" == \#* ]] && continue - export "$line" - done <<< "$DEPLOY_ENV_VARS" - fi - + source .etherform/scripts/deploy/prepare-env.sh forge script ${{ inputs.deploy-script }} \ --rpc-url "$RPC_URL" \ --broadcast \ @@ -112,24 +112,12 @@ jobs: - name: Parse deployment addresses id: parse - run: | - # Find the latest broadcast file - BROADCAST_FILE=$(find broadcast -name "run-latest.json" -type f | head -1) - if [[ -z "$BROADCAST_FILE" ]]; then - echo "No broadcast file found" - exit 1 - fi - echo "broadcast_file=$BROADCAST_FILE" >> $GITHUB_OUTPUT - - # Extract deployed contract addresses - jq -r '.transactions[] | select(.transactionType == "CREATE") | "\(.contractName): \(.contractAddress)"' "$BROADCAST_FILE" | tee deployment-summary.txt + run: bash .etherform/scripts/deploy/parse-broadcast.sh - name: Save deployment artifacts run: | mkdir -p deployments/${{ steps.network.outputs.network_name }} BROADCAST_FILE="${{ steps.parse.outputs.broadcast_file }}" - - # Extract contracts with proper sourcePathAndName format jq '{contracts: [.transactions[] | select(.transactionType == "CREATE") | {sourcePathAndName: "src/\(.contractName).sol:\(.contractName)", address: .contractAddress}]}' "$BROADCAST_FILE" \ > deployments/${{ steps.network.outputs.network_name }}/deployment.json @@ -142,79 +130,13 @@ jobs: - name: Create deployment summary env: BLOCKSCOUT_URL: ${{ steps.network.outputs.blockscout_url }} - ETH_RPC_URL: ${{ secrets.RPC_URL }} - run: | - echo "## Testnet Deployment Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Contract | Address | Explorer |" >> $GITHUB_STEP_SUMMARY - echo "|----------|---------|----------|" >> $GITHUB_STEP_SUMMARY - - while read -r line; do - CONTRACT=$(echo "$line" | cut -d: -f1) - ADDRESS=$(echo "$line" | cut -d: -f2 | tr -d ' ') - echo "| $CONTRACT | \`$ADDRESS\` | [View](${BLOCKSCOUT_URL}/address/$ADDRESS) |" >> $GITHUB_STEP_SUMMARY - done < deployment-summary.txt + SUMMARY_TITLE: Testnet Deployment Summary + run: bash .etherform/scripts/deploy/deployment-summary.sh - name: Verify contracts on Blockscout if: inputs.verify-contracts env: BLOCKSCOUT_URL: ${{ steps.network.outputs.blockscout_url }} ETH_RPC_URL: ${{ secrets.RPC_URL }} - run: | - # Validate Blockscout URL format (fail fast on bad URL) - if [[ -z "$BLOCKSCOUT_URL" || "$BLOCKSCOUT_URL" == "null" ]]; then - echo "::error::BLOCKSCOUT_URL is empty or not configured. Check deploy-networks.json." - exit 1 - fi - if [[ ! "$BLOCKSCOUT_URL" =~ ^https?:// ]]; then - echo "::error::BLOCKSCOUT_URL has invalid format (got: '$BLOCKSCOUT_URL'). Must start with http:// or https://." - exit 1 - fi - - BROADCAST_FILE="${{ steps.parse.outputs.broadcast_file }}" - VERIFY_FAILED=0 - - # Use process substitution (not pipe) so variables propagate to this shell - while read -r tx; do - CONTRACT_NAME=$(echo "$tx" | jq -r '.contractName') - CONTRACT_ADDR=$(echo "$tx" | jq -r '.contractAddress') - - echo "Verifying $CONTRACT_NAME at $CONTRACT_ADDR..." - VERIFIED=0 - - for attempt in 1 2 3; do - # Capture output to check for actual success (exit code alone is unreliable) - # Use timeout to prevent hanging on unreachable URLs - VERIFY_OUTPUT=$(timeout 120 forge verify-contract "$CONTRACT_ADDR" "$CONTRACT_NAME" \ - --verifier blockscout \ - --verifier-url "${BLOCKSCOUT_URL}/api" \ - --guess-constructor-args \ - --watch 2>&1) || true - echo "$VERIFY_OUTPUT" - - if echo "$VERIFY_OUTPUT" | grep -qi "Contract successfully verified\|Pass - Verified"; then - echo "✓ Verified $CONTRACT_NAME" - VERIFIED=1 - break - elif echo "$VERIFY_OUTPUT" | grep -qi "Already Verified"; then - echo "✓ $CONTRACT_NAME already verified" - VERIFIED=1 - break - else - if [[ $attempt -lt 3 ]]; then - echo "Attempt $attempt failed, retrying in $((attempt * 30))s..." - sleep $((attempt * 30)) - fi - fi - done - - if [[ $VERIFIED -eq 0 ]]; then - echo "::error::Verification failed for $CONTRACT_NAME after 3 attempts" - VERIFY_FAILED=1 - fi - done < <(jq -c '.transactions[] | select(.transactionType == "CREATE")' "$BROADCAST_FILE") - - if [[ $VERIFY_FAILED -eq 1 ]]; then - echo "::error::One or more contracts failed verification" - exit 1 - fi + BROADCAST_FILE: ${{ steps.parse.outputs.broadcast_file }} + run: bash .etherform/scripts/deploy/verify-blockscout.sh diff --git a/.github/workflows/_foundry-cicd.yml b/.github/workflows/_foundry-cicd.yml index 726083c..22724de 100644 --- a/.github/workflows/_foundry-cicd.yml +++ b/.github/workflows/_foundry-cicd.yml @@ -78,10 +78,6 @@ on: default: 0 # Upgrade Safety Options - run-upgrade-safety: - description: 'Run upgrade safety validation' - type: boolean - default: true upgrades-config: description: 'Path to upgrade safety configuration (upgrades.json)' type: string @@ -118,6 +114,12 @@ on: type: boolean default: false + # Etherform Options + etherform-ref: + description: 'Git ref for etherform scripts checkout (default: main)' + type: string + default: 'main' + secrets: PRIVATE_KEY: description: 'Deployer wallet private key' @@ -273,6 +275,14 @@ jobs: with: submodules: recursive + - name: Checkout etherform scripts + uses: actions/checkout@v4 + with: + repository: BreadchainCoop/etherform + ref: ${{ inputs.etherform-ref }} + path: .etherform + sparse-checkout: scripts + - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 @@ -307,66 +317,9 @@ jobs: - name: Extract summary table id: summary - run: | - # Extract the final summary table (box-drawing characters) - awk '/^╭/,/^╰/' coverage-raw.txt > coverage-table.txt - - # Filter to only source files and compute totals from raw counts - grep '^|' coverage-table.txt | grep '${{ inputs.coverage-source-filter }}' > src-rows.txt || true - - # Sum up hit/total for each metric across source files - total_lines_hit=0; total_lines_all=0 - total_stmts_hit=0; total_stmts_all=0 - total_branch_hit=0; total_branch_all=0 - total_funcs_hit=0; total_funcs_all=0 - - while IFS='|' read -r _ file lines stmts branches funcs _; do - extract() { echo "$1" | grep -oP '\(\K[0-9]+/[0-9]+' | tr '/' ' '; } - read lh lt <<< "$(extract "$lines")" - read sh st <<< "$(extract "$stmts")" - read bh bt <<< "$(extract "$branches")" - read fh ft <<< "$(extract "$funcs")" - total_lines_hit=$((total_lines_hit + lh)); total_lines_all=$((total_lines_all + lt)) - total_stmts_hit=$((total_stmts_hit + sh)); total_stmts_all=$((total_stmts_all + st)) - total_branch_hit=$((total_branch_hit + bh)); total_branch_all=$((total_branch_all + bt)) - total_funcs_hit=$((total_funcs_hit + fh)); total_funcs_all=$((total_funcs_all + ft)) - done < src-rows.txt - - pct() { [ "$2" -eq 0 ] && echo "100.00% (0/0)" || printf "%.2f%% (%d/%d)" "$(echo "scale=4; $1 * 100 / $2" | bc)" "$1" "$2"; } - - # Compute numeric percentages for threshold checking - num_pct() { [ "$2" -eq 0 ] && echo "100.00" || echo "scale=2; $1 * 100 / $2" | bc; } - echo "lines_pct=$(num_pct $total_lines_hit $total_lines_all)" >> $GITHUB_OUTPUT - echo "stmts_pct=$(num_pct $total_stmts_hit $total_stmts_all)" >> $GITHUB_OUTPUT - echo "branch_pct=$(num_pct $total_branch_hit $total_branch_all)" >> $GITHUB_OUTPUT - echo "funcs_pct=$(num_pct $total_funcs_hit $total_funcs_all)" >> $GITHUB_OUTPUT - - # Build the markdown comment - { - echo "## Coverage Report" - echo "" - echo "| Metric | Coverage |" - echo "|--------|----------|" - echo "| Lines | $(pct $total_lines_hit $total_lines_all) |" - echo "| Statements | $(pct $total_stmts_hit $total_stmts_all) |" - echo "| Branches | $(pct $total_branch_hit $total_branch_all) |" - echo "| Functions | $(pct $total_funcs_hit $total_funcs_all) |" - echo "" - echo "
Coverage by file" - echo "" - echo "| File | Lines | Statements | Branches | Functions |" - echo "|------|-------|------------|----------|-----------|" - while IFS='|' read -r _ file lines stmts branches funcs _; do - file=$(echo "$file" | xargs) - lines=$(echo "$lines" | xargs) - stmts=$(echo "$stmts" | xargs) - branches=$(echo "$branches" | xargs) - funcs=$(echo "$funcs" | xargs) - echo "| \`$file\` | $lines | $stmts | $branches | $funcs |" - done < src-rows.txt - echo "" - echo "
" - } > coverage-comment.md + env: + COVERAGE_SOURCE_FILTER: ${{ inputs.coverage-source-filter }} + run: bash .etherform/scripts/coverage/extract-summary.sh - name: Post coverage comment if: inputs.coverage-post-comment && github.event_name == 'pull_request' @@ -376,24 +329,13 @@ jobs: - name: Check coverage threshold if: inputs.coverage-min-threshold > 0 - run: | - THRESHOLD=${{ inputs.coverage-min-threshold }} - FAILED=0 - for metric in "Lines:${{ steps.summary.outputs.lines_pct }}" \ - "Statements:${{ steps.summary.outputs.stmts_pct }}" \ - "Branches:${{ steps.summary.outputs.branch_pct }}" \ - "Functions:${{ steps.summary.outputs.funcs_pct }}"; do - NAME="${metric%%:*}" - VALUE="${metric#*:}" - if (( $(echo "$VALUE < $THRESHOLD" | bc -l) )); then - echo "::error::$NAME coverage ($VALUE%) is below threshold ($THRESHOLD%)" - FAILED=1 - fi - done - if [[ $FAILED -eq 1 ]]; then - exit 1 - fi - echo "All coverage metrics meet the $THRESHOLD% threshold" + env: + THRESHOLD: ${{ inputs.coverage-min-threshold }} + LINES_PCT: ${{ steps.summary.outputs.lines_pct }} + STMTS_PCT: ${{ steps.summary.outputs.stmts_pct }} + BRANCH_PCT: ${{ steps.summary.outputs.branch_pct }} + FUNCS_PCT: ${{ steps.summary.outputs.funcs_pct }} + run: bash .etherform/scripts/coverage/check-threshold.sh halmos: name: Symbolic Execution @@ -445,9 +387,7 @@ jobs: name: Upgrade Safety runs-on: ubuntu-latest needs: [detect-changes, ci] - if: | - needs.detect-changes.outputs.should-run == 'true' && - inputs.run-upgrade-safety + if: needs.detect-changes.outputs.should-run == 'true' steps: - name: Checkout repository uses: actions/checkout@v4 @@ -455,16 +395,18 @@ jobs: submodules: recursive fetch-depth: 0 - - name: Setup Node.js - uses: actions/setup-node@v4 + - name: Checkout etherform scripts + uses: actions/checkout@v4 with: - node-version: ${{ inputs.node-version }} + repository: BreadchainCoop/etherform + ref: ${{ inputs.etherform-ref }} + path: .etherform + sparse-checkout: scripts - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 - name: Setup Node.js - if: inputs.package-manager != 'none' uses: actions/setup-node@v4 with: node-version: ${{ inputs.node-version }} @@ -482,172 +424,18 @@ jobs: - name: Build contracts run: forge build --build-info --extra-output storageLayout - - name: Validate upgrade safety - run: | - set -euo pipefail - - CONFIG="${{ inputs.upgrades-config }}" - BASE_BRANCH="${{ inputs.main-branch }}" - CURRENT_BUILD="out/build-info" - - # Check if config exists - if [[ ! -f "$CONFIG" ]]; then - echo "::warning::No upgrades config found at $CONFIG — skipping upgrade safety validation" - { - echo "## Upgrade Safety Validation" - echo "" - echo "> No \`$CONFIG\` found. Skipping validation." - } >> "$GITHUB_STEP_SUMMARY" - exit 0 - fi - - # Check for contracts array and handle empty contracts - CONTRACT_COUNT=$(jq -r 'try (.contracts | length) catch "INVALID_CONTRACTS"' "$CONFIG") - if [[ "$CONTRACT_COUNT" == "INVALID_CONTRACTS" ]]; then - echo "::error::Invalid upgrades config at $CONFIG — \`.contracts\` must be an array" - exit 1 - fi - if [[ "$CONTRACT_COUNT" -eq 0 ]]; then - echo "::warning::No contracts defined in $CONFIG — skipping upgrade safety validation" - { - echo "## Upgrade Safety Validation" - echo "" - echo "> No contracts defined in \`$CONFIG\`. Skipping validation." - } >> "$GITHUB_STEP_SUMMARY" - exit 0 - fi - - # Check if any contracts need base branch comparison (no explicit reference) - NEEDS_BASE=false - while IFS= read -r entry; do - REF_VALUE=$(echo "$entry" | jq -c '.reference // empty') - if [[ -z "$REF_VALUE" || "$REF_VALUE" == "null" ]]; then - NEEDS_BASE=true - break - fi - done < <(jq -c '.contracts[]' "$CONFIG") - - # Build base branch in a worktree if needed - BASE_BUILD="" - BASE_DIR="" - if [[ "$NEEDS_BASE" == "true" ]]; then - echo "Building base branch ($BASE_BRANCH) for comparison..." - git fetch origin "$BASE_BRANCH" 2>/dev/null || true - BASE_DIR=$(mktemp -d) - - if git worktree add --detach "$BASE_DIR" "origin/$BASE_BRANCH" 2>/dev/null; then - (cd "$BASE_DIR" && git submodule update --init --recursive 2>/dev/null || true) - - # Install dependencies in base worktree if needed - if [[ "${{ inputs.package-manager }}" != "none" ]]; then - (cd "$BASE_DIR" && case "${{ inputs.package-manager }}" in - npm) npm ci ;; - yarn) yarn --frozen-lockfile ;; - pnpm) corepack enable && pnpm install --frozen-lockfile ;; - esac) || echo "::warning::Failed to install dependencies in base branch" - fi - - if (cd "$BASE_DIR" && forge build --build-info --extra-output storageLayout 2>&1); then - mv "${BASE_DIR}/out/build-info" "${BASE_DIR}/out/base-build-info" - BASE_BUILD="${BASE_DIR}/out/base-build-info" - echo "Base branch built successfully" - else - echo "::warning::Failed to build base branch — contracts without explicit reference will be validated for upgradeability only" - fi - else - echo "::warning::Could not checkout base branch '$BASE_BRANCH' — contracts without explicit reference will be validated for upgradeability only" - fi - fi - - PASSED=0 - FAILED=0 - SUMMARY="" - - # Validate each contract - while IFS= read -r entry; do - CONTRACT=$(echo "$entry" | jq -r '.contract') - REF_VALUE=$(echo "$entry" | jq -c '.reference // empty') - CONTRACT_PATH="${CONTRACT%%:*}" - - echo "::group::Validating $CONTRACT" - - if [[ -z "$REF_VALUE" || "$REF_VALUE" == "null" ]]; then - # === Base branch comparison (default) === - if [[ -n "$BASE_BUILD" ]] && git show "origin/${BASE_BRANCH}:${CONTRACT_PATH}" > /dev/null 2>&1; then - # Contract exists on base branch — compare storage layouts - if OUTPUT=$(npx @openzeppelin/upgrades-core validate "$CURRENT_BUILD" \ - --contract "$CONTRACT" \ - --reference "base-build-info:${CONTRACT}" \ - --referenceBuildInfoDirs "$BASE_BUILD" 2>&1); then - echo "$OUTPUT" - SUMMARY="${SUMMARY}| \`${CONTRACT}\` | \`${BASE_BRANCH}\` branch | Pass |"$'\n' - PASSED=$((PASSED + 1)) - else - echo "$OUTPUT" - SUMMARY="${SUMMARY}| \`${CONTRACT}\` | \`${BASE_BRANCH}\` branch | **FAIL** |"$'\n' - FAILED=$((FAILED + 1)) - fi - else - # Contract doesn't exist on base branch or base build failed — validate upgradeability only - echo "Contract not found on $BASE_BRANCH or base build unavailable, validating upgradeability only..." - if OUTPUT=$(npx @openzeppelin/upgrades-core validate "$CURRENT_BUILD" \ - --contract "$CONTRACT" 2>&1); then - echo "$OUTPUT" - SUMMARY="${SUMMARY}| \`${CONTRACT}\` | (new contract) | Pass |"$'\n' - PASSED=$((PASSED + 1)) - else - echo "$OUTPUT" - SUMMARY="${SUMMARY}| \`${CONTRACT}\` | (new contract) | **FAIL** |"$'\n' - FAILED=$((FAILED + 1)) - fi - fi - - elif echo "$REF_VALUE" | jq -e 'type == "string"' > /dev/null 2>&1; then - # === Contract qualifier reference === - QUALIFIER=$(echo "$entry" | jq -r '.reference') - if OUTPUT=$(npx @openzeppelin/upgrades-core validate "$CURRENT_BUILD" \ - --contract "$CONTRACT" \ - --reference "$QUALIFIER" 2>&1); then - echo "$OUTPUT" - SUMMARY="${SUMMARY}| \`${CONTRACT}\` | \`${QUALIFIER}\` | Pass |"$'\n' - PASSED=$((PASSED + 1)) - else - echo "$OUTPUT" - SUMMARY="${SUMMARY}| \`${CONTRACT}\` | \`${QUALIFIER}\` | **FAIL** |"$'\n' - FAILED=$((FAILED + 1)) - fi - - else - echo "::error::Invalid reference format for $CONTRACT" - SUMMARY="${SUMMARY}| \`${CONTRACT}\` | (invalid reference) | **FAIL** |"$'\n' - FAILED=$((FAILED + 1)) - fi - - echo "::endgroup::" - done < <(jq -c '.contracts[]' "$CONFIG") - - # Clean up worktree - if [[ -n "$BASE_DIR" ]]; then - git worktree remove "$BASE_DIR" --force 2>/dev/null || true - fi - - # Write Step Summary - { - echo "## Upgrade Safety Validation" - echo "" - echo "| Contract | Reference | Result |" - echo "|----------|-----------|--------|" - echo -n "$SUMMARY" - echo "" - echo "**${PASSED} passed, ${FAILED} failed**" - } >> "$GITHUB_STEP_SUMMARY" - - if [[ "$FAILED" -gt 0 ]]; then - echo "::error::${FAILED} contract(s) failed upgrade safety validation" - exit 1 - fi + - name: Validate upgrades config + env: + UPGRADES_CONFIG: ${{ inputs.upgrades-config }} + BASE_BRANCH: ${{ inputs.main-branch }} + run: bash .etherform/scripts/upgrade-safety/validate-config.sh - echo "All contracts passed upgrade safety validation" + - name: Validate upgrade safety + env: + UPGRADES_CONFIG: ${{ inputs.upgrades-config }} + BASE_BRANCH: ${{ inputs.main-branch }} + PACKAGE_MANAGER: ${{ inputs.package-manager }} + run: bash .etherform/scripts/upgrade-safety/validate.sh deploy-testnet: name: Deploy Testnet @@ -657,7 +445,7 @@ jobs: always() && needs.detect-changes.outputs.should-run == 'true' && needs.ci.result == 'success' && - (needs.upgrade-safety.result == 'success' || needs.upgrade-safety.result == 'skipped') && + needs.upgrade-safety.result == 'success' && inputs.deploy-on-pr && github.event_name == 'pull_request' steps: @@ -666,6 +454,14 @@ jobs: with: submodules: recursive + - name: Checkout etherform scripts + uses: actions/checkout@v4 + with: + repository: BreadchainCoop/etherform + ref: ${{ inputs.etherform-ref }} + path: .etherform + sparse-checkout: scripts + - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 @@ -691,19 +487,8 @@ jobs: RPC_URL: ${{ secrets.RPC_URL }} DEPLOY_ENV_VARS: ${{ secrets.DEPLOY_ENV_VARS }} run: | - if [[ "$PRIVATE_KEY" != 0x* ]]; then - export PRIVATE_KEY="0x$PRIVATE_KEY" - fi - - # Export user-provided environment variables - if [[ -n "$DEPLOY_ENV_VARS" ]]; then - while IFS= read -r line; do - [[ -z "$line" || "$line" == \#* ]] && continue - export "$line" - done <<< "$DEPLOY_ENV_VARS" - fi - - forge script ${{ inputs.deploy-script }} \ + source .etherform/scripts/deploy/prepare-env.sh + forge script "${{ inputs.deploy-script }}" \ --rpc-url "$RPC_URL" \ --private-key "$PRIVATE_KEY" \ --broadcast \ @@ -715,175 +500,25 @@ jobs: - name: Parse deployment addresses id: parse - run: | - BROADCAST_FILE=$(find broadcast -name "run-latest.json" -type f | head -1) - if [[ -z "$BROADCAST_FILE" ]]; then - echo "No broadcast file found" - exit 1 - fi - echo "broadcast_file=$BROADCAST_FILE" >> $GITHUB_OUTPUT - - # Extract the chain ID from the broadcast file path (broadcast/