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

Contracts Main Pipeline

Contracts - Main Pipeline

.github/workflows/contracts-main-pipeline.yml

.github/workflows/contracts-main-pipeline.yml
name: Contracts - Main Pipeline
on:
workflow_call:
inputs:
# Runner configuration
runner:
description: 'Runner type'
required: false
type: string
default: 'ubuntu-latest'
# Node / Hardhat configuration
container_image:
description: >-
Run the Node-based jobs inside this image (e.g.
ghcr.io/codehunters/ci-base-images:v1.0.0-node). Empty = plain
runners with actions/setup-node. The slither job always brings its
own image and ignores this.
required: false
type: string
default: ''
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'
pnpm_version:
description: 'pnpm version (used when package_manager: pnpm)'
required: false
type: string
default: '10'
dockerfile_path:
description: 'Path to Dockerfile (build context)'
required: false
type: string
default: '.'
# Pipeline steps control
run_commit_lint:
description: 'Run commit message validation'
required: false
type: boolean
default: false
run_trufflehog:
description: 'Run TruffleHog secret scanning'
required: false
type: boolean
default: false
trufflehog_only_verified:
description: 'Only report verified secrets'
required: false
type: boolean
default: true
trufflehog_fail_on_findings:
description: 'Fail the workflow if secrets are found'
required: false
type: boolean
default: true
run_dependency_review:
description: 'Run dependency review (PR only)'
required: false
type: boolean
default: false
dependency_review_severity:
description: 'Minimum severity to fail: low, moderate, high, critical'
required: false
type: string
default: 'high'
run_build:
description: 'Run build & compile job'
required: false
type: boolean
default: true
run_test:
description: 'Run test job (hardhat test)'
required: false
type: boolean
default: false
test_command:
description: 'Command that runs the contract tests. Empty = hardhat test.'
required: false
type: string
default: ''
run_coverage:
description: 'Run solidity-coverage'
required: false
type: boolean
default: false
coverage_command:
description: >-
Command that produces and may gate coverage. Empty = hardhat
coverage. A non-zero exit from this command fails the job, which is
how a repository's own threshold script becomes the gate.
required: false
type: string
default: ''
coverage_threshold:
description: 'Minimum coverage percentage (0-100). 0 = disabled.'
required: false
type: number
default: 0
run_gas_reporter:
description: 'Enable hardhat-gas-reporter'
required: false
type: boolean
default: false
upload_reports:
description: 'Upload coverage and gas reports as artifacts'
required: false
type: boolean
default: false
run_size_check:
description: 'Run hardhat-contract-sizer'
required: false
type: boolean
default: true
# SDK (a client library shipped from the same repository)
run_sdk:
description: 'Run the SDK build + test job'
required: false
type: boolean
default: false
sdk_build_command:
description: 'Command that compiles the SDK. Empty = skip.'
required: false
type: string
default: ''
sdk_test_command:
description: 'Command that runs the SDK suite (run it with coverage)'
required: false
type: string
default: ''
sdk_coverage_path:
description: 'Directory holding the SDK coverage report'
required: false
type: string
default: 'sdk/coverage'
# End-to-end (local chain + deploy + suite)
run_e2e:
description: 'Run the end-to-end job against a local chain'
required: false
type: boolean
default: false
e2e_build_command:
description: 'Command run before the chain starts (e.g. compile the SDK). Empty = skip.'
required: false
type: string
default: ''
e2e_node_command:
description: 'Command that starts the local chain'
required: false
type: string
default: 'npx hardhat node'
e2e_deploy_command:
description: 'Command that deploys to the local chain'
required: false
type: string
default: ''
e2e_command:
description: 'Command that runs the end-to-end suite'
required: false
type: string
default: ''
e2e_rpc_url:
description: 'RPC endpoint polled before deploying'
required: false
type: string
default: 'http://127.0.0.1:8545'
# Static analysis
run_analysis:
description: 'Run the static analysis job (solhint + slither)'
required: false
type: boolean
default: false
run_solhint:
description: 'Run solhint inside the analysis job'
required: false
type: boolean
default: true
solhint_command:
description: "Command that runs solhint. Empty = solhint 'contracts/**/*.sol'."
required: false
type: string
default: ''
run_slither:
description: 'Run slither inside the analysis job'
required: false
type: boolean
default: true
slither_target:
description: 'Path slither analyzes'
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'
required: false
type: string
default: 'high'
run_artifact:
description: 'Build and push Docker image to ECR'
required: false
type: boolean
default: false
run_deploy:
description: 'Deploy container to EC2 after ECR push'
required: false
type: boolean
default: false
deploy_target:
description: 'Deploy target: ec2 or ec2-vpn'
required: false
type: string
default: 'ec2-vpn'
memory_limit:
description: 'Container memory limit (e.g., 1024m, 2g)'
required: false
type: string
default: '1024m'
memory_reservation:
description: 'Container memory reservation (e.g., 256m, 512m)'
required: false
type: string
default: '512m'
internal_port:
description: 'Container internal port (Hardhat node = 8545)'
required: false
type: string
default: '8545'
extra_volumes:
description: 'Additional docker-compose volume mounts (one per line, leading "- ")'
required: false
type: string
default: ''
verify_vpn_connectivity:
description: 'Ping EC2 host before deploying (deploy_target: ec2-vpn only)'
required: false
type: boolean
default: false
run_create_issue_on_failure:
description: 'Create GitHub issue on pipeline failure'
required: false
type: boolean
default: false
issue_labels:
description: 'Labels for the failure issue (comma-separated)'
required: false
type: string
default: 'bug,pipeline-failure'
run_cleanup:
description: 'Delete merged branch after artifact push'
required: false
type: boolean
default: false
run_release:
description: 'Create release PR after artifact push'
required: false
type: boolean
default: false
# Artifact options
image_tag:
description: 'Docker image tag (empty = commit SHA)'
required: false
type: string
default: ''
docker_platform:
description: 'Docker platform (linux/amd64, linux/arm64)'
required: false
type: string
default: 'linux/arm64'
# Trivy scan of the image before it is pushed
vulnerability_gate:
description: 'Fail the artifact job on a fixable vulnerability at or above scan_severity. false reports only.'
required: false
type: boolean
default: true
scan_severity:
description: 'Trivy severities the vulnerability gate acts on'
required: false
type: string
default: 'CRITICAL,HIGH'
trivyignores:
description: 'Path to a Trivy ignore file. Empty means none.'
required: false
type: string
default: ''
ignore_policy:
description: 'Path to a Trivy Rego ignore policy. Empty means none.'
required: false
type: string
default: ''
push_latest:
description: 'Also push :latest tag to ECR'
required: false
type: boolean
default: false
# Environment for ECR push
environment:
description: 'GitHub environment (develop, staging, production)'
required: false
type: string
default: 'develop'
# Release options
release_target_branch:
description: 'Target branch for release PR'
required: false
type: string
default: 'main'
release_strict_flow:
description: 'Enforce GitFlow on release PR (base must be develop, target must be main)'
required: false
type: boolean
default: true
run_tag:
description: 'Create git tag and GitHub Release after artifact push'
required: false
type: boolean
default: false
aws_role_to_assume:
description: >-
IAM role ARN to assume via GitHub OIDC, threaded down to the AWS jobs
below. Empty keeps the static-key path.
required: false
type: string
default: ''
outputs:
image_tag:
description: 'Docker image tag pushed'
value: ${{ jobs.artifact.outputs.image_tag }}
image_uri:
description: 'Full Docker image URI'
value: ${{ jobs.artifact.outputs.image_uri }}
image_digest:
description: 'Digest of the pushed image'
value: ${{ jobs.artifact.outputs.image_digest }}
coverage_percentage:
description: 'Line coverage percentage'
value: ${{ jobs.test.outputs.coverage_percentage }}
tests_passed:
description: 'Number of tests passed'
value: ${{ jobs.test.outputs.tests_passed }}
tests_failed:
description: 'Number of tests failed'
value: ${{ jobs.test.outputs.tests_failed }}
sdk_result:
description: 'SDK suite result'
value: ${{ jobs.sdk.outputs.result }}
e2e_result:
description: 'End-to-end result'
value: ${{ jobs.e2e.outputs.result }}
deploy_status:
description: 'Deployment status'
value: ${{ jobs.deploy-ec2.outputs.deploy_status || jobs.deploy-ec2-vpn.outputs.deploy_status }}
deleted_branch:
description: 'Name of deleted branch'
value: ${{ jobs.cleanup.outputs.deleted_branch }}
release_version:
description: 'Release version created'
value: ${{ jobs.release.outputs.version }}
release_pr_url:
description: 'Release PR URL'
value: ${{ jobs.release.outputs.pr_url }}
release_changelog:
description: 'Release changelog'
value: ${{ jobs.release.outputs.changelog }}
# 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: write
packages: read
issues: write
# For the OIDC path in the AWS workflows below; a caller's grant is the
# ceiling for everything beneath it.
id-token: write
jobs:
# ============================================
# COMMIT LINT (parallel, no deps)
# ============================================
commit-lint:
name: Commit Lint
if: inputs.run_commit_lint
uses: ./.github/workflows/shared-commit-lint.yml
with:
runner: ${{ inputs.runner }}
# ============================================
# SECURITY - TruffleHog (parallel, no deps)
# ============================================
security:
name: Security Scan
if: inputs.run_trufflehog
uses: ./.github/workflows/security-trufflehog.yml
with:
runner: ${{ inputs.runner }}
only_verified: ${{ inputs.trufflehog_only_verified }}
fail_on_findings: ${{ inputs.trufflehog_fail_on_findings }}
secrets: inherit
# ============================================
# SECURITY - Dependency Review (parallel, no deps)
# ============================================
dependency-review:
name: Dependency Review
if: inputs.run_dependency_review
uses: ./.github/workflows/security-dependency-review.yml
with:
runner: ${{ inputs.runner }}
fail_on_severity: ${{ inputs.dependency_review_severity }}
secrets: inherit
# ============================================
# BUILD (Install + Compile + Size Check)
# ============================================
build:
name: Build & Compile
if: inputs.run_build
uses: ./.github/workflows/contracts-build.yml
with:
runner: ${{ inputs.runner }}
container_image: ${{ inputs.container_image }}
node_version: ${{ inputs.node_version }}
package_manager: ${{ inputs.package_manager }}
pnpm_version: ${{ inputs.pnpm_version }}
run_size_check: ${{ inputs.run_size_check }}
secrets: inherit
# ============================================
# TEST (hardhat test + optional coverage + gas)
# ============================================
test:
name: Test & Coverage
if: inputs.run_test
needs: build
uses: ./.github/workflows/contracts-test.yml
with:
runner: ${{ inputs.runner }}
node_version: ${{ inputs.node_version }}
package_manager: ${{ inputs.package_manager }}
pnpm_version: ${{ inputs.pnpm_version }}
container_image: ${{ inputs.container_image }}
artifact_name: ${{ needs.build.outputs.artifact_name }}
test_command: ${{ inputs.test_command }}
run_coverage: ${{ inputs.run_coverage }}
coverage_command: ${{ inputs.coverage_command }}
coverage_threshold: ${{ inputs.coverage_threshold }}
run_gas_reporter: ${{ inputs.run_gas_reporter }}
upload_reports: ${{ inputs.upload_reports }}
secrets: inherit
# ============================================
# SDK (build + test the client library)
# ============================================
sdk:
name: SDK Build & Test
if: inputs.run_sdk
uses: ./.github/workflows/contracts-sdk-test.yml
with:
runner: ${{ inputs.runner }}
container_image: ${{ inputs.container_image }}
node_version: ${{ inputs.node_version }}
package_manager: ${{ inputs.package_manager }}
build_command: ${{ inputs.sdk_build_command }}
test_command: ${{ inputs.sdk_test_command }}
upload_reports: ${{ inputs.upload_reports }}
coverage_path: ${{ inputs.sdk_coverage_path }}
secrets: inherit
# ============================================
# E2E (local chain + deploy + suite)
# ============================================
e2e:
name: End-to-End
if: inputs.run_e2e
uses: ./.github/workflows/contracts-e2e.yml
with:
runner: ${{ inputs.runner }}
container_image: ${{ inputs.container_image }}
node_version: ${{ inputs.node_version }}
package_manager: ${{ inputs.package_manager }}
build_command: ${{ inputs.e2e_build_command }}
node_command: ${{ inputs.e2e_node_command }}
deploy_command: ${{ inputs.e2e_deploy_command }}
e2e_command: ${{ inputs.e2e_command }}
rpc_url: ${{ inputs.e2e_rpc_url }}
secrets: inherit
# ============================================
# STATIC ANALYSIS (solhint + slither)
# ============================================
analysis:
name: Static Analysis
if: inputs.run_analysis
uses: ./.github/workflows/contracts-analysis.yml
with:
runner: ${{ inputs.runner }}
node_version: ${{ inputs.node_version }}
package_manager: ${{ inputs.package_manager }}
run_solhint: ${{ inputs.run_solhint }}
solhint_command: ${{ inputs.solhint_command }}
run_slither: ${{ inputs.run_slither }}
slither_target: ${{ inputs.slither_target }}
slither_args: ${{ inputs.slither_args }}
slither_fail_on: ${{ inputs.slither_fail_on }}
secrets: inherit
# ============================================
# ARTIFACT - Build Docker & Push to ECR
# ============================================
artifact:
name: Build & Push ECR
if: |
inputs.run_artifact &&
always() &&
(needs.build.result == 'success' || needs.build.result == 'skipped') &&
(needs.test.result == 'success' || needs.test.result == 'skipped') &&
(needs.sdk.result == 'success' || needs.sdk.result == 'skipped') &&
(needs.e2e.result == 'success' || needs.e2e.result == 'skipped') &&
(needs.analysis.result == 'success' || needs.analysis.result == 'skipped')
needs: [build, test, sdk, e2e, analysis]
uses: ./.github/workflows/shared-artifact-docker-ecr.yml
with:
aws_role_to_assume: ${{ inputs.aws_role_to_assume }}
runner: ${{ inputs.runner }}
image_tag: ${{ inputs.image_tag }}
docker_platform: ${{ inputs.docker_platform }}
vulnerability_gate: ${{ inputs.vulnerability_gate }}
scan_severity: ${{ inputs.scan_severity }}
trivyignores: ${{ inputs.trivyignores }}
ignore_policy: ${{ inputs.ignore_policy }}
dockerfile_path: ${{ inputs.dockerfile_path }}
environment: ${{ inputs.environment }}
push_latest: ${{ inputs.push_latest }}
build_from_source: false
secrets: inherit
# ============================================
# DEPLOY - EC2 (direct)
# ============================================
deploy-ec2:
name: Deploy EC2
if: |
inputs.run_deploy &&
inputs.deploy_target == 'ec2' &&
always() &&
needs.artifact.result == 'success'
needs: [artifact]
uses: ./.github/workflows/shared-deploy-ec2.yml
with:
runner: ${{ inputs.runner }}
image_tag: ${{ needs.artifact.outputs.image_tag }}
image_digest: ${{ needs.artifact.outputs.image_digest }}
docker_platform: ${{ inputs.docker_platform }}
environment: ${{ inputs.environment }}
memory_limit: ${{ inputs.memory_limit }}
memory_reservation: ${{ inputs.memory_reservation }}
internal_port: ${{ inputs.internal_port }}
extra_volumes: ${{ inputs.extra_volumes }}
secrets: inherit
# ============================================
# DEPLOY - EC2 via WireGuard VPN
# ============================================
deploy-ec2-vpn:
name: Deploy EC2 (VPN)
if: |
inputs.run_deploy &&
inputs.deploy_target == 'ec2-vpn' &&
always() &&
needs.artifact.result == 'success'
needs: [artifact]
uses: ./.github/workflows/shared-deploy-ec2-vpn.yml
with:
runner: ${{ inputs.runner }}
image_tag: ${{ needs.artifact.outputs.image_tag }}
image_digest: ${{ needs.artifact.outputs.image_digest }}
docker_platform: ${{ inputs.docker_platform }}
environment: ${{ inputs.environment }}
memory_limit: ${{ inputs.memory_limit }}
memory_reservation: ${{ inputs.memory_reservation }}
internal_port: ${{ inputs.internal_port }}
extra_volumes: ${{ inputs.extra_volumes }}
verify_vpn_connectivity: ${{ inputs.verify_vpn_connectivity }}
secrets: inherit
# ============================================
# RELEASE - Create release PR
# ============================================
release:
name: Create Release
if: |
inputs.run_release &&
always() &&
(needs.deploy-ec2.result == 'success' || needs.deploy-ec2-vpn.result == 'success' || needs.artifact.result == 'success')
needs: [artifact, deploy-ec2, deploy-ec2-vpn]
uses: ./.github/workflows/shared-release.yml
with:
base_branch: ${{ github.ref_name }}
target_branch: ${{ inputs.release_target_branch }}
strict_flow: ${{ inputs.release_strict_flow }}
secrets: inherit
# ============================================
# CLEANUP - Delete merged branch
# ============================================
cleanup:
name: Delete Branch
if: |
inputs.run_cleanup &&
always() &&
(needs.deploy-ec2.result == 'success' || needs.deploy-ec2-vpn.result == 'success' || needs.artifact.result == 'success') &&
(needs.release.result == 'success' || needs.release.result == 'skipped')
needs: [artifact, deploy-ec2, deploy-ec2-vpn, release]
uses: ./.github/workflows/shared-delete-branch.yml
secrets: inherit
# ============================================
# TAG - Create git tag and GitHub Release
# ============================================
tag:
name: Tag Release
if: |
inputs.run_tag &&
always() &&
(needs.deploy-ec2.result == 'success' || needs.deploy-ec2-vpn.result == 'success' || needs.artifact.result == 'success')
needs: [build, test, artifact, deploy-ec2, deploy-ec2-vpn, cleanup, release]
uses: ./.github/workflows/shared-tag-release.yml
secrets: inherit
# ============================================
# NOTIFY - Slack notification (final, auto-derived)
# ============================================
notify:
name: Notify
if: always()
needs: [commit-lint, security, dependency-review, build, test, artifact, deploy-ec2, deploy-ec2-vpn, release, cleanup, tag]
uses: ./.github/workflows/shared-notifications.yml
with:
status: ${{ contains(needs.*.result, 'failure') && 'failure' || contains(needs.*.result, 'cancelled') && 'cancelled' || 'success' }}
environment: ${{ inputs.environment }}
secrets: inherit
# ============================================
# CREATE ISSUE - On failure (runs last, always)
# ============================================
create-issue:
name: Create Issue
if: |
inputs.run_create_issue_on_failure &&
always()
needs: [build, test, artifact, deploy-ec2, deploy-ec2-vpn, cleanup, release, tag]
uses: ./.github/workflows/shared-create-issue-on-failure.yml
with:
status: ${{ (needs.build.result == 'failure' || needs.test.result == 'failure' || needs.artifact.result == 'failure' || needs.deploy-ec2.result == 'failure' || needs.deploy-ec2-vpn.result == 'failure') && 'failure' || 'success' }}
environment: ${{ inputs.environment }}
version: ${{ needs.release.outputs.version }}
changelog: ${{ needs.release.outputs.changelog }}
labels: ${{ inputs.issue_labels }}