Web3 Devops with Azure Devops pipeline

  • zhengyq
  • 更新于 2022-09-04 15:24
  • 阅读 2522

Web3 Devops with Azure Devops pipeline

VSTS_workflow.png

注:开发环境和开发工具的内容是基于我之前发布的几篇文章,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/

开发环境(Docker Images)准备

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

开发工具(Remote Development)准备

VSCode安装完成之后,需要安装VSCode Remote插件。在插件搜索框中搜索remote,就可以看到Remote三件套:SSH、Containers、WSL。SSH和Containers就不多解释了,WSL是Windows Subsystem for Linux,如果操作系统是windows11可以直接开启WSL,通过windows docker desktop在WSL里启用docker,效果是完全一样的

Untitled15.png

Attach到容器

安装完插件之后,就可以看到romote图标,点击进去后切换到containers就可以看到运行中的镜像了,选中后鼠标右键Attach到镜像,就会开启一个新的vscode。这样整个开发环境就准备完成了

Untitled16.png


测试项目

测试项目一共两个部分,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 Pipeline

在Azure Devops中创建新的项目,Version Control 选择Git,

Untitled17.png

创建好项目之后,在Repos/Files中找到repository的地址,点击Generate GIt Credentials生成Password。之后在本地设置Git连接到这个远程库

Untitled18.png

使用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 按钮

Untitled19.png

Where is your code? 时选择Azure Repos Git。之后选择存放代码的Repo,然后选择 Starter pipeline

Untitled20.png

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,确认一切结构正确并正在运行

Untitled21.png

测试结果的输出,所有的测试案例都通过了

Untitled22.png

点赞 1
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

1 条评论

请先 登录 后评论
zhengyq
zhengyq
我们是微软CSI(Microosft China Strategic Incubator)团队,目前负责中国区web3领域创业孵化。主要是中国大陆、香港、台湾等地区的公司出海去往Global的web3项目。希望大家以后多多支持我们,谢谢!此外,我们在 web3领域和Microsoft Azure相关的所有研究都将放在这个 github 地址上: https://github.com/0xMSDN 联系邮箱:xiaopingfeng@microsoft.com