Reusable GitHub Actions workflows for Foundry smart contract CI/CD with upgrade safety validation.
| Workflow | Description |
|---|---|
_ci.yml |
Build, test, format check, coverage, and Halmos |
_upgrade-safety.yml |
OpenZeppelin upgrade safety validation |
_deploy-testnet.yml |
Testnet deployment with Blockscout verification |
_foundry-cicd.yml |
All-in-one orchestrator combining all of the above |
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
ci:
uses: BreadchainCoop/etherform/.github/workflows/_ci.yml@main
with:
check-formatting: true
test-verbosity: 'vvv'# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
permissions:
contents: read
pull-requests: write
jobs:
ci:
uses: BreadchainCoop/etherform/.github/workflows/_ci.yml@main
with:
package-manager: yarn
run-coverage: true
coverage-min-threshold: 80
run-halmos: true
secrets:
RPC_URL: ${{ secrets.RPC_URL }}Etherform validates upgrade safety using the OpenZeppelin upgrades-core CLI, which checks storage layout compatibility, initializer safety, and proxy semantics.
- On PR: The upgrade-safety job checks out the base branch via git worktree, builds it, and compares each contract's storage layout against the current branch using the OZ CLI
- Next PR: Validates against the latest base branch
build_info = true
extra_output = ["storageLayout"]Each entry specifies a contract to validate. The reference field controls what to compare against:
reference value |
Behavior |
|---|---|
Omitted / null |
Compare against the same contract on the base branch (default) |
"src/V1.sol:V1" |
Compare against another contract in the same build |
Minimal — validate against the base branch (most common):
{
"contracts": [
{ "contract": "src/Greeter.sol:Greeter" },
{ "contract": "src/Token.sol:Token" }
]
}With explicit contract reference — compare against a V1 contract kept in the repo:
{
"contracts": [
{
"contract": "src/GreeterV2.sol:GreeterV2",
"reference": "src/GreeterV1.sol:GreeterV1"
}
]
}# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
ci:
uses: BreadchainCoop/etherform/.github/workflows/_ci.yml@main
upgrade-safety:
needs: [ci]
uses: BreadchainCoop/etherform/.github/workflows/_upgrade-safety.yml@mainOn the first run, contracts are validated for upgradeability only.
Use NatSpec annotations in your Solidity source:
/// @custom:oz-upgrades-unsafe-allow delegatecall
contract MyContract is Initializable {
// ...
}See the OpenZeppelin docs for all supported annotations.
Create .github/deploy-networks.json in your repository:
{
"testnets": [
{
"name": "sepolia",
"chain_id": 11155111,
"blockscout_url": "https://eth-sepolia.blockscout.com",
"environment": "testnet"
}
]
}If your Foundry project uses npm/yarn/pnpm for Solidity dependencies (e.g., OpenZeppelin via node_modules), set package-manager to your package manager. This installs Node.js and runs the appropriate install command before any forge operations.
Note: If using the
_foundry-cicd.ymlall-in-one workflow withskip-if-no-changes: true, addpackage.jsonand your lock file (e.g.,yarn.lock) to thecontract-pathsinput so dependency changes trigger the workflow.
| Secret | Used by | Description |
|---|---|---|
PRIVATE_KEY |
Deploy workflows | Deployer wallet private key |
RPC_URL |
All workflows | Network RPC endpoint (also used for fork-based tests) |
DEPLOY_ENV_VARS |
Deploy workflows | Optional; newline-separated KEY=VALUE pairs exported as environment variables before running the deploy script |
| Input | Type | Default | Description |
|---|---|---|---|
check-formatting |
boolean | true |
Run forge fmt --check |
test-verbosity |
string | 'vvv' |
Test verbosity (v, vv, vvv, vvvv) |
package-manager |
string | 'none' |
Package manager (none, npm, yarn, pnpm) |
node-version |
string | '20' |
Node.js version for package installation |
run-slither |
boolean | false |
Run Slither static analysis |
slither-fail-on |
string | 'high' |
Minimum severity to fail on (low, medium, high) |
slither-config |
string | 'slither.config.json' |
Path to slither.config.json |
run-coverage |
boolean | false |
Run forge coverage and post PR comment |
coverage-exclude-paths |
string | '' |
Path pattern to exclude from coverage (--no-match-path) |
coverage-source-filter |
string | ' src/' |
Grep filter for source files in coverage report |
coverage-post-comment |
boolean | true |
Post coverage summary as a sticky PR comment |
coverage-min-threshold |
number | 0 |
Minimum coverage % to pass (0 = disabled) |
run-halmos |
boolean | false |
Run Halmos symbolic execution |
| Secret | Required | Description |
|---|---|---|
RPC_URL |
No | RPC endpoint for fork-based tests and coverage |
Note: When
run-coverageandcoverage-post-commentare enabled, the calling workflow must havepull-requests: writepermission for the sticky comment to be posted.
| Input | Type | Default | Description |
|---|---|---|---|
package-manager |
string | 'none' |
Package manager (none, npm, yarn, pnpm) |
node-version |
string | '20' |
Node.js version for package installation |
upgrades-config |
string | '.github/upgrades.json' |
Path to upgrade safety config |
base-branch |
string | 'main' |
Base branch for upgrade safety comparison |
| Input | Type | Default | Description |
|---|---|---|---|
deploy-script |
string | 'script/Deploy.s.sol:Deploy' |
Deployment script |
network-config-path |
string | '.github/deploy-networks.json' |
Network config path |
network-index |
number | 0 |
Index in testnets array |
indexing-wait |
number | 60 |
Seconds to wait before verification |
verify-contracts |
boolean | true |
Verify on Blockscout |
package-manager |
string | 'none' |
Package manager (none, npm, yarn, pnpm) |
node-version |
string | '20' |
Node.js version for package installation |
The all-in-one workflow accepts all inputs from the above workflows plus:
| Input | Type | Default | Description |
|---|---|---|---|
skip-if-no-changes |
boolean | true |
Skip if no contract files changed |
contract-paths |
string | src/**, script/**, etc. |
Paths to watch for changes |
main-branch |
string | 'main' |
Base branch for upgrade safety comparison |
deploy-on-pr |
boolean | false |
Deploy to testnet on PR |