├── api ├── models │ └── arithmeticModel.js ├── routes.js └── controllers │ └── arithmeticController.js ├── .dockerignore ├── .gitignore ├── calculator-ui.png ├── public ├── digits.png ├── background.png ├── digits-grey.png ├── digits-darkgrey.png ├── default.css └── index.html ├── test ├── config.json ├── helpers.js └── arithmetic.js ├── app.js ├── server-test.js ├── .vscode └── launch.json ├── _demo-assets ├── demo-dockerfile ├── kube-deploy.yml ├── azure-pipelines.yml ├── addition-tests.js └── 00-demo-script.md ├── README.md ├── package.json ├── LICENSE ├── server.js └── web.config /api/models/arithmeticModel.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .npm 2 | node_modules/ 3 | out/ 4 | *.zip 5 | -------------------------------------------------------------------------------- /calculator-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureDevOps101/calculator/HEAD/calculator-ui.png -------------------------------------------------------------------------------- /public/digits.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureDevOps101/calculator/HEAD/public/digits.png -------------------------------------------------------------------------------- /public/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureDevOps101/calculator/HEAD/public/background.png -------------------------------------------------------------------------------- /public/digits-grey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureDevOps101/calculator/HEAD/public/digits-grey.png -------------------------------------------------------------------------------- /public/digits-darkgrey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureDevOps101/calculator/HEAD/public/digits-darkgrey.png -------------------------------------------------------------------------------- /test/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "reporterEnabled": "spec,mocha-junit-reporter", 3 | "mochaJunitReporterReporterOptions": { 4 | "mochaFile": "./out/test-results.xml" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /api/routes.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app) { 2 | var arithmetic = require("./controllers/arithmeticController"); 3 | 4 | app.route("/arithmetic").get(arithmetic.calculate); 5 | }; 6 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | 4 | app.use(express.static('public')); 5 | var routes = require("./api/routes"); 6 | routes(app); 7 | 8 | module.exports = app; -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | var supertest = require('supertest'); 2 | var chai = require('chai'); 3 | var app = require('../server-test'); 4 | 5 | global.app = app; 6 | global.expect = chai.expect; 7 | global.request = supertest(app); 8 | -------------------------------------------------------------------------------- /server-test.js: -------------------------------------------------------------------------------- 1 | var express = require('express'), 2 | app = express(), 3 | port = 3000; 4 | 5 | app.use(express.static('public')); 6 | 7 | var routes = require("./api/routes"); 8 | routes(app); 9 | 10 | if (! module.parent) { 11 | app.listen(port); 12 | } 13 | 14 | module.exports = app 15 | 16 | console.log("Server running on port " + port); -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "program": "${workspaceFolder}\\server.js" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /_demo-assets/demo-dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8 2 | 3 | # Create app directory 4 | WORKDIR /usr/src/app 5 | 6 | # Install app dependencies 7 | # A wildcard is used to ensure both package.json and package-lock.json are copied 8 | # where available (npm@5+) 9 | 10 | COPY package*.json ./ 11 | 12 | RUN npm install 13 | # if youa re building your code for production 14 | # RUN npm install --only=production 15 | 16 | # Bundle app source 17 | COPY . . 18 | 19 | # Expose 3000 port on container 20 | EXPOSE 3000 21 | 22 | # Start up your application 23 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Calculator.js: node.js 演示项目 2 | 3 | 本应用使用node.js创建,并包含了试用mocha的单元测试代码。本应用的运行状态为网页中的计算器,如下图: 4 | 5 | ![](calculator-ui.png) 6 | 7 | 代码中使用node.js代码提供了REST APIs,其中提供各种数学计算功能单元。 8 | 9 | 使用mocah编写的测试代码可以完成所有以上API内部运算运算逻辑的验证,最终使用 `mocha-junit-reports` 来生成XML格式的测试结果文件以便 [Azure DevOps](https://azure.com/devops) 可以读取测试结果提供DevOps流水线的测试集成。 10 | 11 | 本地构建本项目的方式: 12 | 13 | 1. 运行 `npm install` 安装所有依赖包 14 | 2. 运行 `npm test` 运行所有测试 15 | 3. 运行 `npm start` 启动应用,并打开 http://localhost:3000 16 | 17 | 使用Docker构建和运行项目的方式: 18 | 19 | 1. 复制 _demo-assets/demo-dockerfile 到根目录下的Dockerfile文件 20 | 21 | 2. 运行以下命令完成 `calculator` 容器构建打包 22 | 23 | ```shell 24 | docker build -t calculator . 25 | ``` 26 | 27 | 2. 运行 `calculator` 容器 28 | 29 | ```shell 30 | docker run -itd -p 8080:3000 calculator 31 | ``` 32 | 33 | 打开 http://localhost:8080 34 | 35 | 应用启动后的状态如上图。 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "calculator", 3 | "version": "1.0.0", 4 | "description": "Calculator", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/azure-pipelines-samples/node.js" 8 | }, 9 | "main": "index.js", 10 | "scripts": { 11 | "start": "node ./server.js --production", 12 | "build": "exit 0", 13 | "test": "mocha --timeout 10000 --reporter mocha-multi-reporters --reporter-options configFile=test/config.json" 14 | }, 15 | "keywords": [ 16 | "azure", 17 | "pipelines", 18 | "devops", 19 | "vsts", 20 | "ci", 21 | "cd" 22 | ], 23 | "author": "Edward Thomson ", 24 | "license": "MIT", 25 | "dependencies": { 26 | "express": "^4.16.3" 27 | }, 28 | "devDependencies": { 29 | "chai": "^4.1.2", 30 | "mocha": "^5.2.0", 31 | "mocha-junit-reporter": "^1.18.0", 32 | "mocha-multi-reporters": "^1.1.7", 33 | "supertest": "^3.1.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /_demo-assets/kube-deploy.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | service: calculator 6 | app.kubernetes.io/name: calculator 7 | name: calculator 8 | spec: 9 | type: LoadBalancer 10 | ports: 11 | - port: 80 12 | targetPort: 3000 13 | selector: 14 | service: calculator 15 | --- 16 | apiVersion: extensions/v1beta1 17 | kind: Deployment 18 | metadata: 19 | creationTimestamp: null 20 | labels: 21 | service: calculator 22 | name: calculator 23 | spec: 24 | replicas: 1 25 | template: 26 | metadata: 27 | labels: 28 | app.kubernetes.io/name: calculator 29 | app.kubernetes.io/component: calculator 30 | app: calculator 31 | service: calculator 32 | spec: 33 | containers: 34 | - image: azuredevops101.azurecr.io/azuredevops101/calculator:latest 35 | name: calculator 36 | ports: 37 | - containerPort: 3000 38 | imagePullSecrets: 39 | - name: regcred 40 | restartPolicy: Always -------------------------------------------------------------------------------- /_demo-assets/azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # Node.js 2 | # Build a general Node.js project with npm. 3 | # Add steps that analyze code, save build artifacts, deploy, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/javascript 5 | 6 | trigger: 7 | - master 8 | 9 | pool: 10 | vmImage: 'Ubuntu 16.04' 11 | 12 | steps: 13 | - task: NodeTool@0 14 | inputs: 15 | versionSpec: '10.x' 16 | displayName: 'Install Node.js' 17 | 18 | - script: | 19 | npm install 20 | npm run build 21 | npm test 22 | displayName: 'npm install, build and test' 23 | 24 | - task: PublishTestResults@2 25 | displayName: 'Publish Test Results **/test-*.xml' 26 | inputs: 27 | testResultsFiles: '**/test-*.xml' 28 | condition: succeededOrFailed() 29 | 30 | - task: ArchiveFiles@2 31 | displayName: 'Archive $(Build.SourcesDirectory)' 32 | inputs: 33 | rootFolderOrFile: '$(Build.SourcesDirectory)' 34 | includeRootFolder: false 35 | archiveFile: '$(Build.ArtifactStagingDirectory)/Calculator-$(Build.BuildId).zip' 36 | 37 | - task: PublishBuildArtifacts@1 38 | displayName: 'Publish Artifact: drop' 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Microsoft Corporation. All rights reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /api/controllers/arithmeticController.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.calculate = function(req, res) { 4 | req.app.use(function(err, req, res, next) { 5 | if (res.headersSent) { 6 | return next(err); 7 | } 8 | 9 | res.status(400); 10 | res.json({ error: err.message }); 11 | }); 12 | 13 | var operations = { 14 | // this should work now 15 | 'add': function(a,b) { return a + b }, 16 | 'subtract': function(a,b) { return a - b }, 17 | 'multiply': function(a,b) { return a * b }, 18 | 'divide': function(a,b) { return a / b }, 19 | }; 20 | 21 | // Determine the operation 22 | 23 | if (! req.query.operation) { 24 | throw new Error("Unspecified operation"); 25 | } 26 | 27 | var operation = operations[req.query.operation]; 28 | 29 | if (! operation) { 30 | throw new Error("Invalid operation: " + req.query.operation); 31 | } 32 | 33 | // Validate operands 34 | 35 | if (! req.query.operand1 || 36 | ! req.query.operand1.match(/^(-)?[0-9\.]+(e(-)?[0-9]+)?$/) || 37 | req.query.operand1.replace(/[-0-9e]/g, '').length > 1) { 38 | throw new Error("Invalid operand1: " + req.query.operand1); 39 | } 40 | 41 | if (! req.query.operand2 || 42 | ! req.query.operand2.match(/^(-)?[0-9\.]+(e(-)?[0-9]+)?$/) || 43 | req.query.operand2.replace(/[-0-9e]/g, '').length > 1) { 44 | throw new Error("Invalid operand2: " + req.query.operand2); 45 | } 46 | 47 | var operand1 = parseInt(req.query.operand1, 10); 48 | var operand2 = parseInt(req.query.operand2, 10); 49 | 50 | res.json({ result: operation(req.query.operand1, req.query.operand2) }); 51 | }; 52 | -------------------------------------------------------------------------------- /_demo-assets/addition-tests.js: -------------------------------------------------------------------------------- 1 | describe('Addition', function() { 2 | it('adds two positive integers', function(done) { 3 | request.get('/arithmetic?operation=add&operand1=21&operand2=21') 4 | .expect(200) 5 | .end(function(err, res) { 6 | expect(res.body).to.eql({ result: 42 }); 7 | done(); 8 | }); 9 | }); 10 | it('adds zero to an integer', function(done) { 11 | request.get('/arithmetic?operation=add&operand1=42&operand2=0') 12 | .expect(200) 13 | .end(function(err, res) { 14 | expect(res.body).to.eql({ result: 42 }); 15 | done(); 16 | }); 17 | }); 18 | it('adds a negative integer to a positive integer', function(done) { 19 | request.get('/arithmetic?operation=add&operand1=21&operand2=-42') 20 | .expect(200) 21 | .end(function(err, res) { 22 | expect(res.body).to.eql({ result: -21 }); 23 | done(); 24 | }); 25 | }); 26 | it('adds two negative integers', function(done) { 27 | request.get('/arithmetic?operation=add&operand1=-21&operand2=-21') 28 | .expect(200) 29 | .end(function(err, res) { 30 | expect(res.body).to.eql({ result: -42 }); 31 | done(); 32 | }); 33 | }); 34 | it('adds an integer to a floating point number', function(done) { 35 | request.get('/arithmetic?operation=add&operand1=2.5&operand2=-5') 36 | .expect(200) 37 | .end(function(err, res) { 38 | expect(res.body).to.eql({ result: -2.5 }); 39 | done(); 40 | }); 41 | }); 42 | it('adds with negative exponent', function(done) { 43 | request.get('/arithmetic?operation=add&operand1=1.2e-5&operand2=-1.2e-5') 44 | .expect(200) 45 | .end(function(err, res) { 46 | expect(res.body).to.eql({ result: 0 }); 47 | done(); 48 | }); 49 | }); 50 | }); -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | const app = require('./app'); 8 | const debug = require('debug')('calculator:server'); 9 | const http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | const port = normalizePort(process.env.PORT || '3000'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | const server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on('error', onError); 30 | server.on('listening', onListening); 31 | 32 | console.log("Server running on port " + port); 33 | 34 | /** 35 | * Normalize a port into a number, string, or false. 36 | */ 37 | 38 | function normalizePort(val) { 39 | const port = parseInt(val, 10); 40 | 41 | if (isNaN(port)) { 42 | // named pipe 43 | return val; 44 | } 45 | 46 | if (port >= 0) { 47 | // port number 48 | return port; 49 | } 50 | return false; 51 | } 52 | 53 | /** 54 | * Event listener for HTTP server "error" event. 55 | */ 56 | 57 | function onError(error) { 58 | if (error.syscall !== 'listen') { 59 | throw error; 60 | } 61 | 62 | const bind = typeof port === 'string' 63 | ? 'Pipe ' + port 64 | : 'Port ' + port; 65 | 66 | // handle specific listen errors with friendly messages 67 | switch (error.code) { 68 | case 'EACCES': 69 | console.error(bind + ' requires elevated privileges'); 70 | process.exit(1); 71 | break; 72 | case 'EADDRINUSE': 73 | console.error(bind + ' is already in use'); 74 | process.exit(1); 75 | break; 76 | default: 77 | throw error; 78 | } 79 | } 80 | 81 | /** 82 | * Event listener for HTTP server "listening" event. 83 | */ 84 | 85 | function onListening() { 86 | const addr = server.address(); 87 | const bind = typeof addr === 'string' 88 | ? 'pipe ' + addr 89 | : 'port ' + addr.port; 90 | debug('Listening on ' + bind); 91 | } 92 | 93 | -------------------------------------------------------------------------------- /web.config: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /public/default.css: -------------------------------------------------------------------------------- 1 | BODY 2 | { 3 | height: 100%; 4 | width: 100%; 5 | padding: 0; 6 | margin: 0; 7 | background-color: #1b4076; 8 | } 9 | 10 | @media screen and (min-device-width: 500px) and (min-device-height: 1024px) 11 | { 12 | BODY 13 | { 14 | transform: scale(1.75); 15 | transform-origin: 50% 0; 16 | height: 50%; 17 | } 18 | } 19 | 20 | @media screen and (min-device-width: 1024px) and (min-device-height: 1366px) 21 | { 22 | BODY 23 | { 24 | transform: scale(2.0); 25 | transform-origin: 50% 0; 26 | height: 50%; 27 | } 28 | } 29 | 30 | .container 31 | { 32 | min-width: 250px; 33 | min-height: 531px; 34 | width: 250px; 35 | padding: 25px; 36 | margin: 0 auto; 37 | position: relative; 38 | background-image: url(background.png); 39 | background-size: 300px 531px; 40 | background-repeat: no-repeat; 41 | background-position: center top; 42 | } 43 | 44 | #results 45 | { 46 | position: relative; 47 | top: 0; 48 | left: 0; 49 | } 50 | 51 | #buttons 52 | { 53 | position: relative; 54 | } 55 | 56 | #result 57 | { 58 | background-color: #9b9b9b; 59 | width: 226px; 60 | height: 63px; 61 | margin: 0; 62 | padding: 0 12px; 63 | position: absolute; 64 | border: 0; 65 | left: 0; 66 | top: 32px; 67 | font-family: system-ui; 68 | font-size: 25px; 69 | text-align: right; 70 | } 71 | 72 | BUTTON 73 | { 74 | background-color: #d5d5d5; 75 | border: 0; 76 | border-radius: 0; 77 | width: 55px; 78 | height: 42px; 79 | position: absolute; 80 | 81 | font-size: 22px; 82 | } 83 | 84 | #sign, #clear 85 | { 86 | font-size: 17px; 87 | } 88 | #clearEntry 89 | { 90 | font-size: 15px; 91 | } 92 | 93 | #seven, #four, #one, #zero { left: 0px; } 94 | #sign, #eight, #five, #two, #decimal { left: 65px; } 95 | #clear, #nine, #six, #three, #equal { left: 130px; } 96 | #clearEntry, #divide, #multiply, #subtract, #add { left: 195px; } 97 | 98 | #sign, #clear, #clearEntry { top: 202px; } 99 | #seven, #eight, #nine, #divide { top: 255px; } 100 | #four, #five, #six, #multiply { top: 308px; } 101 | #one, #two, #three, #subtract { top: 361px; } 102 | #zero, #decimal, #equal, #add { top: 414px; } 103 | 104 | .resultchar 105 | { 106 | display: inline-block; 107 | width: 25px; 108 | height: 45px; 109 | background: url('digits.png') 0 0 no-repeat; 110 | background-size: cover; 111 | margin-top: 10px; 112 | font: 0/0 Arial; 113 | color: rgba(255,255,255,0); 114 | text-indent: -9999px; 115 | z-index: 1; 116 | } 117 | 118 | .digit0 { background-position: 0 0; } 119 | .digit1 { background-position: -25px 0; } 120 | .digit2 { background-position: -50px 0; } 121 | .digit3 { background-position: -75px 0; } 122 | .digit4 { background-position: -100px 0; } 123 | .digit5 { background-position: -125px 0; } 124 | .digit6 { background-position: -150px 0; } 125 | .digit7 { background-position: -175px 0; } 126 | .digit8 { background-position: -200px 0; } 127 | .digit9 { background-position: -225px 0; } 128 | 129 | .resultchar.decimal 130 | { 131 | width: 7px; 132 | background-position: -250px 0; 133 | } 134 | 135 | .resultchar.negative 136 | { 137 | background-position: -257px 0; 138 | width: 17px; 139 | } 140 | 141 | .resultchar.exponent 142 | { 143 | background-position: -274px 0; 144 | width: 25px; 145 | } 146 | -------------------------------------------------------------------------------- /_demo-assets/00-demo-script.md: -------------------------------------------------------------------------------- 1 | # Calculator Demo for Azure Pipeline 2 | 3 | ## Demo 1 - Integrate GitHub public repo with Azure Pipeline 4 | 5 | **Setup** 6 | 1. Git clone https://github.com/AzureDevOps101/calculator 7 | 2. Make the following changes 8 | a. Introduce the bug change (+a + +b) to (a + b) 9 | b. Remove unit test for Additions to avoid test failure 10 | c. Remove /azure-pipelines.yml so we can create to from scratch 11 | 3. Push it back to Github for a new repo 12 | 4. Create a Azure Web App for the Release Pipeline to use and make sure you have the following Configuration 13 | WEBSITE_NODE_DEFAULT_VERSION = 8.9.4 14 | 15 | **Steps:** 16 | 17 | ### Section 1 18 | 19 | 1. Go to the new repo 20 | 2. Toto GitHub Marketplace and search for "Azure Pipeline" 21 | 3. Enable Azure Pipeline and setup an Azure Pipeline for the Calculator app 22 | 4. Add steps for packaging Artefacts 23 | 5. Add Release Pipeline to deploy application to Azure Web App 24 | 6. Trigger deploy and show the app running 25 | 7. Send the URL to audience for testing 26 | 27 | ### Section 2 28 | 29 | 1. Audience find the bug about a + b 30 | 2. Create an GitHub Issue and try to fix the issue 31 | 3. Follow TDD and add unit test for a + b first 32 | 4. Commit to master branch 33 | 5. Re-configure Azure Pipeline 34 | a. Override YAML build triggers 35 | b. Trigger on master branch 36 | c. Trigger on Pull Request and require team member comment to start build 37 | d. (Release Pipeline) only trigger on master branch (prevent PR build to trigger deploy) 38 | 39 | ### Section 3 40 | 41 | 1. Use another account leixu216 42 | 2. (leixu216) Fork the repo 43 | 3. (leixu216) commit #1 try to make some changes which is not going to fix the build 44 | 4. (leixu216) create PR to main repo 45 | 5. (ups216) to trigger the build using /azurepipeline run 46 | 6. (ups216) use vscode PR extension to review the code 47 | 7. (leixu216) commit #2 finally fix the code 48 | 8. (ups216) close the review, trigger build again (/azp run) and accept PR when build is ok 49 | 9. Check the CI/CD full pipeline is running and URL is updated 50 | 10. Ask audience to test 51 | 52 | ## Demo 2 - Docker Build and Deploy to AKS 53 | 54 | prep 55 | 56 | ```shell 57 | az group create --name MyResourceGroup --location southeastasia 58 | az aks create -g MyResourceGroup -n MyAKS --location southeastasia --node-vm-size Standard_DS2_v2 --node-count 2 --disable-rbac --generate-ssh-keys 59 | 60 | az aks get-credentials -g MyResourceGroup -n MyAKS 61 | 62 | az aks use-dev-spaces -g MyResourceGroup -n MyAKS --space dev --yes 63 | 64 | ``` 65 | 66 | 1. Add a Dockerfile 67 | 2. Build the docker container and run it locally 68 | 69 | ```shell 70 | docker build -t calculator:v2 . 71 | ``` 72 | 73 | 3. Show the ACR and copy the key 74 | 75 | ```shell 76 | docker login {acr-name}.azurecr.io 77 | ``` 78 | 79 | 4. Tag and push the image 80 | 81 | ```shell 82 | docker tag calculator:v2 {acr-name}.azurecr.io/calculator:v2 83 | docker push {acr-name}.azurecr.io/calculator:v2 84 | ``` 85 | 86 | 5. Create kube-deploy.yml and deploy to AKS 87 | 88 | ```shell 89 | az login 90 | az account set -s {sub-id} 91 | az aks get-credential -g {resourceGroup} -n {aksName} 92 | 93 | kubectl create secret docker-registry regcred --docker-server={acr-name}.azurecr.io --docker-username={acr-name} --docker-password={acr-password} --docker-email=leixu@leansoftx.com 94 | 95 | kubectl apploy -f kube-deploy.yml 96 | ``` 97 | ## Demo 3 - Create pipeline for docker build and AKS deployment 98 | 99 | 1. Create a classic build with Docker Container template 100 | - tick add "latest" tags 101 | 2. Add the following 2 steps 102 | - Copy File 103 | - Publish Build Artifact 104 | and publish kube-deploy.yml 105 | 3. Trigger the build 106 | 4. Create a release definition with Kubernetes Cluster deployment template 107 | - create secret name: regcred 108 | 109 | ## Demo 4 - Use Azure Dev Spaces in Visual Studio 2019 with .net core web app 110 | 111 | 1. create new .net core web app with mvc 112 | 2. Enable Dev Space 113 | 3. Start debugging and set breakpoint 114 | 4. add Environment.MachineName to show the container (pod) name 115 | 116 | ## Demo 5 - Use Azure Dev Spaces in Visual Studio Code 117 | 118 | 1. Following documentation from https://github.com/AzureDevOps101/BikeSharingApp 119 | 120 | 121 | ## Demo 6 - Istio 122 | 123 | 1. show test environment and pipeline 124 | - vote: http://13.76.172.131:31654/ 125 | - result: http://13.76.172.131:31844/ 126 | 127 | 2. show production environment 128 | - vote: http://vote.devopshub.cn 129 | - result: http://result.devopshub.cn 130 | show gateway.yaml 131 | 132 | 3. Sceanrio: v2 is ready to go live 133 | 134 | 4. Use kubectl to explain how istio working 135 | 136 | ```shell 137 | 138 | # rollback to old version first 139 | kubectl apply -f vote-app-old.yml 140 | # refresh http://vote.devlopshub.cn and always see v1 141 | 142 | # start canary deployment 10% for vew version v2 143 | kubectl apply -f vote-app-90-10.yml 144 | # refresh http://vote.devlopshub.cn and see v2 for 10% of time 145 | # try to change 50/50 and refresh again 146 | 147 | # direct all traffic to v2 148 | kubectl apply -f vote-app-new.yml 149 | # refresh http://vote.devlopshub.cn and always see v2 150 | 151 | # rollback to old version again 152 | kubectl apply -f vote-app-old.yml 153 | # refresh http://vote.devlopshub.cn and always see v1 154 | ``` 155 | 156 | 5. Show the Production Pipeline and simulate the same process as above 157 | 158 | ## Demo 7 - GitHub Actions 159 | 160 | Enable github action for caculator app. 161 | 162 | 163 | 1. in Github repo, click on Action | New workflow 164 | 2. Choose [Deploy Node.js to Azure Web App](https://github.com/actions/starter-workflows/blob/3c3736f59805d1e4f838182263705f44fab9cf68/ci/azure.yml) action 165 | 3. In Azure Portal, create a "Web App" with Node 10.0 as the runtime 166 | 4. Download "Publish Profile" from the web app homepage 167 | 5. Create a secret in GitHub repo, name it AZURE_WEBAPP_PUBLISH_PROFILE and copy all content from "publish profile" into this secret 168 | 6. in the Github action configuration file, default path is ".github/workflows/azure.yml", update the following parameters 169 | 170 | ```yml 171 | AZURE_WEBAPP_NAME: ## get this from azure 172 | ``` 173 | 174 | 7. Update the following as the trigger 175 | 176 | Note: this is only for demo purpose 177 | 178 | ```yml 179 | on: 180 | ##release: 181 | ## types: [created] 182 | push: 183 | branches: [ master ] 184 | pull_request: 185 | branches: [ master ] 186 | ``` 187 | 188 | 8. Commit the yml file will trigger the action to push the app into web app 189 | 190 | then you can start your demo for the calculator scenario. 191 | 192 | 193 | 194 | 195 | 196 | -------------------------------------------------------------------------------- /test/arithmetic.js: -------------------------------------------------------------------------------- 1 | describe('Arithmetic', function() { 2 | describe('Validation', function() { 3 | it('rejects missing operation', function(done) { 4 | request.get('/arithmetic?operand1=21&operand2=21') 5 | .expect(400) 6 | .end(function(err, res) { 7 | expect(res.body).to.eql({ error: "Unspecified operation" }); 8 | done(); 9 | }); 10 | }); 11 | it('rejects invalid operation', function(done) { 12 | request.get('/arithmetic?operation=foobar&operand1=21&operand2=21') 13 | .expect(400) 14 | .end(function(err, res) { 15 | expect(res.body).to.eql({ error: "Invalid operation: foobar" }); 16 | done(); 17 | }); 18 | }); 19 | it('rejects missing operand1', function(done) { 20 | request.get('/arithmetic?operation=add&operand2=21') 21 | .expect(400) 22 | .end(function(err, res) { 23 | expect(res.body).to.eql({ error: "Invalid operand1: undefined" }); 24 | done(); 25 | }); 26 | }); 27 | it('rejects missing operand2', function(done) { 28 | request.get('/arithmetic?operation=add&operand1=21') 29 | .expect(400) 30 | .end(function(err, res) { 31 | expect(res.body).to.eql({ error: "Invalid operand2: undefined" }); 32 | done(); 33 | }); 34 | }); 35 | it('rejects operands with invalid sign', function(done) { 36 | request.get('/arithmetic?operation=add&operand1=4.2-1&operand2=4') 37 | .expect(400) 38 | .end(function(err, res) { 39 | expect(res.body).to.eql({ error: "Invalid operand1: 4.2-1" }); 40 | done(); 41 | }); 42 | }); 43 | it('rejects operands with invalid decimals', function(done) { 44 | request.get('/arithmetic?operation=add&operand1=4.2.1&operand2=4') 45 | .expect(400) 46 | .end(function(err, res) { 47 | expect(res.body).to.eql({ error: "Invalid operand1: 4.2.1" }); 48 | done(); 49 | }); 50 | }); 51 | }); 52 | 53 | //insert Addition Tests here 54 | 55 | describe('Subtraction', function() { 56 | it('subtracts two positive integers', function(done) { 57 | request.get('/arithmetic?operation=subtract&operand1=42&operand2=21') 58 | .expect(200) 59 | .end(function(err, res) { 60 | expect(res.body).to.eql({ result: 21 }); 61 | done(); 62 | }); 63 | }); 64 | it('subtracts an integer from itself', function(done) { 65 | request.get('/arithmetic?operation=subtract&operand1=42&operand2=42') 66 | .expect(200) 67 | .end(function(err, res) { 68 | expect(res.body).to.eql({ result: 0 }); 69 | done(); 70 | }); 71 | }); 72 | it('subtracts a larger integer from another', function(done) { 73 | request.get('/arithmetic?operation=subtract&operand1=21&operand2=42') 74 | .expect(200) 75 | .end(function(err, res) { 76 | expect(res.body).to.eql({ result: -21 }); 77 | done(); 78 | }); 79 | }); 80 | it('subtracts a negative integer from a positive integer', function(done) { 81 | request.get('/arithmetic?operation=subtract&operand1=21&operand2=-21') 82 | .expect(200) 83 | .end(function(err, res) { 84 | expect(res.body).to.eql({ result: 42 }); 85 | done(); 86 | }); 87 | }); 88 | }); 89 | 90 | describe('Multiplication', function() { 91 | it('multiplies two positive integers', function(done) { 92 | request.get('/arithmetic?operation=multiply&operand1=21&operand2=2') 93 | .expect(200) 94 | .end(function(err, res) { 95 | expect(res.body).to.eql({ result: 42 }); 96 | done(); 97 | }); 98 | }); 99 | it('multiplies a positive integer with zero', function(done) { 100 | request.get('/arithmetic?operation=multiply&operand1=21&operand2=0') 101 | .expect(200) 102 | .end(function(err, res) { 103 | expect(res.body).to.eql({ result: 0 }); 104 | done(); 105 | }); 106 | }); 107 | it('multiplies a positive integer and negative integer', function(done) { 108 | request.get('/arithmetic?operation=multiply&operand1=21&operand2=-2') 109 | .expect(200) 110 | .end(function(err, res) { 111 | expect(res.body).to.eql({ result: -42 }); 112 | done(); 113 | }); 114 | }); 115 | it('multiplies two negative integers', function(done) { 116 | request.get('/arithmetic?operation=multiply&operand1=-21&operand2=-2') 117 | .expect(200) 118 | .end(function(err, res) { 119 | expect(res.body).to.eql({ result: 42 }); 120 | done(); 121 | }); 122 | }); 123 | it('multiplies two floating point numbers', function(done) { 124 | request.get('/arithmetic?operation=multiply&operand1=.5&operand2=0.5') 125 | .expect(200) 126 | .end(function(err, res) { 127 | expect(res.body).to.eql({ result: 0.25 }); 128 | done(); 129 | }); 130 | }); 131 | it('multiplies supporting exponential notation', function(done) { 132 | request.get('/arithmetic?operation=multiply&operand1=4.2e1&operand2=1e0') 133 | .expect(200) 134 | .end(function(err, res) { 135 | expect(res.body).to.eql({ result: 42 }); 136 | done(); 137 | }); 138 | }); 139 | }); 140 | 141 | describe('Division', function() { 142 | it('divides a positive integer by an integer factor ', function(done) { 143 | request.get('/arithmetic?operation=divide&operand1=42&operand2=2') 144 | .expect(200) 145 | .end(function(err, res) { 146 | expect(res.body).to.eql({ result: 21 }); 147 | done(); 148 | }); 149 | }); 150 | it('divides a negative integer by an integer factor ', function(done) { 151 | request.get('/arithmetic?operation=divide&operand1=-42&operand2=2') 152 | .expect(200) 153 | .end(function(err, res) { 154 | expect(res.body).to.eql({ result: -21 }); 155 | done(); 156 | }); 157 | }); 158 | it('divides a positive integer by a non-factor', function(done) { 159 | request.get('/arithmetic?operation=divide&operand1=21&operand2=42') 160 | .expect(200) 161 | .end(function(err, res) { 162 | expect(res.body).to.eql({ result: 0.5 }); 163 | done(); 164 | }); 165 | }); 166 | it('divides a positive integer by a negative integer', function(done) { 167 | request.get('/arithmetic?operation=divide&operand1=21&operand2=-42') 168 | .expect(200) 169 | .end(function(err, res) { 170 | expect(res.body).to.eql({ result: -0.5 }); 171 | done(); 172 | }); 173 | }); 174 | it('divides zero by a positive integer', function(done) { 175 | request.get('/arithmetic?operation=divide&operand1=0&operand2=42') 176 | .expect(200) 177 | .end(function(err, res) { 178 | expect(res.body).to.eql({ result: 0 }); 179 | done(); 180 | }); 181 | }); 182 | it('divides by zero', function(done) { 183 | request.get('/arithmetic?operation=divide&operand1=0.5&operand2=2') 184 | .expect(200) 185 | .end(function(err, res) { 186 | expect(res.body).to.eql({ result: 0.25 }); 187 | done(); 188 | }); 189 | }); 190 | it('divides by zero', function(done) { 191 | request.get('/arithmetic?operation=divide&operand1=21&operand2=0') 192 | .expect(200) 193 | .end(function(err, res) { 194 | expect(res.body).to.eql({ result: null }); 195 | done(); 196 | }); 197 | }); 198 | }); 199 | }); 200 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Pocket Calculator 4 | 5 | 6 | 224 | 225 | 226 |
227 |
228 |
0
229 | 230 |
231 | 232 | 233 | 234 |
235 |
236 | 237 | 238 |
239 | 240 |
241 | 242 | 243 | 244 | 245 |
246 |
247 | 248 | 249 | 250 | 251 |
252 |
253 | 254 | 255 | 256 | 257 |
258 |
259 | 260 | 261 | 262 | 263 |
264 |
265 |
266 | 267 | 268 | --------------------------------------------------------------------------------