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