Skip to content

Staging - Deploy PR #258

Staging - Deploy PR

Staging - Deploy PR #258

name: Staging - Deploy PR
# **What it does**: To deploy PRs to a Heroku staging environment.
# **Why we have it**: To deploy with high visibility in case of failures.
# **Who does it impact**: All contributors.
on:
workflow_run:
workflows:
- 'Staging - Build PR'
types:
- completed
env:
EARLY_ACCESS_SCRIPT_PATH: script/early-access/clone-for-build.js
EARLY_ACCESS_SUPPORT_FILES: script/package.json
# In this specific workflow relationship, the `github.event.workflow_run.pull_requests`
# array will always contain only 1 item! Specifically, it will contain the PR associated
# with the `github.event.workflow_run.head_branch` that triggered the preceding
# `pull_request` event that triggered the "Staging - Build PR" workflow.
PR_URL: ${{ github.event.workflow_run.repository.html_url }}/pull/${{ github.event.workflow_run.pull_requests[0].number }}
jobs:
prepare:
if: >-
${{
github.event.workflow_run.conclusion == 'success' &&
(github.repository == 'github/docs-internal' || github.repository == 'github/docs')
}}
runs-on: ubuntu-latest
timeout-minutes: 5
concurrency:
group: staging_${{ github.event.workflow_run.head_branch }}
cancel-in-progress: true
outputs:
source_blob_url: ${{ steps.build-source.outputs.download_url }}
check_run_id: ${{ steps.create-check-run.outputs.check_run_id }}
check_run_name: ${{ steps.create-check-run.outputs.check_run_name }}
steps:
- name: Dump event context
env:
GITHUB_EVENT_CONTEXT: ${{ toJSON(github.event) }}
run: echo "$GITHUB_EVENT_CONTEXT"
- name: Create check run
id: create-check-run
uses: actions/github-script@2b34a689ec86a68d8ab9478298f91d5401337b7d
with:
script: |
// Create a check run
const CHECK_NAME = '${{ github.workflow }} / ${{ github.job }} (${{ github.event_name }})'
const { data } = await github.checks.create({
name: CHECK_NAME,
head_sha: '${{github.sha}}',
owner: 'github',
repo: 'docs-internal',
status: 'in_progress',
started_at: new Date()
})
core.setOutput('check_run_id', data.id)
core.setOutput('check_run_name', CHECK_NAME)
- name: Download build artifact
uses: dawidd6/action-download-artifact@b9571484721e8187f1fd08147b497129f8972c74
with:
workflow: ${{ github.event.workflow_run.workflow_id }}
run_id: ${{ github.event.workflow_run.id }}
name: pr_build
path: ./
- name: Show contents
run: ls -l
- name: Extract the archive
run: |
tar -xf app.tar -C ./
rm app.tar
- name: Show contents again
run: ls -l
- if: ${{ github.repository == 'github/docs-internal' }}
name: Setup node to clone early access
uses: actions/setup-node@38d90ce44d5275ad62cc48384b3d8a58c500bb5f
with:
node-version: 16.x
cache: npm
- if: ${{ github.repository == 'github/docs-internal' }}
name: Download the script to clone early access
uses: Bhacaz/checkout-files@c8f01756bfd894ba746d5bf48205e19000b0742b
with:
files: ${{ env.EARLY_ACCESS_SCRIPT_PATH }} ${{ env.EARLY_ACCESS_SUPPORT_FILES }}
token: ${{ secrets.GITHUB_TOKEN }}
# Add any dependencies that are needed for this workflow below
- if: ${{ github.repository == 'github/docs-internal' }}
name: Install temporary development-only dependencies
run: npm install --no-save rimraf
- if: ${{ github.repository == 'github/docs-internal' }}
name: Clone early access
run: node ${{ env.EARLY_ACCESS_SCRIPT_PATH }}
env:
DOCUBOT_REPO_PAT: ${{ secrets.DOCUBOT_REPO_PAT }}
GIT_BRANCH: ${{ github.event.workflow_run.head_branch }}
# Remove any dependencies installed for this workflow below
- if: ${{ github.repository == 'github/docs-internal' }}
name: Remove development-only dependencies
run: npm prune --production
- if: ${{ github.repository == 'github/docs-internal' }}
name: Delete the script directory after cloning early access
run: rm -rf script/
- name: Create a gzipped archive
run: |
touch app.tar.gz
tar --exclude=app.tar.gz -czf app.tar.gz ./
- name: Install Heroku client development-only dependency
run: npm install --no-save heroku-client
- name: Create a Heroku build source
id: build-source
uses: actions/github-script@2b34a689ec86a68d8ab9478298f91d5401337b7d
env:
HEROKU_API_TOKEN: ${{ secrets.HEROKU_API_TOKEN }}
with:
script: |
const { owner, repo } = context.repo
if (owner !== 'github') {
throw new Error(`Repository owner must be 'github' but was: ${owner}`)
}
if (repo !== 'docs-internal' && repo !== 'docs') {
throw new Error(`Repository name must be either 'docs-internal' or 'docs' but was: ${repo}`)
}
const Heroku = require('heroku-client')
const heroku = new Heroku({ token: process.env.HEROKU_API_TOKEN })
const { source_blob: sourceBlob } = await heroku.post('/sources')
const { put_url: uploadUrl, get_url: downloadUrl } = sourceBlob
core.setOutput('upload_url', uploadUrl)
core.setOutput('download_url', downloadUrl)
# See: https://devcenter.heroku.com/articles/build-and-release-using-the-api#sources-endpoint
- name: Upload to the Heroku build source
run: |
curl '${{ steps.build-source.outputs.upload_url }}' \
-X PUT \
-H 'Content-Type:' \
--data-binary @app.tar.gz
- name: Send Slack notification if workflow fails
uses: someimportantcompany/github-actions-slack-message@0b470c14b39da4260ed9e3f9a4f1298a74ccdefd
if: ${{ failure() }}
with:
channel: ${{ secrets.DOCS_STAGING_DEPLOYMENT_FAILURES_SLACK_CHANNEL_ID }}
bot-token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }}
color: failure
text: Staging preparation failed for PR ${{ env.PR_URL }} at commit ${{ github.sha }}. See https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
deploy:
needs: prepare
runs-on: ubuntu-latest
timeout-minutes: 10
concurrency:
group: staging_${{ github.event.workflow_run.head_branch }}
cancel-in-progress: true
steps:
- name: Check out repo's default branch
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Setup node
uses: actions/setup-node@38d90ce44d5275ad62cc48384b3d8a58c500bb5f
with:
node-version: 16.x
cache: npm
- name: Install dependencies
run: npm ci
- name: Install one-off development-only dependencies
run: npm install --no-save --include=optional esm
- name: Deploy
id: deploy
uses: actions/github-script@2b34a689ec86a68d8ab9478298f91d5401337b7d
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HEROKU_API_TOKEN: ${{ secrets.HEROKU_API_TOKEN }}
HYDRO_ENDPOINT: ${{ secrets.HYDRO_ENDPOINT }}
HYDRO_SECRET: ${{ secrets.HYDRO_SECRET }}
PR_URL: ${{ env.PR_URL }}
SOURCE_BLOB_URL: ${{ needs.prepare.outputs.source_blob_url }}
CHECK_RUN_ID: ${{ needs.prepare.outputs.check_run_id }}
CHECK_RUN_NAME: ${{ needs.prepare.outputs.check_run_name }}
with:
script: |
const { GITHUB_TOKEN, HEROKU_API_TOKEN } = process.env
// Exit if GitHub Actions PAT is not found
if (!GITHUB_TOKEN) {
throw new Error('You must supply a GITHUB_TOKEN environment variable!')
}
// Exit if Heroku API token is not found
if (!HEROKU_API_TOKEN) {
throw new Error('You must supply a HEROKU_API_TOKEN environment variable!')
}
// Workaround to allow us to load ESM files with `require(...)`
const esm = require('esm')
require = esm({})
const { default: parsePrUrl } = require('./script/deployment/parse-pr-url')
const { default: getOctokit } = require('./script/helpers/github')
const { default: deployToStaging } = require('./script/deployment/deploy-to-staging')
// This helper uses the `GITHUB_TOKEN` implicitly!
// We're using our usual version of Octokit vs. the provided `github`
// instance to avoid versioning discrepancies.
const octokit = getOctokit()
try {
const { PR_URL, SOURCE_BLOB_URL, CHECK_RUN_ID, CHECK_RUN_NAME } = process.env
const { owner, repo, pullNumber } = parsePrUrl(PR_URL)
if (!owner || !repo || !pullNumber) {
throw new Error(`'pullRequestUrl' input must match URL format 'https://github.com/github/(docs|docs-internal)/pull/123' but was '${PR_URL}'`)
}
const { data: pullRequest } = await octokit.pulls.get({
owner,
repo,
pull_number: pullNumber
})
await deployToStaging({
octokit,
pullRequest,
forceRebuild: false,
// These parameters will ONLY be set by Actions
sourceBlobUrl: SOURCE_BLOB_URL,
runId: context.runId
})
await octokit.checks.update({
owner: 'github',
repo: 'docs-internal',
conclusion: 'success',
status: 'completed',
name: CHECK_RUN_NAME,
check_run_id: CHECK_RUN_ID,
output: {
title: 'Successfully deployed to staging',
summary: 'Success',
text: 'Succeeded! 🚀\n\nSee full logs: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}'
},
completed_at: new Date()
})
} catch (error) {
await octokit.checks.update({
owner: 'github',
repo: 'docs-internal',
conclusion: 'failure',
status: 'completed',
name: CHECK_RUN_NAME,
check_run_id: CHECK_RUN_ID,
output: {
title: `Failed to deploy to staging: ${error.message}`,
summary: error.message,
text: 'Failed! 🚫\n\nSee full logs: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}'
}
completed_at: new Date()
})
console.error(`Failed to deploy to staging: ${error.message}`)
console.error(error)
throw error
}
- name: Mark the deployment as inactive if timed out
if: ${{ steps.deploy.outcome == 'cancelled' }}
uses: actions/github-script@2b34a689ec86a68d8ab9478298f91d5401337b7d
with:
script: |
// TODO: Find the relevant deployment
// TODO: Create a new deployment status for it as "inactive"
return 'TODO'
- name: Send Slack notification if workflow fails
uses: someimportantcompany/github-actions-slack-message@0b470c14b39da4260ed9e3f9a4f1298a74ccdefd
if: ${{ failure() }}
with:
channel: ${{ secrets.DOCS_STAGING_DEPLOYMENT_FAILURES_SLACK_CHANNEL_ID }}
bot-token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }}
color: failure
text: Staging deployment failed for PR ${{ env.PR_URL }} at commit ${{ github.sha }}. See https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}