On the wave of CI/CD for Web3

Jan Kalivoda
Ackee Blockchain
Published in
5 min readDec 7, 2023

So you want to write and deploy a smart contract, huh?

Go for it and do it as a professional, use the best practices common for the best software out there. There is no need to reinvent the wheel and change processes that work. Sometimes we just need to adjust the tooling — and that’s what this article is about.

Let’s set up this environment for a sample project from scratch.

Setting up the repository

Starting with two repositories is considered to be a good practice. One is public for the main activity, and the second is private and also holds the codebase because some activity on the project should not be discussed publicly, for example, live vulnerabilities/security patches that could affect users. This approach helps us to avoid hackers who could exploit a disclosed bug in pull requests before it is fixed for the released code.

To avoid complicating this tutorial, we will stick only to a public repository (on GitHub).

Let’s start going step by step through repository settings and consider changing crucial parameters, such as branch protection rules or access controls for the team.

Initialize the project

Clone your empty repository and enter it. In the repository folder, initialize the template for your project with:

wake init

This command will prepare all needed folders, including the .gitignore file.

Write the code

Now, we are ready to write the code. Let’s start with the following one generated by OpenZeppelin Solidity Wizard with a few changes (bugs).

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract MyToken is ERC20 {
address public owner;

modifier onlyOwner {
require(tx.origin == owner, "Not an owner");
_;
}

constructor(address initialOwner) ERC20("MyToken", "MTK") {
owner = initialOwner;
}

function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
}

This code includes some packages, so we must add them with npm (don’t forget to add node_modules to .gitignore).

npm i @openzeppelin/contracts

When we have installed the packages, we can test the compilation.

wake compile

In the current state, we have a working project with the following structure.

Write tests

Firstly, we need to initialize pytypes against the source code:

wake init pytypes

Then we will write some basic test:

from wake.testing import *
from pytypes.contracts.MyToken import MyToken

@default_chain.connect()
def test_default():
owner = default_chain.accounts[0]
mtk = MyToken.deploy(owner)
mtk.mint(owner, 1000, from_=owner)
assert mtk.balanceOf(owner) == 1000

And we can run the tests with:

wake test

Write deployment scripts

We will be deploying our contract to Holesky testnet. Writing deployment scripts is very similar to writing tests.

from wake.deployment import *
from dotenv import load_dotenv
from pytypes.contracts.MyToken import MyToken

import os

load_dotenv()

NODE_URL = "https://ethereum-holesky.publicnode.com"
PRIVATE_KEY = os.getenv('PRIVATE_KEY')

@default_chain.connect(NODE_URL)
def main():
owner = Account.from_key(PRIVATE_KEY)
default_chain.set_default_accounts(owner)
mtk = MyToken.deploy(owner)
print(mtk)

Before deploying, we need to set the private key in .env file and retrieve it using dotenv library, and then we can test deployment.

Everything works as expected: tests, deployment, and compilation, so we can proceed to writing the pipeline to have these steps automated.

Write pipeline

We will start with creating a folder for our pipeline:

mkdir -p .github/workflows/

Then, in this folder, we will create the following file: pipeline.yml.

name: CI
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]

jobs:
test_and_analyze:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- name: Setup Wake
uses: Ackee-Blockchain/wake-setup-action@0.1.0
- name: Install Dependencies
run: npm ci
- name: Generate pytypes
run: wake init pytypes
- name: Run tests
run: |
wake test
- name: Run static analysis
uses: Ackee-Blockchain/wake-detect-action@0.1.0
with:
export-sarif: true
id: wake-detect
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: ${{ steps.wake-detect.outputs.sarif }}
checkout_path: ${{ github.workspace }}

deploy:
if: github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Wake
uses: Ackee-Blockchain/wake-setup-action@0.1.0
- name: Install Dependencies
run: npm ci
- name: Generate pytypes
run: wake init pytypes
- name: Prepare .env
run: |
pip3 install python-dotenv
echo "PRIVATE_KEY=${{ secrets.PRIVATE_KEY }}" > .env
- name: Deploy
run: |
wake run --no-ask scripts/deploy.py

We have two jobs. One is for pull requests to master testing and analyzing the new code that will be merged. The second job is for deployment, and it’s triggered only on merge/push to master. We are using Wake Setup action to set up the environment for tests and deployment. Then, we are using specialized Wake Detect action for code scanning for vulnerabilities. In the deployment part, we must pass a private key with dotenv via GitHub secrets. That secret has to be set in repository settings.

Once we set a secret, we are good to go.

Deploy

Everything is set. We can check out to another branch and push our codebase. After push, nothing is triggered because we don’t have a pipeline defined for this behavior. That’s expected.

After creating a pull request, we can see the pipeline is triggered (skipping deploy).

Apparently, since we didn’t run static analysis before, we can see the pipeline detected bugs and attached them to our pull request.

So, with our repository policies, we may not be able to merge to master until it is fully resolved.

We go back to the code to fix it. Remove unused import and replace tx.origin with msg.sender.

Now we can see we are ready to deploy.

Let’s merge it!

We successfully deployed the contract via GitHub actions.

Final remarks

This tutorial explained how to use GitHub actions to enhance your CI/CD processes.

These actions will help you make your project more durable and efficient. The provided example is purely informational, and now it’s on you to find the best match for your project.

Originally published at https://ackeeblockchain.com on December 7, 2023.

--

--