Automating Smart Contract Deployment: CI/CD and Security

When a project grows to include multiple environments (dev, staging, production) and multiple team members, manual deployment is no longer suitable. At this stage, CI/CD becomes the protective layer that standardizes and secures the smart contract release process.

In practice, manual deployment carries many risks: forgetting to verify, deploying to the wrong network, using incorrect configurations, and more. A CI/CD pipeline helps eliminate human errors.

1. High-Level Model

GitLab creates two separate types of pipelines: branch pipelines and tag pipelines. The strategy here is:

  • Branch pipeline (push to default branch): build + test + automatic testnet deployment
  • Tag pipeline (push tag x.x.x): build + test again + allow manual mainnet deployment

Branch Pipeline

git push origin main
        ↓
 [build]
    ├── compile → artifacts/
    ├── test (parallel)
    └── gas-report (allow_failure)
        ↓
 [deploy-testnet]
    └── deploy-sepolia (auto)
        → deploy + verify on Sepolia

Tag Pipeline

git tag 1.0.0 && git push origin 1.0.0
        ↓
 [build]
    ├── compile
    ├── test
    └── gas-report
        ↓
 [deploy-mainnet] (manual)
    → click “Run” in GitLab UI
    → deploy + verify on Mainnet

Why doesn’t deploy-mainnet depend on deploy-sepolia?
Because they belong to different pipelines. GitLab does not allow jobs in a tag pipeline to depend on jobs from a branch pipeline. Therefore, the tag pipeline runs a full rebuild and retest before allowing mainnet deployment.

2. GitLab CI Pipeline

Create a .gitlab-ci.yml file at the root of the repository.

The pipeline consists of three stages:

  • build
  • deploy-testnet
  • deploy-mainnet

Stage 1 – Build & Test

  • compile: install dependencies and compile contracts
  • test: run Hardhat tests
  • gas-report: generate gas usage report (allowed to fail)

Artifacts from compilation are stored temporarily for deployment.

Stage 2 – Deploy to Testnet

Triggered automatically when pushing to the default branch.

Steps:

  • Deploy to Sepolia
  • Extract contract address
  • Verify contract on Etherscan
  • Store deployment artifacts

Stage 3 – Deploy to Mainnet (Manual)

Triggered only when pushing a version tag matching x.x.x.

  • Requires manual approval (when: manual)
  • Re-runs compile + test
  • Deploys to mainnet
  • Verifies contract
  • Stores deployment artifacts

Mainnet Deployment Process

1. Merge code into default branch → automatic testnet deployment runs

2. Verify contract behavior on Sepolia

3. Create and push version tag

    git tag 1.0.0
    git push origin 1.0.0

    4. Go to GitLab UI → open tag pipeline → click “Run” on deploy-mainnet

      3. Security in CI/CD

      Important considerations:

      • Store private keys in GitLab CI/CD Variables (Masked + Protected enabled). Never hardcode.
      • Mark sensitive variables as Protected so they are only injected into protected branches or tags.
      • when: manual on mainnet deploy adds an additional confirmation layer.
      • Consider using a multi-sig wallet (e.g., Gnosis Safe) as contract owner instead of a single EOA.
      • Use separate private keys for testnet and mainnet.

      Leave a Comment

      Your email address will not be published. Required fields are marked *