Virtual TestNets
CI/CD
Hardhat & Github Actions

Setting up Github Actions with Hardhat

Learn how to configure a GitHub Action to set up a Continuous Integration and Continuous Deployment pipeline (CI/CD) with Hardhat on Tenderly Virtual TestNets.

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 TestNets.

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:

This guide demonstrates a CI setup that relies on Hardhat-ignition. For hardhat-verify setup, the process and configuration are similar.

Tenderly Docs
Continuous integration (CI) and continous deployment (CD) with Virtual TestNets

Stage 1: Local setup

First, we’ll install necessary dependencies and create a workflow file.

Install dependencies

Make sure you have the following packages installed:

npm install --save-dev @tenderly/hardhat-tenderly @nomicfoundation/hardhat-ignition

Create the workflow file

To set up the GitHub Action, create a new workflow:

mkdir -p .github/workflows
touch ci-cd.yaml

Next, paste the following yaml file that configures two jobs:

  • 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

The mode argument takes values CI and CD.

  • The CD mode keeps the Virtual TestNet active and you can work with deployed contracts.
  • The CI mode pauses the Virtual TestNet after the step completes. You’ll be able to inspect transactions but won’t be able to send further RPC requests.
ci-cd.yaml
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 TestNet
        uses: tenderly/vnet-github-action@v1.0.14
        with:
          mode: CI    # pauses testnet 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 TestNet
        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

Prepare environment variables

For local testing, set up an .env file using this template:

## 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

Required variables:

Configure Hardhat

Add the Virtual TestNet configuration to hardhat.config.ts:

import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
import "@tenderly/hardhat-tenderly";
import "@nomicfoundation/hardhat-ignition";
 
import * as dotenv from 'dotenv';
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/etherscan`,
          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/etherscan`,
          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;

Add test and deploy scripts

Add the following scripts to your package.json for multi-network testing and deployment:

{
  "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"
  }
}

The deploy commands use the BUILD_SLUG environment variable (provided by the action) to create unique deployment identifiers.

Test the build locally

To test the build locally, run the following commands:

# Test on mainnet fork
npx hardhat test:1
 
# Deploy to mainnet fork
npx hardhat deploy:1
 
# Deploy to Base fork
npx hardhat deploy:8453

Test the Github Action locally

To test your action locally, you can use Act:

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.

Configure secrets and environment variables in GitHub

You must configure the following Github variables and secret:

Variables:

  • TENDERLY_PROJECT_NAME
  • TENDERLY_ACCOUNT_NAME

Secrets:

  • TENDERLY_ACCESS_KEY

You can configure these via the GitHub UI or using the gh command line:

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}

Push the updates

To trigger the GitHub Action, push your changes:

git add .
git commit -m "Add CI/CD workflow for multichain deployment"
git push

Check your action

Go to your Github repository and check the Actions tab. You should see:

  1. Test job running against the mainnet fork
  2. Deploy job creating two Virtual TestNets (mainnet and Base)
  3. Deployment artifacts being pushed to the repository

Get the RPC links

To get the RPC links for your deployed contracts:

  1. Go to Tenderly dashboard > Virtual TestNets
  2. Find your CD Virtual TestNets (one for mainnet fork, one for Base fork)
  3. 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 TestNet 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 TestNet slug
BUILD_SLUGUnique identifier for the current build

Next steps

Explore other examples: