diff --git a/.github/workflows/node-azure-deploy.yml b/.github/workflows/node-azure-deploy.yml index 2e221e9..1eedfcc 100644 --- a/.github/workflows/node-azure-deploy.yml +++ b/.github/workflows/node-azure-deploy.yml @@ -26,6 +26,7 @@ env: NODE_VERSION: '18.x' AZURE_WEBAPP_NAME: 'webapp-az400-demo' AZURE_WEBAPP_PACKAGE_PATH: './nodeapp-1' + WORKING_DIRECTORY: 'nodeapp-1' jobs: # ======================================== @@ -44,42 +45,38 @@ jobs: with: node-version: ${{ env.NODE_VERSION }} cache: 'npm' - cache-dependency-path: '${{ env.AZURE_WEBAPP_PACKAGE_PATH }}/package-lock.json' + cache-dependency-path: '${{ env.WORKING_DIRECTORY }}/package-lock.json' - name: ๐Ÿ“ฆ Install dependencies run: npm ci - working-directory: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }} + working-directory: ${{ env.WORKING_DIRECTORY }} - name: ๐Ÿ”’ Security audit run: npm audit --audit-level=high - working-directory: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }} + working-directory: ${{ env.WORKING_DIRECTORY }} continue-on-error: true - name: ๐Ÿงน Lint code - run: npm run lint || echo "No lint script configured" - working-directory: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }} + run: npm run lint + working-directory: ${{ env.WORKING_DIRECTORY }} - - name: ๐Ÿงช Run tests - run: npm test -- --coverage || npm test - working-directory: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }} + - name: ๐Ÿงช Run tests with Mocha + run: npm test + working-directory: ${{ env.WORKING_DIRECTORY }} - - name: ๐Ÿ“Š Upload coverage reports - uses: codecov/codecov-action@v3 + - name: ๐Ÿ“Š Upload test results + uses: actions/upload-artifact@v4 + if: always() with: - directory: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }}/coverage - flags: unittests - name: codecov-umbrella - fail_ci_if_error: false - - - name: ๐Ÿ”จ Build application - run: npm run build || echo "No build required" - working-directory: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }} + name: mochawesome-report + path: ${{ env.WORKING_DIRECTORY }}/mochawesome-report + retention-days: 7 - name: ๐Ÿ“ค Upload artifact for deployment uses: actions/upload-artifact@v4 with: name: node-app - path: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }} + path: ${{ env.WORKING_DIRECTORY }} retention-days: 5 # ======================================== @@ -137,6 +134,9 @@ jobs: url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} steps: + - name: ๐Ÿ“ฅ Checkout for tests + uses: actions/checkout@v4 + - name: ๐Ÿ“ฅ Download artifact uses: actions/download-artifact@v4 with: @@ -158,9 +158,11 @@ jobs: - name: ๐Ÿงช Run integration tests run: | - echo "Running integration tests against staging..." - # Add your integration test commands here - echo "โœ… Integration tests passed!" + cd ${{ env.WORKING_DIRECTORY }} + npm ci + export TEST_URL=https://${{ env.AZURE_WEBAPP_NAME }}-staging.azurewebsites.net + npm test + continue-on-error: true # ======================================== # JOB 4: DEPLOY TO PRODUCTION @@ -204,12 +206,6 @@ jobs: }); console.log(`โœ… Created tag: ${tag}`); - - name: ๐Ÿ“Š Monitor deployment - run: | - echo "Monitoring production deployment..." - # Add Application Insights queries here - echo "โœ… Production deployment healthy!" - # ======================================== # REUSABLE WORKFLOW: SECURITY SCAN # ======================================== @@ -222,12 +218,7 @@ jobs: - name: ๐Ÿ“ฅ Checkout code uses: actions/checkout@v4 - - name: ๐Ÿ” Run CodeQL Analysis - uses: github/codeql-action/analyze@v2 - with: - languages: javascript - - - name: ๐Ÿ›ก๏ธ Run Dependabot scan + - name: ๐Ÿ” Run Dependabot scan uses: github/super-linter@v5 env: DEFAULT_BRANCH: main diff --git a/nodeapp-1/.dockerignore b/nodeapp-1/.dockerignore new file mode 100644 index 0000000..2eb5f95 --- /dev/null +++ b/nodeapp-1/.dockerignore @@ -0,0 +1,15 @@ +node_modules +npm-debug.log +mochawesome-report +.git +.gitignore +.npmrc +.eslintrc.json +.mocharc.json +test +debug-solution.txt +*.code-workspace +.vscode +.devcontainer +.github +.azure \ No newline at end of file diff --git a/nodeapp-1/.eslintrc.json b/nodeapp-1/.eslintrc.json new file mode 100644 index 0000000..07f88e0 --- /dev/null +++ b/nodeapp-1/.eslintrc.json @@ -0,0 +1,20 @@ +{ + "env": { + "node": true, + "es2021": true, + "mocha": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "rules": { + "indent": ["error", 2], + "linebreak-style": ["error", "unix"], + "quotes": ["error", "single"], + "semi": ["error", "always"], + "no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], + "no-console": "off" + } +} \ No newline at end of file diff --git a/nodeapp-1/.mocharc.json b/nodeapp-1/.mocharc.json new file mode 100644 index 0000000..ca39441 --- /dev/null +++ b/nodeapp-1/.mocharc.json @@ -0,0 +1,13 @@ +{ + "timeout": 5000, + "exit": true, + "reporter": "mochawesome", + "reporter-options": [ + "reportDir=mochawesome-report", + "reportFilename=test-results", + "html=true", + "json=true", + "overwrite=true", + "inline=true" + ] +} \ No newline at end of file diff --git a/nodeapp-1/Dockerfile b/nodeapp-1/Dockerfile new file mode 100644 index 0000000..67af834 --- /dev/null +++ b/nodeapp-1/Dockerfile @@ -0,0 +1,19 @@ +FROM node:18-alpine + +# Create app directory +WORKDIR /usr/src/app + +# Install app dependencies +# A wildcard is used to ensure both package.json AND package-lock.json are copied +COPY package*.json ./ + +RUN npm ci --only=production + +# Bundle app source +COPY . . + +# Expose port +EXPOSE 3000 + +# Start the application +CMD [ "node", "app.js" ] \ No newline at end of file diff --git a/nodeapp-1/README.md b/nodeapp-1/README.md index 7aeb2ec..5d04243 100644 --- a/nodeapp-1/README.md +++ b/nodeapp-1/README.md @@ -1,40 +1,77 @@ -# Node & Express Demo App for GitHub Actions, Azure DevOps, and Beyond +# Node Express Azure - AZ-400 Demo App -## Last edited by Tim Warner +Sample Node.js Express application for demonstrating CI/CD pipelines in AZ-400 training. -> Build Your First CI/CD Pipeline using Azure DevOps with this Demo App. +## Features -This is a Node and Express web application used to demonstrate CI/CD with Azure DevOps. You can clone this repo and use it within Azure DevOps to build, test, and release to an Azure App Service web app. +- Express.js web application +- Handlebars templating +- Mocha/Chai testing with mochawesome reports +- ESLint for code quality +- Docker support +- Azure DevOps and GitHub Actions ready -[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new/timothywarner/node-express-azure) +## Getting Started -## Running and Testing Locally: +### Prerequisites -You can use these commands to install, test, and run the app locally. (Not Required) +- Node.js 18.x or higher +- npm 8.x or higher -### Install +### Installation -``` +```bash npm install ``` -### Test +### Running Locally +```bash +npm start ``` + +The app will be available at http://localhost:3000 + +### Running Tests + +```bash npm test ``` -![alt text](https://user-images.githubusercontent.com/5126491/51065379-c1743280-15c1-11e9-80fd-6a3d7ab4ac1b.jpg "Unit Test") +### Linting -Navigate to the `/test` folder to review the unit tests for this project. These tests will run as part of your Azure DevOps Build pipeline. See `azure-pipelines.yml` in this repo. +```bash +npm run lint +``` -### Start +### Docker +Build the image: +```bash +docker build -t nodeapp-1 . ``` -npm start + +Run the container: +```bash +docker run -p 3000:3000 nodeapp-1 ``` +## CI/CD Pipelines + +This app includes example pipelines for: +- Azure Pipelines (see `/pipelines` folder in repo root) +- GitHub Actions (see `.github/workflows` in repo root) + +## Azure Artifacts + +To publish to Azure Artifacts: + +1. Create `.npmrc` from `.npmrc.template` +2. Set up authentication +3. Run `npm run publish:dev` or `npm run publish:prod` -### License +## Environment Variables -This project is licensed under the Apache License 2.0 +- `PORT` - Server port (default: 3000) +- `NODE_ENV` - Environment (development/production) +- `AZURE_ARTIFACTS_FEED` - Azure Artifacts feed URL diff --git a/nodeapp-1/app.js b/nodeapp-1/app.js index 3e81e4f..4d7975b 100644 --- a/nodeapp-1/app.js +++ b/nodeapp-1/app.js @@ -27,7 +27,12 @@ app.use('/', index); app.use('/who', who); app.use('/contact', contact); -// Start the server -app.listen(app.get('port'), () => { - console.log(`Server running on port ${app.get('port')}`); -}); \ No newline at end of file +// Export the app for testing +module.exports = app; + +// Only start the server if this file is run directly +if (require.main === module) { + app.listen(app.get('port'), () => { + console.log(`Server running on port ${app.get('port')}`); + }); +} \ No newline at end of file diff --git a/nodeapp-1/config.js b/nodeapp-1/config.js index 60c6ce8..7ba4252 100644 --- a/nodeapp-1/config.js +++ b/nodeapp-1/config.js @@ -1,3 +1,7 @@ module.exports = { - port: process.env.PORT || 443, + port: process.env.PORT || 3000, + environment: process.env.NODE_ENV || 'development', + azure: { + artifactsFeed: process.env.AZURE_ARTIFACTS_FEED || 'https://pkgs.dev.azure.com/certstarorg/_packaging/az400-npm-feed/npm/registry/' + } }; \ No newline at end of file diff --git a/nodeapp-1/server.js b/nodeapp-1/server.js new file mode 100644 index 0000000..1db95bd --- /dev/null +++ b/nodeapp-1/server.js @@ -0,0 +1,9 @@ +// Server entry point for production +const app = require('./app'); + +const port = process.env.PORT || app.get('port'); + +app.listen(port, () => { + console.log(`Server running on port ${port}`); + console.log(`Environment: ${process.env.NODE_ENV || 'development'}`); +}); \ No newline at end of file diff --git a/nodeapp-1/test/contact_test.js b/nodeapp-1/test/contact_test.js index e45a537..41c3a71 100644 --- a/nodeapp-1/test/contact_test.js +++ b/nodeapp-1/test/contact_test.js @@ -3,17 +3,17 @@ const config = require('../config'); const chai = require('chai'); const chaiHttp = require('chai-http'); const should = chai.should(); -const server = require('../app'); +const app = require('../app'); chai.use(chaiHttp); -describe('/GET', () => { +describe('/GET contact', () => { it('returns the contact page', (done) => { - chai.request(`http://localhost:${config.port}`) + chai.request(app) .get('/contact') .end((err, res) => { res.should.have.status(200); - res.text.should.contain('Contact Us'); + res.text.should.contain('Contact information'); done(); }); }); diff --git a/nodeapp-1/test/index_test.js b/nodeapp-1/test/index_test.js index e48fe76..74cb98c 100644 --- a/nodeapp-1/test/index_test.js +++ b/nodeapp-1/test/index_test.js @@ -3,13 +3,13 @@ const config = require('../config'); const chai = require('chai'); const chaiHttp = require('chai-http'); const should = chai.should(); -const server = require('../app'); +const app = require('../app'); chai.use(chaiHttp); describe('/GET', () => { it('returns the homepage', (done) => { - chai.request(`http://localhost:${config.port}`) + chai.request(app) .get('/') .end((err, res) => { res.should.have.status(200); @@ -17,4 +17,4 @@ describe('/GET', () => { done(); }); }); -}); +}); \ No newline at end of file diff --git a/nodeapp-1/test/who_test.js b/nodeapp-1/test/who_test.js index 18529c6..04d9bf4 100644 --- a/nodeapp-1/test/who_test.js +++ b/nodeapp-1/test/who_test.js @@ -3,18 +3,18 @@ const config = require('../config'); const chai = require('chai'); const chaiHttp = require('chai-http'); const should = chai.should(); -const server = require('../app'); +const app = require('../app'); chai.use(chaiHttp); -describe('/GET', () => { +describe('/GET who', () => { it('returns the who page', (done) => { - chai.request(`http://localhost:${config.port}`) + chai.request(app) .get('/who') .end((err, res) => { res.should.have.status(200); - res.text.should.contain('Who We Are'); + res.text.should.contain('Who are you?'); done(); }); }); -}); +}); \ No newline at end of file diff --git a/pipelines/multi-stage-cicd.yml b/pipelines/multi-stage-cicd.yml index fd3a2c0..108c341 100644 --- a/pipelines/multi-stage-cicd.yml +++ b/pipelines/multi-stage-cicd.yml @@ -27,6 +27,7 @@ variables: environmentNameDev: 'Development' environmentNameStaging: 'Staging' environmentNameProd: 'Production' + workingDirectory: 'nodeapp-1' stages: # ======================================== @@ -49,60 +50,43 @@ stages: echo "๐Ÿ“ฆ Installing dependencies..." npm ci displayName: 'Install dependencies' - workingDirectory: nodeapp-1 + workingDirectory: $(workingDirectory) # Run security audit - script: | echo "๐Ÿ”’ Running security audit..." npm audit --audit-level=high || true displayName: 'Security audit' - workingDirectory: nodeapp-1 + workingDirectory: $(workingDirectory) # Run linting - script: | echo "๐Ÿ” Running linter..." - npm run lint || echo "No lint script configured" + npm run lint displayName: 'Lint code' - workingDirectory: nodeapp-1 + workingDirectory: $(workingDirectory) continueOnError: true - # Run unit tests + # Run unit tests with Mocha - script: | - echo "๐Ÿงช Running unit tests..." - npm test -- --coverage || npm test + echo "๐Ÿงช Running Mocha tests..." + npm test displayName: 'Run unit tests' - workingDirectory: nodeapp-1 + workingDirectory: $(workingDirectory) # Publish test results - - task: PublishTestResults@2 - displayName: 'Publish test results' + - task: PublishHtmlReport@1 + displayName: 'Publish Mochawesome Report' inputs: - testResultsFormat: 'JUnit' - testResultsFiles: '**/test-*.xml' - searchFolder: nodeapp-1 - failTaskOnFailedTests: true + reportDir: '$(workingDirectory)/mochawesome-report' + tabName: 'Test Results' condition: succeededOrFailed() - # Publish code coverage - - task: PublishCodeCoverageResults@1 - displayName: 'Publish code coverage' - inputs: - codeCoverageTool: 'Cobertura' - summaryFileLocation: 'nodeapp-1/coverage/**/cobertura-coverage.xml' - condition: succeededOrFailed() - - # Build application - - script: | - echo "๐Ÿ”จ Building application..." - npm run build || echo "No build required" - displayName: 'Build application' - workingDirectory: nodeapp-1 - # Create deployment package - task: ArchiveFiles@2 displayName: 'Create deployment package' inputs: - rootFolderOrFile: 'nodeapp-1' + rootFolderOrFile: $(workingDirectory) includeRootFolder: false archiveType: 'zip' archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip' @@ -112,6 +96,15 @@ stages: artifact: drop displayName: 'Publish artifact' + # Publish to Azure Artifacts Dev Feed + - task: Npm@1 + displayName: 'Publish to Dev Feed' + inputs: + command: 'custom' + workingDir: $(workingDirectory) + customCommand: 'run publish:dev' + condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/develop')) + # ======================================== # STAGE 2: DEPLOY TO DEVELOPMENT # ======================================== @@ -185,21 +178,18 @@ stages: package: '$(Pipeline.Workspace)/drop/*.zip' runtimeStack: 'NODE|18-lts' - # Run integration tests + # Run integration tests using Chai-HTTP - script: | echo "๐Ÿงช Running integration tests..." - npm run test:integration || echo "No integration tests configured" + # Install test dependencies + npm ci + # Run tests against staging URL + export TEST_URL=https://$(webAppName)-staging.azurewebsites.net + npm test displayName: 'Integration tests' - workingDirectory: nodeapp-1 + workingDirectory: $(workingDirectory) continueOnError: true - # Performance test - - script: | - echo "โšก Running performance tests..." - # Add your performance testing tool here - echo "Performance baseline: Response time < 200ms" - displayName: 'Performance tests' - # ======================================== # STAGE 4: DEPLOY TO PRODUCTION # ======================================== @@ -221,7 +211,7 @@ stages: echo "๐Ÿ“‹ Pre-deployment checklist:" echo "โœ“ All tests passed" echo "โœ“ Security scan completed" - echo "โœ“ Performance benchmarks met" + echo "โœ“ Staging validation complete" displayName: 'Pre-deployment validation' deploy: @@ -253,6 +243,15 @@ stages: SourceSlot: 'staging' SwapWithProduction: true + # Publish to Azure Artifacts Prod Feed + - task: Npm@1 + displayName: 'Publish to Prod Feed' + inputs: + command: 'custom' + workingDir: $(workingDirectory) + customCommand: 'run publish:prod' + condition: succeeded() + postRouteTraffic: steps: # Monitor deployment @@ -284,33 +283,3 @@ stages: echo "Build: $(Build.BuildId)" echo "Commit: $(Build.SourceVersion)" displayName: 'Deployment summary' - -# ======================================== -# STAGE 5: POST-DEPLOYMENT -# ======================================== -- stage: PostDeployment - displayName: 'Post-Deployment Tasks' - dependsOn: DeployProd - condition: succeeded() - jobs: - - job: PostDeploymentTasks - displayName: 'Run post-deployment tasks' - steps: - # Tag the release - - script: | - echo "๐Ÿท๏ธ Tagging release..." - git tag -a "v$(Build.BuildId)" -m "Release $(Build.BuildId)" - displayName: 'Tag release' - condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main')) - - # Update documentation - - script: | - echo "๐Ÿ“š Updating deployment documentation..." - echo "Deployment completed at: $(date)" >> deployment-log.md - displayName: 'Update documentation' - - # Send notifications - - script: | - echo "๐Ÿ“ง Sending deployment notifications..." - # Add your notification logic here (Teams, Slack, email) - displayName: 'Send notifications' diff --git a/pipelines/single-stage-ci.yml b/pipelines/single-stage-ci.yml index bb0399d..1c2296e 100644 --- a/pipelines/single-stage-ci.yml +++ b/pipelines/single-stage-ci.yml @@ -16,6 +16,7 @@ pool: variables: nodeVersion: '18.x' artifactName: 'drop' + workingDirectory: 'nodeapp-1' steps: # Step 1: Setup Node.js environment @@ -29,64 +30,49 @@ steps: echo "Installing npm dependencies..." npm ci displayName: 'npm install' - workingDirectory: nodeapp-1 + workingDirectory: $(workingDirectory) # Step 3: Run linting - script: | echo "Running ESLint..." - npm run lint || echo "No lint script found, skipping..." + npm run lint displayName: 'Run linting' - workingDirectory: nodeapp-1 + workingDirectory: $(workingDirectory) continueOnError: true -# Step 4: Run unit tests +# Step 4: Run unit tests with Mocha - script: | - echo "Running tests..." + echo "Running Mocha tests..." npm test displayName: 'Run unit tests' - workingDirectory: nodeapp-1 + workingDirectory: $(workingDirectory) -# Step 5: Generate test results -- task: PublishTestResults@2 - displayName: 'Publish test results' +# Step 5: Publish test results (mochawesome generates HTML reports) +- task: PublishHtmlReport@1 + displayName: 'Publish Mochawesome Report' inputs: - testResultsFormat: 'JUnit' - testResultsFiles: '**/test-results.xml' - searchFolder: nodeapp-1 + reportDir: '$(workingDirectory)/mochawesome-report' + tabName: 'Test Results' condition: succeededOrFailed() -# Step 6: Code coverage +# Step 6: Security audit - script: | - echo "Generating code coverage..." - npm run coverage || echo "No coverage script found, skipping..." - displayName: 'Generate code coverage' - workingDirectory: nodeapp-1 + echo "Running npm audit..." + npm audit --audit-level=high + displayName: 'Security audit' + workingDirectory: $(workingDirectory) continueOnError: true -- task: PublishCodeCoverageResults@1 - displayName: 'Publish code coverage' - inputs: - codeCoverageTool: 'Cobertura' - summaryFileLocation: 'nodeapp-1/coverage/cobertura-coverage.xml' - condition: succeededOrFailed() - -# Step 7: Build the application -- script: | - echo "Building application..." - npm run build || npm run compile || echo "No build script, using source files..." - displayName: 'Build application' - workingDirectory: nodeapp-1 - -# Step 8: Create artifact +# Step 7: Create artifact (no build step - it's a runtime app) - task: ArchiveFiles@2 displayName: 'Archive application files' inputs: - rootFolderOrFile: 'nodeapp-1' + rootFolderOrFile: $(workingDirectory) includeRootFolder: false archiveType: 'zip' archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip' -# Step 9: Publish artifact +# Step 8: Publish artifact - task: PublishBuildArtifacts@1 displayName: 'Publish artifact' inputs: @@ -94,6 +80,13 @@ steps: ArtifactName: $(artifactName) publishLocation: 'Container' +# Step 9: Publish to Azure Artifacts (optional) +- script: | + echo "Ready to publish to Azure Artifacts feed" + echo "Use: npm run publish:dev or npm run publish:prod" + displayName: 'Azure Artifacts info' + condition: succeeded() + # Success notification - script: | echo "โœ… Build completed successfully!"