Saltar al contenido
mypipelines
Contracts (Hardhat/Solidity)·Reusable workflow·on: workflow_call

Contracts Analysis

Contracts - Static Analysis

.github/workflows/contracts-analysis.yml

.github/workflows/contracts-analysis.yml
name: Contracts - Static Analysis
# Tests prove the contract does what its suite imagined. Static analysis is the
# other half: reentrancy, ignored return values, shadowed state, unchecked
# low-level calls — findings a passing suite is perfectly happy to keep.
on:
workflow_call:
inputs:
runner:
description: 'Runner type'
required: false
type: string
default: 'ubuntu-latest'
node_version:
description: 'Node.js version'
required: false
type: string
default: '20'
package_manager:
description: 'Package manager (npm, yarn, or pnpm)'
required: false
type: string
default: 'pnpm'
run_solhint:
description: 'Run solhint'
required: false
type: boolean
default: true
solhint_command:
description: >-
Command that runs solhint. Empty = `solhint contracts/**/*.sol`
through the selected package manager. solhint needs a config file in
the repository; without one it exits non-zero on every run.
required: false
type: string
default: ''
run_slither:
description: 'Run slither'
required: false
type: boolean
default: true
slither_target:
description: 'Path slither analyzes (a Hardhat project root compiles through hardhat)'
required: false
type: string
default: '.'
slither_args:
description: 'Extra arguments passed to slither'
required: false
type: string
default: '--exclude-dependencies'
slither_fail_on:
description: >-
Severity that fails the job: none, low, medium, high. Start at high
on an existing codebase and tighten — a job that fails on day one for
findings nobody has triaged gets switched off, not fixed.
required: false
type: string
default: 'high'
outputs:
solhint_result:
description: 'solhint outcome'
value: ${{ jobs.solhint.outputs.result }}
slither_result:
description: 'slither outcome'
value: ${{ jobs.slither.outputs.result }}
# Least privilege for what this workflow actually does. Without it the
# job inherits whatever the calling repository's default happens to be,
# which on older repositories is write-all.
permissions:
contents: read
jobs:
solhint:
name: Solhint
if: inputs.run_solhint
runs-on: ${{ inputs.runner }}
timeout-minutes: 10
outputs:
result: ${{ steps.lint.outcome }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup pnpm
if: inputs.package_manager == 'pnpm'
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- name: Setup Node.js ${{ inputs.node_version }}
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ inputs.node_version }}
cache: ${{ inputs.package_manager }}
- name: Install dependencies
run: |
case "${{ inputs.package_manager }}" in
pnpm) pnpm install --frozen-lockfile ;;
yarn) yarn install --frozen-lockfile ;;
*) npm ci ;;
esac
- name: Run solhint
id: lint
env:
SOLHINT_COMMAND: ${{ inputs.solhint_command }}
run: |
set -o pipefail
if [ -n "${SOLHINT_COMMAND}" ]; then
eval "${SOLHINT_COMMAND}" 2>&1 | tee solhint-output.log
exit $?
fi
case "${{ inputs.package_manager }}" in
pnpm) pnpm solhint 'contracts/**/*.sol' 2>&1 | tee solhint-output.log ;;
yarn) yarn solhint 'contracts/**/*.sol' 2>&1 | tee solhint-output.log ;;
*) npx solhint 'contracts/**/*.sol' 2>&1 | tee solhint-output.log ;;
esac
- name: Summarize
if: always()
run: |
{
echo "### Solhint"
echo ""
echo "**Result:** ${{ steps.lint.outcome }}"
} >> "$GITHUB_STEP_SUMMARY"
if [ -f solhint-output.log ]; then
PROBLEMS=$(grep -aoE '[0-9]+ problems? \([0-9]+ errors?, [0-9]+ warnings?\)' solhint-output.log | tail -1 || true)
if [ -n "${PROBLEMS}" ]; then
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "${PROBLEMS}" >> "$GITHUB_STEP_SUMMARY"
fi
fi
slither:
name: Slither
if: inputs.run_slither
runs-on: ${{ inputs.runner }}
timeout-minutes: 20
outputs:
result: ${{ steps.analysis.outcome }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# No container: for this job. slither-action brings its own image with
# python, solc and crytic-compile already matched to each other.
- name: Run slither
id: analysis
uses: crytic/slither-action@b52cc1cbfee9ca3e8722dd5224299d16c9a6b80f # v0.4.2
with:
target: ${{ inputs.slither_target }}
slither-args: ${{ inputs.slither_args }}
fail-on: ${{ inputs.slither_fail_on }}
node-version: ${{ inputs.node_version }}
- name: Summarize
if: always()
run: |
{
echo "### Slither"
echo ""
echo "**Result:** ${{ steps.analysis.outcome }} (fail-on: ${{ inputs.slither_fail_on }})"
} >> "$GITHUB_STEP_SUMMARY"