On the wave of CI/CD for Web3
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.