diff --git a/.github/workflows/node-azure-deploy.yml b/.github/workflows/node-azure-deploy.yml new file mode 100644 index 0000000..1eedfcc --- /dev/null +++ b/.github/workflows/node-azure-deploy.yml @@ -0,0 +1,228 @@ +name: Node.js CI/CD to Azure + +# Teaching example for GitHub Actions with Azure deployment + +on: + push: + branches: [ main, master ] + paths: + - 'nodeapp-1/**' + - '.github/workflows/node-azure-deploy.yml' + pull_request: + branches: [ main, master ] + workflow_dispatch: + inputs: + environment: + description: 'Environment to deploy to' + required: true + default: 'development' + type: choice + options: + - development + - staging + - production + +env: + NODE_VERSION: '18.x' + AZURE_WEBAPP_NAME: 'webapp-az400-demo' + AZURE_WEBAPP_PACKAGE_PATH: './nodeapp-1' + WORKING_DIRECTORY: 'nodeapp-1' + +jobs: + # ======================================== + # JOB 1: BUILD AND TEST + # ======================================== + build: + name: Build and Test + runs-on: ubuntu-latest + + steps: + - name: ๐Ÿ“ฅ Checkout code + uses: actions/checkout@v4 + + - name: ๐Ÿ”ง Setup Node.js ${{ env.NODE_VERSION }} + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + cache-dependency-path: '${{ env.WORKING_DIRECTORY }}/package-lock.json' + + - name: ๐Ÿ“ฆ Install dependencies + run: npm ci + working-directory: ${{ env.WORKING_DIRECTORY }} + + - name: ๐Ÿ”’ Security audit + run: npm audit --audit-level=high + working-directory: ${{ env.WORKING_DIRECTORY }} + continue-on-error: true + + - name: ๐Ÿงน Lint code + run: npm run lint + working-directory: ${{ env.WORKING_DIRECTORY }} + + - name: ๐Ÿงช Run tests with Mocha + run: npm test + working-directory: ${{ env.WORKING_DIRECTORY }} + + - name: ๐Ÿ“Š Upload test results + uses: actions/upload-artifact@v4 + if: always() + with: + 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.WORKING_DIRECTORY }} + retention-days: 5 + + # ======================================== + # JOB 2: DEPLOY TO DEVELOPMENT + # ======================================== + deploy-dev: + name: Deploy to Development + runs-on: ubuntu-latest + needs: build + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + environment: + name: development + url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} + + steps: + - name: ๐Ÿ“ฅ Download artifact + uses: actions/download-artifact@v4 + with: + name: node-app + path: ./app + + - name: ๐Ÿ” Login to Azure + uses: azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: ๐Ÿš€ Deploy to Azure Web App + id: deploy-to-webapp + uses: azure/webapps-deploy@v3 + with: + app-name: ${{ env.AZURE_WEBAPP_NAME }}-dev + package: ./app + startup-command: 'npm start' + + - name: ๐Ÿ”ฅ Smoke test + run: | + sleep 30 + response=$(curl -s -o /dev/null -w "%{http_code}" https://${{ env.AZURE_WEBAPP_NAME }}-dev.azurewebsites.net) + if [ $response -eq 200 ]; then + echo "โœ… Smoke test passed!" + else + echo "โŒ Smoke test failed with status code: $response" + exit 1 + fi + + # ======================================== + # JOB 3: DEPLOY TO STAGING + # ======================================== + deploy-staging: + name: Deploy to Staging + runs-on: ubuntu-latest + needs: deploy-dev + environment: + name: staging + 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: + name: node-app + path: ./app + + - name: ๐Ÿ” Login to Azure + uses: azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: ๐Ÿš€ Deploy to staging slot + id: deploy-to-webapp + uses: azure/webapps-deploy@v3 + with: + app-name: ${{ env.AZURE_WEBAPP_NAME }} + slot-name: staging + package: ./app + + - name: ๐Ÿงช Run integration tests + run: | + 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 + # ======================================== + deploy-prod: + name: Deploy to Production + runs-on: ubuntu-latest + needs: deploy-staging + environment: + name: production + url: ${{ steps.swap-slots.outputs.webapp-url }} + + steps: + - name: ๐Ÿ” Login to Azure + uses: azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: ๐Ÿ”„ Swap staging to production + id: swap-slots + run: | + az webapp deployment slot swap \ + --resource-group rg-az400-demo \ + --name ${{ env.AZURE_WEBAPP_NAME }} \ + --slot staging \ + --target-slot production + + echo "webapp-url=https://${{ env.AZURE_WEBAPP_NAME }}.azurewebsites.net" >> $GITHUB_OUTPUT + + - name: ๐Ÿท๏ธ Create release tag + if: success() + uses: actions/github-script@v7 + with: + script: | + const tag = `v${context.runNumber}`; + await github.rest.git.createRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: `refs/tags/${tag}`, + sha: context.sha + }); + console.log(`โœ… Created tag: ${tag}`); + +# ======================================== +# REUSABLE WORKFLOW: SECURITY SCAN +# ======================================== + security-scan: + name: Security Scanning + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - name: ๐Ÿ“ฅ Checkout code + uses: actions/checkout@v4 + + - name: ๐Ÿ” Run Dependabot scan + uses: github/super-linter@v5 + env: + DEFAULT_BRANCH: main + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VALIDATE_JAVASCRIPT_ES: true + VALIDATE_JSON: true + VALIDATE_YAML: true diff --git a/pipelines/multi-stage-cicd.yml b/pipelines/multi-stage-cicd.yml new file mode 100644 index 0000000..108c341 --- /dev/null +++ b/pipelines/multi-stage-cicd.yml @@ -0,0 +1,285 @@ +# Multi-Stage CI/CD Pipeline for Node.js to Azure +# Teaching example for full DevOps pipeline with environments + +trigger: + branches: + include: + - master + - main + paths: + include: + - nodeapp-1/* + +pr: + branches: + include: + - master + - main + +pool: + vmImage: 'ubuntu-latest' + +variables: + nodeVersion: '18.x' + azureSubscription: 'AZ400-ServiceConnection' # Update with your service connection + resourceGroupName: 'rg-az400-demo' + webAppName: 'webapp-az400-$(Build.BuildId)' + environmentNameDev: 'Development' + environmentNameStaging: 'Staging' + environmentNameProd: 'Production' + workingDirectory: 'nodeapp-1' + +stages: +# ======================================== +# STAGE 1: BUILD +# ======================================== +- stage: Build + displayName: 'Build and Test' + jobs: + - job: BuildJob + displayName: 'Build Node.js Application' + steps: + # Setup Node.js + - task: NodeTool@0 + displayName: 'Install Node.js $(nodeVersion)' + inputs: + versionSpec: $(nodeVersion) + + # Install dependencies + - script: | + echo "๐Ÿ“ฆ Installing dependencies..." + npm ci + displayName: 'Install dependencies' + workingDirectory: $(workingDirectory) + + # Run security audit + - script: | + echo "๐Ÿ”’ Running security audit..." + npm audit --audit-level=high || true + displayName: 'Security audit' + workingDirectory: $(workingDirectory) + + # Run linting + - script: | + echo "๐Ÿ” Running linter..." + npm run lint + displayName: 'Lint code' + workingDirectory: $(workingDirectory) + continueOnError: true + + # Run unit tests with Mocha + - script: | + echo "๐Ÿงช Running Mocha tests..." + npm test + displayName: 'Run unit tests' + workingDirectory: $(workingDirectory) + + # Publish test results + - task: PublishHtmlReport@1 + displayName: 'Publish Mochawesome Report' + inputs: + reportDir: '$(workingDirectory)/mochawesome-report' + tabName: 'Test Results' + condition: succeededOrFailed() + + # Create deployment package + - task: ArchiveFiles@2 + displayName: 'Create deployment package' + inputs: + rootFolderOrFile: $(workingDirectory) + includeRootFolder: false + archiveType: 'zip' + archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip' + + # Publish artifact + - publish: $(Build.ArtifactStagingDirectory) + 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 +# ======================================== +- stage: DeployDev + displayName: 'Deploy to Development' + dependsOn: Build + condition: succeeded() + jobs: + - deployment: DeployToDev + displayName: 'Deploy to Dev Environment' + environment: $(environmentNameDev) + strategy: + runOnce: + deploy: + steps: + # Download artifact + - download: current + artifact: drop + displayName: 'Download artifact' + + # Deploy to Azure Web App + - task: AzureWebApp@1 + displayName: 'Deploy to Dev Web App' + inputs: + azureSubscription: $(azureSubscription) + appType: 'webAppLinux' + appName: '$(webAppName)-dev' + package: '$(Pipeline.Workspace)/drop/*.zip' + runtimeStack: 'NODE|18-lts' + startUpCommand: 'npm start' + + # Smoke test + - script: | + echo "๐Ÿ”ฅ Running smoke tests..." + sleep 30 + curl -f https://$(webAppName)-dev.azurewebsites.net || exit 1 + echo "โœ… Smoke tests passed!" + displayName: 'Smoke test' + +# ======================================== +# STAGE 3: DEPLOY TO STAGING +# ======================================== +- stage: DeployStaging + displayName: 'Deploy to Staging' + dependsOn: DeployDev + condition: succeeded() + jobs: + - deployment: DeployToStaging + displayName: 'Deploy to Staging Environment' + environment: + name: $(environmentNameStaging) + resourceType: VirtualMachine + strategy: + runOnce: + deploy: + steps: + # Download artifact + - download: current + artifact: drop + displayName: 'Download artifact' + + # Deploy to staging slot + - task: AzureWebApp@1 + displayName: 'Deploy to Staging Slot' + inputs: + azureSubscription: $(azureSubscription) + appType: 'webAppLinux' + appName: '$(webAppName)-staging' + deployToSlotOrASE: true + slotName: 'staging' + package: '$(Pipeline.Workspace)/drop/*.zip' + runtimeStack: 'NODE|18-lts' + + # Run integration tests using Chai-HTTP + - script: | + echo "๐Ÿงช Running integration tests..." + # Install test dependencies + npm ci + # Run tests against staging URL + export TEST_URL=https://$(webAppName)-staging.azurewebsites.net + npm test + displayName: 'Integration tests' + workingDirectory: $(workingDirectory) + continueOnError: true + +# ======================================== +# STAGE 4: DEPLOY TO PRODUCTION +# ======================================== +- stage: DeployProd + displayName: 'Deploy to Production' + dependsOn: DeployStaging + condition: succeeded() + jobs: + - deployment: DeployToProd + displayName: 'Deploy to Production Environment' + environment: + name: $(environmentNameProd) + resourceType: VirtualMachine + strategy: + runOnce: + preDeploy: + steps: + - script: | + echo "๐Ÿ“‹ Pre-deployment checklist:" + echo "โœ“ All tests passed" + echo "โœ“ Security scan completed" + echo "โœ“ Staging validation complete" + displayName: 'Pre-deployment validation' + + deploy: + steps: + # Download artifact + - download: current + artifact: drop + displayName: 'Download artifact' + + # Blue-Green deployment + - task: AzureWebApp@1 + displayName: 'Deploy to Production (Blue-Green)' + inputs: + azureSubscription: $(azureSubscription) + appType: 'webAppLinux' + appName: $(webAppName) + deployToSlotOrASE: true + slotName: 'staging' + package: '$(Pipeline.Workspace)/drop/*.zip' + runtimeStack: 'NODE|18-lts' + + # Swap slots + - task: AzureAppServiceManage@0 + displayName: 'Swap staging to production' + inputs: + azureSubscription: $(azureSubscription) + WebAppName: $(webAppName) + ResourceGroupName: $(resourceGroupName) + 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 + - script: | + echo "๐Ÿ“Š Monitoring deployment health..." + # Add Application Insights queries here + echo "โœ… Deployment healthy" + displayName: 'Monitor deployment' + + on: + failure: + steps: + # Rollback on failure + - task: AzureAppServiceManage@0 + displayName: 'Rollback: Swap slots back' + inputs: + azureSubscription: $(azureSubscription) + WebAppName: $(webAppName) + ResourceGroupName: $(resourceGroupName) + SourceSlot: 'production' + TargetSlot: 'staging' + SwapWithProduction: false + + success: + steps: + - script: | + echo "๐ŸŽ‰ Production deployment successful!" + echo "URL: https://$(webAppName).azurewebsites.net" + echo "Build: $(Build.BuildId)" + echo "Commit: $(Build.SourceVersion)" + displayName: 'Deployment summary' diff --git a/pipelines/single-stage-ci.yml b/pipelines/single-stage-ci.yml new file mode 100644 index 0000000..1c2296e --- /dev/null +++ b/pipelines/single-stage-ci.yml @@ -0,0 +1,97 @@ +# Single-Stage CI Pipeline for Node.js +# Teaching example for basic continuous integration + +trigger: + branches: + include: + - master + - main + paths: + include: + - nodeapp-1/* + +pool: + vmImage: 'ubuntu-latest' + +variables: + nodeVersion: '18.x' + artifactName: 'drop' + workingDirectory: 'nodeapp-1' + +steps: +# Step 1: Setup Node.js environment +- task: NodeTool@0 + displayName: 'Install Node.js $(nodeVersion)' + inputs: + versionSpec: $(nodeVersion) + +# Step 2: Install dependencies +- script: | + echo "Installing npm dependencies..." + npm ci + displayName: 'npm install' + workingDirectory: $(workingDirectory) + +# Step 3: Run linting +- script: | + echo "Running ESLint..." + npm run lint + displayName: 'Run linting' + workingDirectory: $(workingDirectory) + continueOnError: true + +# Step 4: Run unit tests with Mocha +- script: | + echo "Running Mocha tests..." + npm test + displayName: 'Run unit tests' + workingDirectory: $(workingDirectory) + +# Step 5: Publish test results (mochawesome generates HTML reports) +- task: PublishHtmlReport@1 + displayName: 'Publish Mochawesome Report' + inputs: + reportDir: '$(workingDirectory)/mochawesome-report' + tabName: 'Test Results' + condition: succeededOrFailed() + +# Step 6: Security audit +- script: | + echo "Running npm audit..." + npm audit --audit-level=high + displayName: 'Security audit' + workingDirectory: $(workingDirectory) + continueOnError: true + +# Step 7: Create artifact (no build step - it's a runtime app) +- task: ArchiveFiles@2 + displayName: 'Archive application files' + inputs: + rootFolderOrFile: $(workingDirectory) + includeRootFolder: false + archiveType: 'zip' + archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip' + +# Step 8: Publish artifact +- task: PublishBuildArtifacts@1 + displayName: 'Publish artifact' + inputs: + PathtoPublish: '$(Build.ArtifactStagingDirectory)' + 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!" + echo "Build ID: $(Build.BuildId)" + echo "Source branch: $(Build.SourceBranch)" + echo "Commit: $(Build.SourceVersion)" + displayName: 'Build summary' + condition: succeeded()