Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
8404cf6
fix(rpc): fix Etherscan contract resolution and payload deserialization
intelliDean Apr 16, 2026
3769253
feat(rpc): persist Etherscan contract name cache to disk
intelliDean Apr 16, 2026
8385f3a
feat: Implement interactive flamegraph in Studio & transition to stan…
intelliDean Apr 16, 2026
44f5c05
feat(v0.2): Unified capture — --profile, --etherscan-key, --studio flags
intelliDean Apr 16, 2026
9b22dfa
chore: broaden .gitignore to exclude local trace artefacts (*.json, *…
intelliDean Apr 16, 2026
769e2df
chore: untrack local test trace artefacts now covered by .gitignore
intelliDean Apr 16, 2026
ad62181
feat: stabilize Stylus trace parsing and Studio integration
intelliDean Apr 22, 2026
b6dcc24
feat: display trace summary in terminal on capture
intelliDean Apr 23, 2026
cb99983
fix: eliminate double RPC fetch on --profile; print summary always; f…
intelliDean Apr 23, 2026
17282e1
feat: upgrade capture summary — colour-coded HostIO table, ASCII flam…
intelliDean Apr 23, 2026
ec297f8
feat: add Execution vs Intrinsic gas split via eth_getTransactionReceipt
intelliDean Apr 24, 2026
a6718f8
fix: route atupa profile SVG output to artifacts/profile/ directory
intelliDean Apr 24, 2026
ce78fc5
fix(audit): fix Lido submit selector, add top-level calldata detectio…
intelliDean Apr 24, 2026
85a0b9f
feat(cli): implement atupa.toml threshold engine for diff command
intelliDean Apr 24, 2026
8a17f22
feat(cli): implement visual diff flamegraph generation via --svg
intelliDean Apr 24, 2026
4a063bd
fix(svg): escape angle brackets in diff legend to fix XML parsing
intelliDean Apr 24, 2026
c6ab749
refactor(svg): rewrite diff flamegraph to use depth-swimlane architec…
intelliDean Apr 24, 2026
cdebce0
style(svg): apply premium Atupa CSS gradients and typography to diff …
intelliDean Apr 24, 2026
0db6477
feat(diff): add protocol deep diff via --protocol flag with Aave fiel…
intelliDean Apr 24, 2026
0106a63
feat(ci): add composite action.yml and gas-regression.yml workflow
intelliDean Apr 24, 2026
cd06c23
feat(cli): add `atupa init` command
intelliDean Apr 24, 2026
be92c09
feat: implement Lido DeepTracer, atupa init, and Studio side-by-side …
intelliDean Apr 24, 2026
29f077a
fix: resolve CI failures (fmt, clippy, compilation errors)
intelliDean Apr 24, 2026
f9daea7
fix: restore threshold checks for JSON output and fix protocol regist…
intelliDean Apr 24, 2026
875e20b
ci: add studio build steps to fix compilation errors in GitHub Actions
intelliDean Apr 25, 2026
8048244
fix: resolve studio typescript errors and type-only import issues
intelliDean Apr 25, 2026
ca5e159
ci: add top-level permissions for gas regression bot
intelliDean Apr 25, 2026
56c5add
ci: allow PR comment step to fail silently on forks
intelliDean Apr 25, 2026
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
261 changes: 261 additions & 0 deletions .github/workflows/gas-regression.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
# ─────────────────────────────────────────────────────────────────────────────
# Atupa Gas Regression — Automated CI Workflow
#
# On every PR against main:
# 1. Spins up Anvil (local EVM) for isolated testing
# 2. Deploys the contract at HEAD of `main` → runs a tx → saves BASELINE hash
# 3. Deploys the contract at HEAD of the PR branch → runs the same tx → TARGET hash
# 4. Runs `atupa diff` to compare gas usage
# 5. Posts a sticky Markdown report to the PR, uploads SVG flamegraph
# 6. Fails CI if any threshold in atupa.toml is exceeded
#
# REQUIRED SECRETS (in repo Settings → Secrets):
# ATUPA_RPC_URL — Optional: override Anvil with a real mainnet fork RPC
#
# REQUIRED REPO VARIABLES:
# PROFILE_SCRIPT — relative path to a forge/cast script that outputs a TX hash
# to stdout (default: script/ProfileScript.s.sol)
# ─────────────────────────────────────────────────────────────────────────────

name: ⛽ Atupa Gas Regression

on:
pull_request:
branches: [ main ]
# Allow manual trigger for debugging
workflow_dispatch:
inputs:
base_tx:
description: 'Override base transaction hash'
required: false
target_tx:
description: 'Override target transaction hash'
required: false

permissions:
contents: read
pull-requests: write

env:
CARGO_TERM_COLOR: always
# Use Anvil by default; override with a real RPC via secret for mainnet-fork
RPC_URL: ${{ secrets.ATUPA_RPC_URL || 'http://127.0.0.1:8545' }}
ANVIL_PORT: 8545
ANVIL_MNEMONIC: 'test test test test test test test test test test test junk'

jobs:
# ── Job 1: Capture BASELINE gas from `main` branch ─────────────────────────
baseline:
name: 📐 Baseline (main)
runs-on: ubuntu-latest
outputs:
tx_hash: ${{ steps.capture.outputs.tx_hash }}

steps:
- name: Checkout main branch
uses: actions/checkout@v4
with:
ref: main
fetch-depth: 1

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Cache Rust build
uses: Swatinem/rust-cache@v2
with:
key: atupa-ci-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}

- name: Install Foundry (Anvil + Cast + Forge)
uses: foundry-rs/foundry-toolchain@v1

- name: Start Anvil (local ephemeral EVM)
shell: bash
run: |
anvil \
--port ${{ env.ANVIL_PORT }} \
--mnemonic "${{ env.ANVIL_MNEMONIC }}" \
--steps-tracing \
--silent &
echo "ANVIL_PID=$!" >> $GITHUB_ENV
# Wait for Anvil to be ready
for i in $(seq 1 20); do
cast block-number --rpc-url http://127.0.0.1:${{ env.ANVIL_PORT }} &>/dev/null && break
sleep 0.5
done
echo "✅ Anvil is ready on port ${{ env.ANVIL_PORT }}"

- name: Deploy contract & capture baseline TX hash
id: capture
shell: bash
env:
PRIVATE_KEY: '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'
run: |
PROFILE_SCRIPT="${{ vars.PROFILE_SCRIPT || 'script/ProfileScript.s.sol' }}"

if [ -f "$PROFILE_SCRIPT" ]; then
# Forge deployment path: run script, capture TX hash from stdout
TX_HASH=$(forge script "$PROFILE_SCRIPT" \
--rpc-url http://127.0.0.1:${{ env.ANVIL_PORT }} \
--private-key $PRIVATE_KEY \
--broadcast \
--json 2>/dev/null \
| jq -r '.receipts[-1].transactionHash' 2>/dev/null)
else
# Fallback: Use a simple cast send to a known deployed contract
echo "⚠️ No PROFILE_SCRIPT found. Sending dummy ETH transfer for baseline."
TX_HASH=$(cast send \
--rpc-url http://127.0.0.1:${{ env.ANVIL_PORT }} \
--private-key $PRIVATE_KEY \
--json \
0x70997970C51812dc3A010C7d01b50e0d17dc79C8 \
--value 0.001ether \
| jq -r '.transactionHash')
fi

echo "📌 Baseline TX: $TX_HASH"
echo "tx_hash=${TX_HASH}" >> $GITHUB_OUTPUT

- name: Stop Anvil
if: always()
run: kill ${{ env.ANVIL_PID }} 2>/dev/null || true

# ── Job 2: Capture TARGET gas from the PR branch ────────────────────────────
target:
name: 🎯 Target (PR branch)
runs-on: ubuntu-latest
outputs:
tx_hash: ${{ steps.capture.outputs.tx_hash }}

steps:
- name: Checkout PR branch
uses: actions/checkout@v4
with:
fetch-depth: 1

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Cache Rust build
uses: Swatinem/rust-cache@v2
with:
key: atupa-ci-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}


- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1

- name: Start Anvil
shell: bash
run: |
anvil \
--port ${{ env.ANVIL_PORT }} \
--mnemonic "${{ env.ANVIL_MNEMONIC }}" \
--steps-tracing \
--silent &
echo "ANVIL_PID=$!" >> $GITHUB_ENV
for i in $(seq 1 20); do
cast block-number --rpc-url http://127.0.0.1:${{ env.ANVIL_PORT }} &>/dev/null && break
sleep 0.5
done
echo "✅ Anvil is ready"

- name: Deploy contract & capture target TX hash
id: capture
shell: bash
env:
PRIVATE_KEY: '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'
run: |
PROFILE_SCRIPT="${{ vars.PROFILE_SCRIPT || 'script/ProfileScript.s.sol' }}"

if [ -f "$PROFILE_SCRIPT" ]; then
TX_HASH=$(forge script "$PROFILE_SCRIPT" \
--rpc-url http://127.0.0.1:${{ env.ANVIL_PORT }} \
--private-key $PRIVATE_KEY \
--broadcast \
--json 2>/dev/null \
| jq -r '.receipts[-1].transactionHash' 2>/dev/null)
else
echo "⚠️ No PROFILE_SCRIPT found. Sending dummy ETH transfer for target."
TX_HASH=$(cast send \
--rpc-url http://127.0.0.1:${{ env.ANVIL_PORT }} \
--private-key $PRIVATE_KEY \
--json \
0x70997970C51812dc3A010C7d01b50e0d17dc79C8 \
--value 0.001ether \
| jq -r '.transactionHash')
fi

echo "📌 Target TX: $TX_HASH"
echo "tx_hash=${TX_HASH}" >> $GITHUB_OUTPUT

- name: Stop Anvil
if: always()
run: kill ${{ env.ANVIL_PID }} 2>/dev/null || true

# ── Job 3: Diff & Report ─────────────────────────────────────────────────────
diff:
name: 🏮 Atupa Gas Diff
runs-on: ubuntu-latest
# Support manual override of hashes via workflow_dispatch
needs: [ baseline, target ]
permissions:
pull-requests: write # Required for posting PR comments

steps:
- name: Checkout PR branch
uses: actions/checkout@v4

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Cache Rust build
uses: Swatinem/rust-cache@v2
with:
key: atupa-ci-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}


- name: Install Foundry (for Anvil)
uses: foundry-rs/foundry-toolchain@v1

- name: Start Anvil (for diff RPC access)
shell: bash
run: |
anvil \
--port ${{ env.ANVIL_PORT }} \
--mnemonic "${{ env.ANVIL_MNEMONIC }}" \
--steps-tracing \
--silent &
echo "ANVIL_PID=$!" >> $GITHUB_ENV
for i in $(seq 1 20); do
cast block-number --rpc-url http://127.0.0.1:${{ env.ANVIL_PORT }} &>/dev/null && break
sleep 0.5
done

# Resolve TX hashes: manual override > job outputs
- name: Resolve TX hashes
id: hashes
shell: bash
run: |
BASE="${{ github.event.inputs.base_tx || needs.baseline.outputs.tx_hash }}"
TARGET="${{ github.event.inputs.target_tx || needs.target.outputs.tx_hash }}"
echo "base=${BASE}" >> $GITHUB_OUTPUT
echo "target=${TARGET}" >> $GITHUB_OUTPUT
echo "📌 Comparing:"
echo " Base: $BASE"
echo " Target: $TARGET"

# Use the Atupa composite action (the action.yml we just created)
- name: Run Atupa Gas Diff
uses: ./
with:
base_tx: ${{ steps.hashes.outputs.base }}
target_tx: ${{ steps.hashes.outputs.target }}
rpc_url: 'http://127.0.0.1:${{ env.ANVIL_PORT }}'
config: 'atupa.toml'
post_comment: 'true'
upload_svg: 'true'

- name: Stop Anvil
if: always()
run: kill ${{ env.ANVIL_PID }} 2>/dev/null || true
10 changes: 10 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ jobs:
with:
components: clippy
- uses: Swatinem/rust-cache@v2
- name: Build Studio
run: |
cd studio
npm install
npm run build
- name: Run clippy
run: cargo clippy --workspace --all-targets -- -D warnings

Expand All @@ -41,6 +46,11 @@ jobs:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Build Studio
run: |
cd studio
npm install
npm run build
- name: Cargo Check
run: cargo check --workspace
- name: Run tests
Expand Down
16 changes: 15 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,24 @@ profile_*.svg

.ideanode_modules/

# Atupa trace reports (local test artefacts)
# Atupa trace reports (local test artefacts — any *.json at workspace root)
report.json
*.report.json
*-report.json
arb-*.json
base-*.json
stylus-*.json

# Atupa generated SVGs at workspace root
*.svg

# Atupa Studio build output
studio/dist/
studio/node_modules/

# Local artifacts
artifacts/

# Playground
playground/out/
playground/cache/
Loading
Loading