Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 23 additions & 80 deletions .github/workflows/_ci.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Reusable CI workflow for Foundry projects
# Usage: jobs.<job>.uses: ./.github/workflows/_ci.yml
# Usage: jobs.<job>.uses: BreadchainCoop/etherform/.github/workflows/_ci.yml@main
name: CI (Reusable)

on:
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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 "<details><summary>Coverage by file</summary>"
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 "</details>"
} > 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'
Expand All @@ -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
Expand Down Expand Up @@ -318,4 +262,3 @@ jobs:
env:
RPC_URL: ${{ secrets.RPC_URL }}
run: halmos

116 changes: 19 additions & 97 deletions .github/workflows/_deploy-testnet.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Reusable testnet deployment workflow
# Usage: jobs.<job>.uses: ./.github/workflows/_deploy-testnet.yml
# Usage: jobs.<job>.uses: BreadchainCoop/etherform/.github/workflows/_deploy-testnet.yml@main
name: Deploy Testnet (Reusable)

on:
Expand Down Expand Up @@ -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'
Comment on lines +36 to +39
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we recommend main by default or a specific commit? specific commit is safer so if we merge something to main they have to chose to upgrade so we can't rug them or something

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make a backlog issue and address in a future release

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we recommend main by default or a specific commit? specific commit is safer so if we merge something to main they have to chose to upgrade so we can't rug them or something

ideally we'll have releases with tags or something, if you look at other github actions they usually have something like a vX appended

Opening an issue

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#34

secrets:
PRIVATE_KEY:
required: true
Expand All @@ -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

Expand Down Expand Up @@ -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 \
Expand All @@ -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

Expand All @@ -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
Loading
Loading