Skip to main content
Learn how to configure a GitHub Action to set up a Continuous Integration and Continuous Deployment pipeline (CI/CD) with Hardhat on Tenderly Virtual Environments. Use the Tenderly Virtual TestNet Setup to enable automated builds that test and deploy your contracts across multiple networks. After successful testing, you can stage them for the rest of your team by deploying them to provisioned Virtual Environments. In this guide, you need to complete two stages:
  1. Local setup: Set up Hardhat and create and test the workflow file.
  2. Github setup: Set up a GitHub Action, configure GitHub secrets and variables, and test the build.
For reference, use this example project:

CI/CD setup with hardhat-ignition

This guide demonstrates a CI setup that relies on Hardhat-ignition. For hardhat-verify setup, the process and configuration are similar.
Continuous integration (CI) and continous deployment (CD) with Virtual Environments

Stage 1: Local setup

First, we’ll install necessary dependencies and create a workflow file.
1
Install dependencies
2
Make sure you have the following packages installed:
3
npm install --save-dev @tenderly/hardhat-tenderly @nomicfoundation/hardhat-ignition
4
Create the workflow file
5
To set up the GitHub Action, create a new workflow:
6
mkdir -p .github/workflows
touch ci-cd.yaml
7
Next, paste the following yaml file that configures two jobs:
8
  • test job to run hardhat tests using a single network for testing
  • deploy job to deploy contracts to multiple networks (Mainnet and Base) after successful testing
  • 9
    The mode argument takes values CI and CD.
    • The CD mode keeps the Virtual Environment active and you can work with deployed contracts.
    • The CI mode pauses the Virtual Environment after the step completes. You’ll be able to inspect transactions but won’t be able to send further RPC requests.
    10
    name: Hardhat CI/CD Multichain
    
    on: [push, pull_request]
    env:
      ## Needed available as env variables for hardhat.config.js
      TENDERLY_PROJECT_NAME: ${{ vars.TENDERLY_PROJECT_NAME }}
      TENDERLY_ACCOUNT_NAME: ${{ vars.TENDERLY_ACCOUNT_NAME }}
    jobs:
      test:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
    
          - name: Setup Node.js
            uses: actions/setup-node@v4
            with:
              node-version: '20'
              cache: 'npm'
    
          - name: Setup Virtual Environment
            uses: tenderly/vnet-github-action@v1.0.14
            with:
              mode: CI    # pauses the Virtual Environment after deployment
              access_key: ${{ secrets.TENDERLY_ACCESS_KEY }}
              project_name: ${{ vars.TENDERLY_PROJECT_NAME }}
              account_name: ${{ vars.TENDERLY_ACCOUNT_NAME }}
              testnet_name: "Testing"
              network_id: 1
              chain_id_prefix: 7357
              public_explorer: true
              verification_visibility: 'src'
              push_on_complete: true
    
          - name: Install dependencies
            run: npm install
            working-directory: examples/hardhat-ignition
    
          - name: Run Tests
            run: npm run test:1
            working-directory: examples/hardhat-ignition
    
      deploy:
        needs: test
        runs-on: ubuntu-latest
        permissions:
          contents: write
        steps:
          - uses: actions/checkout@v4
    
          - name: Setup Node.js
            uses: actions/setup-node@v4
            with:
              node-version: '20'
              cache: 'npm'
    
          - name: Setup Virtual Environment
            uses: tenderly/vnet-github-action@v1.0.14
            with:
              mode: CD
              access_key: ${{ secrets.TENDERLY_ACCESS_KEY }}
              project_name: ${{ vars.TENDERLY_PROJECT_NAME }}
              account_name: ${{ vars.TENDERLY_ACCOUNT_NAME }}
              testnet_name: "Staging"
              network_id: |
                1
                8453
              chain_id_prefix: ""
              public_explorer: true
              verification_visibility: 'src'
              push_on_complete: true
    
          - name: Install dependencies
            run: npm install
            working-directory: examples/hardhat-ignition
    
          - name: Deploy Contracts Mainnet
            run: npm run deploy:1 -- --deployment-id deploy-1-${BUILD_SLUG}
            working-directory: examples/hardhat-ignition
    
          - name: Deploy Contracts Base
            run: npm run deploy:8453 -- --deployment-id deploy-8453-${BUILD_SLUG}
            working-directory: examples/hardhat-ignition
    
    11
    Prepare environment variables
    12
    For local testing, set up an .env file using this template:
    13
    ## Access parameters
    TENDERLY_ACCESS_KEY=...
    TENDERLY_PROJECT_NAME=...
    TENDERLY_ACCOUNT_NAME=...
    
    ## Network-specific parameters (populated by the action in CI/CD)
    TENDERLY_ADMIN_RPC_URL_1=...    # Mainnet RPC
    TENDERLY_ADMIN_RPC_URL_8453=... # Base RPC
    TENDERLY_CHAIN_ID_1=...         # Mainnet chain ID
    TENDERLY_CHAIN_ID_8453=...      # Base chain ID
    
    14
    Required variables:
    15
  • TENDERLY_ACCESS_KEY: Learn how to get your access key
  • TENDERLY_ACCOUNT_NAME and TENDERLY_PROJECT_NAME: Follow steps to get your account and project name
  • 16
    Configure Hardhat
    17
    Add the Virtual Environment configuration to hardhat.config.ts:
    18
    
    dotenv.config();
    
    const config: HardhatUserConfig = {
      solidity: "0.8.27",
      networks: {
        mainnet: {
          url: process.env.TENDERLY_ADMIN_RPC_URL_1,
          chainId: parseInt(process.env.TENDERLY_CHAIN_ID_1 || "1")
        },
        base: {
          url: process.env.TENDERLY_ADMIN_RPC_URL_8453,
          chainId: parseInt(process.env.TENDERLY_CHAIN_ID_8453 || "8453")
        }
      },
      etherscan: {
        apiKey: {
          mainnet: process.env.TENDERLY_ACCESS_KEY!,
          base: process.env.TENDERLY_ACCESS_KEY!
        },
        customChains: [
          {
            network: "mainnet",
            chainId: parseInt(process.env.TENDERLY_CHAIN_ID_1!),
            urls: {
              apiURL: `${process.env.TENDERLY_ADMIN_RPC_URL_1}/verify`,
              browserURL: process.env.TENDERLY_ADMIN_RPC_URL_1!
            }
          },
          {
            network: "base",
            chainId: parseInt(process.env.TENDERLY_CHAIN_ID_8453!),
            urls: {
              apiURL: `${process.env.TENDERLY_ADMIN_RPC_URL_8453}/verify`,
              browserURL: process.env.TENDERLY_ADMIN_RPC_URL_8453!
            }
          }
        ]
      },
      tenderly: {
        project: process.env.TENDERLY_PROJECT_NAME!,
        username: process.env.TENDERLY_ACCOUNT_NAME!,
        accessKey: process.env.TENDERLY_ACCESS_KEY!
      },
      sourcify: {
        enabled: false
      }
    };
    
    export default config;
    
    19
    Add test and deploy scripts
    20
    Add the following scripts to your package.json for multi-network testing and deployment:
    21
    {
      "scripts": {
        "deploy:1": "npx hardhat ignition deploy ./ignition/modules/Counter.js --network mainnet",
        "deploy:8453": "npx hardhat ignition deploy ./ignition/modules/Counter.js --network base",
        "test:1": "npx hardhat test --network mainnet"
      }
    }
    
    22
    The deploy commands use the BUILD_SLUG environment variable (provided by the action) to create unique deployment identifiers.
    23
    Test the build locally
    24
    To test the build locally, run the following commands:
    25
    # Test on mainnet fork
    npx hardhat test:1
    
    # Deploy to mainnet fork
    npx hardhat deploy:1
    
    # Deploy to Base fork
    npx hardhat deploy:8453
    
    26
    Test the Github Action locally
    27
    To test your action locally, you can use Act:
    28
    act --secret-file .env --var-file .env
    

    Stage 2: Github Setup

    After successful local setup, proceed by configuring Github with environment variables and necessary secrets.
    1
    Configure secrets and environment variables in GitHub
    2
    You must configure the following Github variables and secret:
    3
    Variables:
    4
  • TENDERLY_PROJECT_NAME
  • TENDERLY_ACCOUNT_NAME
  • 5
    Secrets:
    6
  • TENDERLY_ACCESS_KEY
  • 7
    You can configure these via the GitHub UI or using the gh command line:
    8
    source .env
    
    gh variable set TENDERLY_PROJECT_NAME --body ${TENDERLY_PROJECT_NAME}
    gh variable set TENDERLY_ACCOUNT_NAME --body ${TENDERLY_ACCOUNT_NAME}
    gh secret set TENDERLY_ACCESS_KEY --body ${TENDERLY_ACCESS_KEY}
    
    9
    Push the updates
    10
    To trigger the GitHub Action, push your changes:
    11
    git add .
    git commit -m "Add CI/CD workflow for multichain deployment"
    git push
    
    12
    Check your action
    13
    Go to your Github repository and check the Actions tab. You should see:
    14
  • Test job running against the mainnet fork
  • Deploy job creating two Virtual Environments (mainnet and Base)
  • Deployment artifacts being pushed to the repository
  • 16
    To get the RPC links for your deployed contracts:
    17
  • Go to Tenderly dashboard > Virtual Environments
  • Find your CD Virtual Environments (one for mainnet fork, one for Base fork)
  • Click on each to get their respective RPC URLs
  • Environment Variables Reference

    The GitHub Action exposes the following network-specific variables:
    VariableDescription
    TENDERLY_TESTNET_ID_{network_id}Virtual Environment UUID
    TENDERLY_ADMIN_RPC_URL_{network_id}Admin RPC endpoint with cheatcode methods
    TENDERLY_PUBLIC_RPC_URL_{network_id}Public RPC endpoint
    TENDERLY_CHAIN_ID_{network_id}Chain ID
    TENDERLY_TESTNET_SLUG_{network_id}Unique Virtual Environment slug
    BUILD_SLUGUnique identifier for the current build

    Next steps

    Explore other examples: