Web3 Devops with Azure Devops pipeline
注:开发环境和开发工具的内容是基于我之前发布的几篇文章,SmartContract的测试代码与之前稍有不同 另:还有一些内容没有写完,后续会慢慢补充
整个技术栈涉及的工具和技术比较多,所以先拉个列表:
名称 | 类型 | 地址 |
---|---|---|
Ubuntu 22.04 LTS | 操作系统 | https://releases.ubuntu.com/22.04/ |
Docker | 开发环境 | https://docs.docker.com/engine/install/ubuntu/ |
VSCode | 开发工具 | https://code.visualstudio.com/ |
Goerli PoW Faucet | 以太坊测试网水龙头 | https://goerli-faucet.pk910.de/ |
Infura | 以太坊测试网 API Gateway | https://infura.io/ |
Solidity | 编写合约语言 | https://docs.soliditylang.org/en/v0.8.16/ |
Truffle | 开发合约的npm toolkit | https://trufflesuite.com/ |
Golang | 创建个人地址和发布合约 | https://goethereumbook.org/ |
React | Dapp前端框架 | https://reactjs.org/ |
Git | 版本管理工具 | https://git-scm.com/ |
Azure | 微软公有云平台 | https://azure.microsoft.com/zh-cn/ |
Azure Devops | 微软公有云开发运维平台 | https://azure.microsoft.com/en-us/services/devops/ |
Base Image 是微软打包的开发镜像,有很多个语言版本,可以直接通过docker hub下载。我为了开发方便,基于node镜像又封装了一个镜像,加入了一些基础包。
FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:latest as base
RUN apt-get update && \
apt-get install --no-install-recommends -y \
build-essential \
curl && \
rm -rf /var/lib/apt/lists/* && \
rm -rf /etc/apt/sources.list.d/*
RUN mkdir -p /home/app
WORKDIR /home/app
RUN npm install --global web3 ethereumjs-testrpc ganache-cli truffle
VSCode安装完成之后,需要安装VSCode Remote插件。在插件搜索框中搜索remote,就可以看到Remote三件套:SSH、Containers、WSL。SSH和Containers就不多解释了,WSL是Windows Subsystem for Linux,如果操作系统是windows11可以直接开启WSL,通过windows docker desktop在WSL里启用docker,效果是完全一样的
安装完插件之后,就可以看到romote图标,点击进去后切换到containers就可以看到运行中的镜像了,选中后鼠标右键Attach到镜像,就会开启一个新的vscode。这样整个开发环境就准备完成了
测试项目一共两个部分,SmartContract和Dapp。
SmartContract的测试账户可以提前创建,并通过 Goerli PoW Faucet 来获取测试用的ETH
package main
import (
"context"
"crypto/ecdsa"
"encoding/hex"
"fmt"
"log"
"math"
"math/big"
"os"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/params"
"golang.org/x/crypto/sha3"
)
func main() {
client := InitClient()
CreateAccountNewWallets(client)
}
func InitClient() *ethclient.Client {
client, err := ethclient.Dial("https://goerli.infura.io/v3/YourApiKey")
if err != nil {
log.Fatal(err)
}
fmt.Println("we have a connection")
return client
}
func CreateAccountNewWallets(client *ethclient.Client) {
privateKey, err := crypto.GenerateKey()
if err != nil {
log.Fatal(err)
}
privateKeyBytes := crypto.FromECDSA(privateKey)
fmt.Println("privateKey")
fmt.Println(hexutil.Encode(privateKeyBytes)[2:])
publicKey := privateKey.Public()
publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
if !ok {
log.Fatal("error casting public key to ECDSA")
}
publicKeyBytes := crypto.FromECDSAPub(publicKeyECDSA)
fmt.Println("publicKeyBytes")
fmt.Println(hexutil.Encode(publicKeyBytes)[4:])
address := crypto.PubkeyToAddress(*publicKeyECDSA).Hex()
fmt.Println("address")
fmt.Println(address)
hash := sha3.New512()
hash.Write(publicKeyBytes[1:])
fmt.Println(hexutil.Encode(hash.Sum(nil)[12:]))
}
SmartContract 是用solidity编写的,并非标准代码,仅用于测试
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;
import "../node_modules/@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "../node_modules/@openzeppelin/contracts/access/Ownable.sol";
import "../node_modules/@openzeppelin/contracts/utils/Counters.sol";
import "../node_modules/@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "../node_modules/@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
contract TestNFT is ERC721, ERC721Enumerable, ERC721URIStorage, Ownable{
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
IERC721Enumerable public whitelistedNftContract;
event Minted(address indexed minter, uint nftID, string uri);
constructor() ERC721("TestNFT", "NFT"){}
function mintNFT(string memory _uri, address _toAddress) public onlyOwner returns (uint256) {
uint256 newItemId = _tokenIds.current();
_mint(_toAddress, newItemId);
_setTokenURI(newItemId, _uri);
_tokenIds.increment();
emit Minted(_toAddress, newItemId, _uri);
return newItemId;
}
function _burn(uint256 tokenId) internal override(ERC721, ERC721URIStorage) {
super._burn(tokenId);
}
function tokenURI(uint256 tokenId) public view override(ERC721, ERC721URIStorage) returns (string memory){
return super.tokenURI(tokenId);
}
function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal override(ERC721, ERC721Enumerable){
super._beforeTokenTransfer(from, to, tokenId);
}
function supportsInterface(bytes4 interfaceId) public view override(ERC721, ERC721Enumerable) returns (bool){
return super.supportsInterface(interfaceId);
}
}
SmartContract的测试代码
const { web3 } = require("@openzeppelin/test-environment");
const { expect } = require("chai");
const { BigNumber } = require("bignumber.js");
const TestNFTContract = artifacts.require("TestNFT");
contract("TestNFT", (accounts) => {
describe("testnft", () => {
beforeEach(async () => {
this.contract = await TestNFTContract.new({ from: accounts[0] });
});
it("It should mint NFT successfully", async () => {
const tokenURI = "ipfs://QmXzG9HN7Z4kFE2yHF81Vjb2xDYu53tqhRciktrt15JpAN";
const mintResult = await this.contract.mintNFT(
tokenURI,
accounts[0],
{ from: accounts[0] }
);
console.log(mintResult);
expect(mintResult.logs[1].args.nftID.toNumber()).to.eq(0);
expect(mintResult.logs[1].args.uri).to.eq(tokenURI);
expect(mintResult.logs[1].args.minter).to.eq(accounts[0]);
});
});
describe("owner()", () => {
it("returns the address of the owner", async () => {
const testntf = await TestNFTContract.deployed();
const owner = await testntf.owner();
assert(owner, "the current owner");
});
it("matches the address that originally deployed the contract", async () => {
const testntf = await TestNFTContract.deployed();
const owner = await testntf.owner();
const expected = accounts[0];
assert.equal(owner, expected, "matches address used to deploy contract");
});
});
});
SmartContract编译与测试 在封装开发镜像的时候,就已经安装了开发智能合约的工具包truffle,以下是truffle配置文件
require("dotenv").config();
const path = require("path");
const HDWalletProvider = require("@truffle/hdwallet-provider");
const mnemonic = process.env.MNEMONIC;
module.exports = {
contracts_build_directory: path.join(__dirname,"build/contracts"),
networks: {
development: {
//goerli测试网API网关,可以在Infura中免费注册使用
provider: () => new HDWalletProvider(mnemonic, `https://goerli.infura.io/v3/yourapikey`),
network_id: "5", // Any network (default: none)
},
},
// Set default mocha options here, use special reporters, etc.
mocha: {
reporter: 'xunit',
reporterOptions: {
output: 'TEST-results.xml'
}
},
// Configure your compilers
compilers: {
solc: {
version: "0.8.14", // Fetch exact version from solc-bin (default: truffle's version)
}
},
};
准备好之后就可以开始合约的编译和测试了
//编译合约
truffle compile
//测试合约
truffle test
//部署合约
truffle migrate
现在我们可以开始在Azure Devops上创建 Pipeline了。Dapp的部分后续再更新
在Azure Devops中创建新的项目,Version Control
选择Git,
创建好项目之后,在Repos/Files中找到repository
的地址,点击Generate GIt Credentials
生成Password。之后在本地设置Git连接到这个远程库
使用Git初始化项目并推送到Remote Repository,使用上一步生成的密码,也可以使用SSH
git init
git config --global user.email "YourEmail@email.com"
git config --global user.name "YourName"
git add .
git commit -m "init project & add README file"
git remote add origin https://YourRemoteRepositoryAddressForHTTPS
git push -u origin --all
将代码推送到 GitHub 后,导航到 Azure DevOps Pipelines
页面,然后单击 Create Pipeline
按钮
在 Where is your code?
时选择Azure Repos Git
。之后选择存放代码的Repo,然后选择 Starter pipeline
。
Azure Pipelines 可以由Stages、Jobs和Steps组成。在开始之前需要布置pipeline的Stages和Jobs。定义Stages和Jobs之间的依赖关系并查看整个pipeline。
初始结构
使用web editor更新代码以定义管道结构。整个pipeline有六个阶段 1、build:编译、测试和打包工件 2、dev:部署基础设施、合约和前端 3、dev_validation:等待手动验证dev并删除dev环境 4、qa:部署基础设施、合约和前端 5、qa_validation 等待手动验证 qa 并删除 qa 环境 6、prod:部署基础设施、合约和前端
在第一部分中,加入了开发环境的部署、合约的编译和测试,以及测试结果的输出
# Starter pipeline
# Start with a minimal pipeline that you can customize to build and deploy your code.
# Add steps that build, run tests, deploy, and more:
# https://aka.ms/yaml
trigger:
- main
pool:
vmImage: ubuntu-latest
stages:
- stage: build
jobs:
- job: compile_test
steps:
- script: npm install --global web3 ethereumjs-testrpc ganache-cli truffle
displayName: "install npm global package"
- script: cd ./contracts3 && npm install
displayName: "Install npm project package"
- script: cd ./contracts3 && truffle compile
displayName: "Compile contracts"
- script: cd ./contracts3 && truffle test
displayName: "Test contracts"
- task: PublishTestResults@2
displayName: "Publish contract test results"
inputs:
testRunTitle: "Contract"
testResultsFormat: "JUnit"
failTaskOnFailedTests: true
testResultsFiles: "**/TEST-*.xml"
- stage: dev
dependsOn: build
jobs:
- job: iac
- job: deploy_contracts
dependsOn: iac
- job: deploy_frontend
dependsOn:
- iac
- deploy_contracts
- stage: dev_validation
dependsOn: dev
jobs:
- job: wait_for_dev_validation
- job: delete_dev
dependsOn: wait_for_dev_validation
- stage: qa
dependsOn: dev_validation
jobs:
- job: iac
- job: deploy_contracts
dependsOn: iac
- job: deploy_frontend
dependsOn:
- iac
- deploy_contracts
- stage: qa_validation
dependsOn: qa
jobs:
- job: wait_for_qa_validation
- job: delete_qa
dependsOn: wait_for_qa_validation
- stage: prod
dependsOn: qa_validation
jobs:
- job: iac
- job: deploy_contracts
dependsOn: iac
- job: deploy_frontend
dependsOn:
- iac
- deploy_contracts
保存并运行Pipeline,确认一切结构正确并正在运行
测试结果的输出,所有的测试案例都通过了
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!