diff --git a/.github/workflows/azure-staging-build-deploy.yml b/.github/workflows/azure-staging-build-deploy.yml index 47059ba334..6c53760ea5 100644 --- a/.github/workflows/azure-staging-build-deploy.yml +++ b/.github/workflows/azure-staging-build-deploy.yml @@ -20,12 +20,151 @@ permissions: contents: read deployments: write +# This allows a subsequently queued workflow run to take priority over +# previously queued runs but NOT interrupt currently executing runs +concurrency: + group: 'staging-env @ ${{ github.head_ref || github.run_id }} for ${{ github.event.number || github.event.inputs.PR_NUMBER }}' + cancel-in-progress: true + jobs: azure-staging-build-and-deploy: if: ${{ github.repository == 'github/docs-internal' }} runs-on: ubuntu-latest + timeout-minutes: 20 + environment: + # TODO: Update name and url to point to a specific slot for the branch/PR + name: staging-env + url: ${{ secrets.STAGING_APP_URL }} + env: + PR_NUMBER: ${{ github.event.number || github.event.inputs.PR_NUMBER || github.run_id }} + COMMIT_REF: ${{ github.event.pull_request.head.sha || github.event.inputs.COMMIT_REF }} + IMAGE_REPO: ${{ github.repository }}/pr-${{ github.event.number || github.event.inputs.PR_NUMBER || github.run_id }} + RESOURCE_GROUP_NAME: docs-staging + APP_SERVICE_NAME: ghdocs-staging + SLOT_NAME: canary steps: - - name: 'No-op' + - name: 'Az CLI login' + uses: azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf + with: + creds: ${{ secrets.PROD_AZURE_CREDENTIALS }} + + - name: 'Docker login' + uses: azure/docker-login@81744f9799e7eaa418697cb168452a2882ae844a + with: + login-server: ${{ secrets.NONPROD_REGISTRY_SERVER }} + username: ${{ secrets.NONPROD_REGISTRY_USERNAME }} + password: ${{ secrets.NONPROD_REGISTRY_PASSWORD }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@94ab11c41e45d028884a99163086648e898eed25 + + - name: Check out repo + uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 + with: + ref: ${{ env.COMMIT_REF }} + # To prevent issues with cloning early access content later + persist-credentials: 'false' + lfs: 'true' + + - name: Check out LFS objects + run: git lfs checkout + + - name: 'Set env vars' run: | - echo "No-op" + # Image tag is unique to each workflow run so that it always triggers a new deployment + echo "DOCKER_IMAGE=${{ secrets.NONPROD_REGISTRY_SERVER }}/${{ env.IMAGE_REPO }}:${{ env.COMMIT_REF }}-${{ github.run_number }}-${{ github.run_attempt }}" >> $GITHUB_ENV + + - name: Setup node + uses: actions/setup-node@1f8c6b94b26d0feae1e387ca63ccbdc44d27b561 + with: + node-version: 16.14.x + cache: npm + + - name: Clone docs-early-access + uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 + with: + repository: github/docs-early-access + token: ${{ secrets.DOCUBOT_REPO_PAT }} + path: docs-early-access + ref: main + + - name: Merge docs-early-access repo's folders + run: .github/actions-scripts/merge-early-access.sh + + - name: 'Build and push image' + uses: docker/build-push-action@7f9d37fa544684fb73bfe4835ed7214c255ce02b + with: + context: . + push: true + target: production + tags: ${{ env.DOCKER_IMAGE }} + build-args: | + BUILD_SHA=${{ env.COMMIT_REF }} + + - name: 'Update docker-compose.staging.yaml template file' + run: | + sed 's|#{IMAGE}#|${{ env.DOCKER_IMAGE }}|g' docker-compose.staging.tmpl.yaml > docker-compose.staging.yaml + + - name: 'Apply updated docker-compose.staging.yaml config to deployment slot' + run: | + az webapp config container set --multicontainer-config-type COMPOSE --multicontainer-config-file docker-compose.staging.yaml --slot ${{ env.SLOT_NAME }} -n ${{ env.APP_SERVICE_NAME }} -g ${{ env.RESOURCE_GROUP_NAME }} + + # Watch deployment slot instances to see when all the instances are ready + - name: Check that deployment slot is ready + uses: actions/github-script@2b34a689ec86a68d8ab9478298f91d5401337b7d + env: + CHECK_INTERVAL: 10000 + with: + script: | + const { execSync } = require('child_process') + + const slotName = process.env.SLOT_NAME + const appServiceName = process.env.APP_SERVICE_NAME + const resourceGroupName = process.env.RESOURCE_GROUP_NAME + + const getStatesForSlot = (slot, appService, resourceGroup) => { + return JSON.parse( + execSync( + `az webapp list-instances --slot ${slot} --query "[].state" -n ${appService} -g ${resourceGroup}`, + { encoding: 'utf8' } + ) + ) + } + + let hasStopped = false + const waitDuration = parseInt(process.env.CHECK_INTERVAL, 10) || 10000 + async function doCheck() { + const states = getStatesForSlot(slotName, appServiceName, resourceGroupName) + console.log(`Instance states:`, states) + + // We must wait until at-least 1 instance has STOPPED to know we're looking at the "next" deployment and not the "previous" one + // That way we don't immediately succeed just because all the previous instances were READY + if (!hasStopped) { + hasStopped = states.some((s) => s === 'STOPPED') + } + + const isAllReady = states.every((s) => s === 'READY') + + if (hasStopped && isAllReady) { + process.exit(0) // success + } + + console.log(`checking again in ${waitDuration}ms`) + setTimeout(doCheck, waitDuration) + } + + doCheck() + + - name: 'Swap deployment slot to production' + run: | + az webapp deployment slot swap --slot ${{ env.SLOT_NAME }} --target-slot production -n ${{ env.APP_SERVICE_NAME }} -g ${{ env.RESOURCE_GROUP_NAME }} + + - name: Send Slack notification if workflow failed + uses: someimportantcompany/github-actions-slack-message@f8d28715e7b8a4717047d23f48c39827cacad340 + if: ${{ failure() }} + with: + channel: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} + bot-token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} + color: failure + text: Staging deployment (Azure) failed at commit ${{ env.COMMIT_REF }}. See https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} diff --git a/docker-compose.staging.tmpl.yaml b/docker-compose.staging.tmpl.yaml new file mode 100644 index 0000000000..96dc3acb6f --- /dev/null +++ b/docker-compose.staging.tmpl.yaml @@ -0,0 +1,43 @@ +version: '3.7' + +services: + ghdocs-staging: + image: '#{IMAGE}#' + ports: + - '4000:4000' + environment: + NODE_ENV: ${NODE_ENV} + NODE_OPTIONS: ${NODE_OPTIONS} + DD_API_KEY: ${DD_API_KEY} + COOKIE_SECRET: ${COOKIE_SECRET} + HYDRO_ENDPOINT: ${HYDRO_ENDPOINT} + HYDRO_SECRET: ${HYDRO_SECRET} + HAYSTACK_URL: ${HAYSTACK_URL} + HEROKU_APP_NAME: ${HEROKU_APP_NAME} + ENABLED_LANGUAGES: ${ENABLED_LANGUAGES} + DEPLOYMENT_ENV: ${DEPLOYMENT_ENV} + HEROKU_PRODUCTION_APP: true + PORT: 4000 + DD_AGENT_HOST: datadog-agent + SIGSCI_RPC_ADDRESS: sigsci-agent:8000 + depends_on: + - datadog-agent + restart: always + + datadog-agent: + image: datadog/dogstatsd:7.32.4 + ports: + - '8125:8125' + environment: + DD_API_KEY: ${DD_API_KEY} + DD_AGENT_HOST: datadog-agent + DD_HISTOGRAM_PERCENTILES: 0.99 0.95 0.50 + + sigsci-agent: + image: signalsciences/sigsci-agent + ports: + - '8000:8000' + environment: + SIGSCI_RPC_ADDRESS: 0.0.0.0:8000 + SIGSCI_ACCESSKEY: ${SIGSCI_ACCESSKEYID} + SIGSCI_SECRETACCESSKEY: ${SIGSCI_SECRETACCESSKEY}