Staging - Deploy PR #258
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }} |