Deploy Website #4369
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: Deploy Website | |
on: | |
workflow_run: | |
workflows: | |
- Build Website | |
types: | |
- completed | |
jobs: | |
deploy: | |
runs-on: ubuntu-20.04 | |
if: github.event.workflow_run.conclusion == 'success' | |
steps: | |
- name: Set Build Context File Prefix | |
id: build_context_file_prefix | |
run: | | |
if [[ "${{ github.event.workflow_run.event }}" == "pull_request" ]]; then | |
echo "build_context_prefix=pr" >> $GITHUB_ENV | |
elif [[ "${{ github.event.workflow_run.event }}" == "push" ]]; then | |
echo "build_context_prefix=push" >> $GITHUB_ENV | |
fi | |
- name: Download Build Context | |
id: build_context | |
uses: actions/github-script@v6 | |
with: | |
result-encoding: string | |
script: | | |
const workflowContextFilePrefix = process.env.build_context_prefix; | |
const workflowContextFileName = `${workflowContextFilePrefix}_info.zip`; | |
let artifactsOpts = github.rest.actions.listWorkflowRunArtifacts.endpoint.merge({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
run_id: context.payload.workflow_run.id | |
}); | |
let artifacts = await github.paginate(artifactsOpts); | |
let prArtifact = artifacts.filter((artifact) => { | |
return artifact.name == workflowContextFileName | |
})[0]; | |
// Build was skipped | |
if (!prArtifact) return ''; | |
let download = await github.rest.actions.downloadArtifact({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
artifact_id: prArtifact.id, | |
archive_format: 'zip' | |
}); | |
let fs = require('fs'); | |
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/${workflowContextFileName}`, Buffer.from(download.data)); | |
return workflowContextFileName; | |
- name: Unpack Build Information | |
if: steps.build_context.outputs.result != '' | |
run: | | |
unzip ${{ steps.build_context.outputs.result }} | |
- name: Read Build Information | |
id: read_build_info | |
if: steps.build_context.outputs.result != '' | |
uses: actions/github-script@v6 | |
with: | |
script: | | |
let fs = require('fs'); | |
const workflowContextFilePrefix = process.env.build_context_prefix; | |
const buildData = fs.readFileSync(`${workflowContextFilePrefix}_info.json`); | |
return JSON.parse(buildData); | |
- name: Parse Pull Request Event Information | |
id: pr_info | |
if: github.event.workflow_run.event == 'pull_request' && steps.build_context.outputs.result != '' | |
run: | | |
PR_ID=$(echo '${{ steps.read_build_info.outputs.result }}' | jq '.id') | |
PR_ID_NO_QUOTE="${PR_ID%\"}" | |
PR_ID_NO_QUOTE="${PR_ID_NO_QUOTE#\"}" | |
echo "pr_id => $PR_ID_NO_QUOTE" | |
echo "pr_id=$PR_ID_NO_QUOTE" >> $GITHUB_OUTPUT | |
echo "pr_site=https://${{ secrets.AWS_WEBSITE }}/${{ secrets.AWS_PR_BUCKET_BUILD_DIR }}/$PR_ID_NO_QUOTE/demonstrations.html" >> $GITHUB_OUTPUT | |
PR_REF=$(echo '${{ steps.read_build_info.outputs.result }}' | jq '.ref') | |
PR_REF_NO_QUOTE="${PR_REF%\"}" | |
PR_REF_NO_QUOTE="${PR_REF_NO_QUOTE#\"}" | |
echo "pr_ref => $PR_REF_NO_QUOTE" | |
echo "pr_ref=$PR_REF_NO_QUOTE" >> $GITHUB_OUTPUT | |
PR_REF_NAME=$(echo '${{ steps.read_build_info.outputs.result }}' | jq '.ref_name') | |
PR_REF_NAME_NO_QUOTE="${PR_REF_NAME%\"}" | |
PR_REF_NAME_NO_QUOTE="${PR_REF_NAME_NO_QUOTE#\"}" | |
echo "pr_ref_name => $PR_REF_NAME_NO_QUOTE" | |
echo "pr_ref_name=$PR_REF_NAME_NO_QUOTE" >> $GITHUB_OUTPUT | |
- name: Parse Push Event Information | |
id: push_info | |
if: github.event.workflow_run.event == 'push' && steps.build_context.outputs.result != '' | |
run: | | |
PUSH_REF=$(echo '${{ steps.read_build_info.outputs.result }}' | jq '.ref') | |
PUSH_REF_NO_QUOTE="${PUSH_REF%\"}" | |
PUSH_REF_NO_QUOTE="${PUSH_REF_NO_QUOTE#\"}" | |
echo "push_ref => $PUSH_REF_NO_QUOTE" | |
echo "push_ref=$PUSH_REF_NO_QUOTE" >> $GITHUB_OUTPUT | |
PUSH_REF_NAME=$(echo '${{ steps.read_build_info.outputs.result }}' | jq '.ref_name') | |
PUSH_REF_NAME_NO_QUOTE="${PUSH_REF_NAME%\"}" | |
PUSH_REF_NAME_NO_QUOTE="${PUSH_REF_NAME_NO_QUOTE#\"}" | |
echo "push_ref_name_raw => $PUSH_REF_NAME_NO_QUOTE" | |
echo "push_ref_name_raw=$PUSH_REF_NAME_NO_QUOTE" >> $GITHUB_OUTPUT | |
BRANCH_NAME=${PUSH_REF_NAME_NO_QUOTE#refs/heads/} | |
BRANCH_NAME_FORMATTED=${BRANCH_NAME^^} | |
echo "push_ref_name => $BRANCH_NAME_FORMATTED" | |
echo "push_ref_name=$BRANCH_NAME_FORMATTED" >> $GITHUB_OUTPUT | |
- name: Create Deployment | |
id: deployment | |
if: github.event.workflow_run.event == 'pull_request' && steps.build_context.outputs.result != '' | |
uses: actions/github-script@v6 | |
with: | |
result-encoding: string | |
script: | | |
const deploymentStage = 'in_progress'; | |
const deployment = await github.rest.repos.createDeployment({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
ref: '${{ steps.pr_info.outputs.pr_ref }}', | |
environment: 'preview', | |
task: 'deploy:pr-${{ steps.pr_info.outputs.pr_id }}', | |
required_contexts: [], | |
transient_environment: true, | |
auto_merge: false, | |
description: `QML doc deployment from pull request ${{ steps.pr_info.outputs.pr_id }}` | |
}); | |
await github.rest.repos.createDeploymentStatus({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
deployment_id: deployment.data.id, | |
state: deploymentStage, | |
log_url: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}', | |
environment_url: '${{ steps.pr_info.outputs.pr_site }}' | |
}); | |
return deployment.data.id | |
- name: Download HTML | |
uses: actions/github-script@v6 | |
if: steps.build_context.outputs.result != '' | |
with: | |
script: | | |
let fs = require('fs'); | |
const maxRetry = 15; | |
const backoffDelay = (retryCount) => new Promise(resolve => setTimeout(resolve, 1000 * retryCount)); | |
const downloadHtmlArtifact = async (artifact) => { | |
let download = await github.rest.actions.downloadArtifact({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
artifact_id: artifact.id, | |
archive_format: 'zip' | |
}); | |
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/${artifact.name}`, Buffer.from(download.data)); | |
}; | |
let artifactsOpts = github.rest.actions.listWorkflowRunArtifacts.endpoint.merge({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
run_id: context.payload.workflow_run.id | |
}); | |
let artifacts = await github.paginate(artifactsOpts); | |
let htmlArtifacts = artifacts.filter((artifact) => { | |
return artifact.name.startsWith('html-') | |
}); | |
/* | |
Attempt to download the artifact one at a time and save them to disk. | |
In the event of an error, back-off (incrementally) and try again. | |
Each error increases the back-off delay by 1s, in other words: | |
> failure -> wait (1s) -> try-again -> failure -> wait (2s) -> try-again -> ... | |
This deals with network congestion and rate-limiting errors that occur if the artifact | |
download happens during a time of high load on GitHub Actions. | |
*/ | |
for (const artifact of htmlArtifacts) { | |
for (let i = 1; i < maxRetry + 1; i++) { | |
try { | |
console.log(`Attempting to download artifact: ${artifact.name}`); | |
await downloadHtmlArtifact(artifact); | |
console.log(`Successfully downloaded artifact: ${artifact.name}`); | |
break; | |
} catch (e) { | |
console.log(`Error while trying to download artifact: ${artifact.name}, i: ${i}, error: ${e}`); | |
['message', 'status', 'request', 'response'].forEach((attr) => { | |
if (e.hasOwnProperty(attr)) { | |
console.log(`error_${attr}:`); | |
console.log(e[attr]); | |
} | |
}); | |
if (i === maxRetry) throw new Error(e); | |
console.log(`Retrying download of artifact: ${artifact.name}`); | |
await backoffDelay(i); | |
} | |
} | |
} | |
- name: Unpack HTML | |
if: steps.build_context.outputs.result != '' | |
run: | | |
mkdir -p website/demos | |
for f in html-*.zip; do | |
unzip -o -d website $f | |
done | |
- name: Upload HTML (Pull Request) | |
if: github.event.workflow_run.event == 'pull_request' && steps.build_context.outputs.result != '' | |
env: | |
AWS_REGION: ${{ secrets.AWS_REGION }} | |
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} | |
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | |
run: | |
aws s3 sync website s3://${{ secrets.AWS_PR_S3_BUCKET_ID }}/${{ secrets.AWS_PR_BUCKET_BUILD_DIR }}/${{ steps.pr_info.outputs.pr_id }}/ --delete | |
- name: Upload HTML (Push to master / dev) | |
if: github.event.workflow_run.event == 'push' && steps.build_context.outputs.result != '' | |
env: | |
AWS_REGION: ${{ secrets.AWS_REGION }} | |
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} | |
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | |
BUCKET_ACTIONS_ID: ${{ format('AWS_ENV_S3_BUCKET_{0}', steps.push_info.outputs.push_ref_name) }} | |
run: | | |
echo "Got actions Bucket ID: ${{ env.BUCKET_ACTIONS_ID }}" | |
AWS_BUCKET_NAME=${{ secrets[env.BUCKET_ACTIONS_ID] }} | |
aws s3 sync website s3://$AWS_BUCKET_NAME/qml/ --delete | |
- name: Upload HTML (dev) | |
if: github.event.workflow_run.event == 'push' && steps.push_info.outputs.push_ref_name == 'dev' && steps.build_context.outputs.result != '' | |
uses: XanaduAI/cloud-actions/push-to-s3-and-invalidate-cloudfront@main | |
with: | |
build-directory: website | |
aws-cloudfront-distribution-id: ${{ secrets.PL_SITE_DEV_NON_REACT_CLOUDFRONT_DISTRIBUTION_ID }} | |
aws-region: ${{ secrets.AWS_REGION }} | |
aws-access-key-id: ${{ secrets.PL_SITE_DEV_NON_REACT_ACCESS_KEY_ID }} | |
aws-secret-access-key: ${{ secrets.PL_SITE_DEV_NON_REACT_SECRET_ACCESS_KEY }} | |
s3-bucket: ${{ secrets.PL_SITE_DEV_S3_BUCKET_NAME }} | |
s3-directory: qml | |
s3-delete-stale-files: true | |
s3-action: upload | |
invalidate-cloudfront-cache: true | |
- name: Upload HTML (prod) | |
if: github.event.workflow_run.event == 'push' && steps.push_info.outputs.push_ref_name == 'master' && steps.build_context.outputs.result != '' | |
uses: XanaduAI/cloud-actions/push-to-s3-and-invalidate-cloudfront@main | |
with: | |
build-directory: website | |
aws-cloudfront-distribution-id: ${{ secrets.PL_SITE_PROD_NON_REACT_CLOUDFRONT_DISTRIBUTION_ID }} | |
aws-region: ${{ secrets.AWS_REGION }} | |
aws-access-key-id: ${{ secrets.PL_SITE_PROD_NON_REACT_ACCESS_KEY_ID }} | |
aws-secret-access-key: ${{ secrets.PL_SITE_PROD_NON_REACT_SECRET_ACCESS_KEY }} | |
s3-bucket: ${{ secrets.PL_SITE_PROD_S3_BUCKET_NAME }} | |
s3-directory: qml | |
s3-delete-stale-files: true | |
s3-action: upload | |
invalidate-cloudfront-cache: true | |
- name: Comment on PR | |
if: github.event.workflow_run.event == 'pull_request' && steps.build_context.outputs.result != '' | |
uses: actions/github-script@v6 | |
with: | |
script: | | |
const actionsBotUserId = 41898282; | |
const prNumber = ${{ steps.pr_info.outputs.pr_id }}; | |
const commentHeader = '**Thank you for opening this pull request.**' | |
const commentBody = ` | |
You can find the built site [at this link](${{ steps.pr_info.outputs.pr_site }}). | |
**Deployment Info:** | |
- Pull Request ID: \`${{ steps.pr_info.outputs.pr_id }}\` | |
- Deployment SHA: \`${{ steps.pr_info.outputs.pr_ref }}\` | |
(The \`Deployment SHA\` refers to the latest commit hash the docs were built from) | |
**Note:** It may take several minutes for updates to this pull request to be reflected on the deployed site. | |
`; | |
const commentText = ` | |
${commentHeader} | |
${commentBody} | |
`; | |
// Get the existing comments. | |
const {data: comments} = await github.rest.issues.listComments({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
issue_number: prNumber | |
}); | |
// Find any comment already made by the bot. | |
const botComment = comments.find(comment => comment.user.id === actionsBotUserId && comment.body.trim().startsWith(commentHeader)); | |
if (botComment) { | |
await github.rest.issues.updateComment({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
comment_id: botComment.id, | |
body: commentText | |
}); | |
} else { | |
await github.rest.issues.createComment({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
issue_number: prNumber, | |
body: commentText | |
}); | |
} | |
- name: Update Deployment (success) | |
if: success() && github.event.workflow_run.event == 'pull_request' && steps.build_context.outputs.result != '' | |
uses: actions/github-script@v6 | |
env: | |
DEPLOYMENT_STAGE: success | |
DEPLOYMENT_ID: ${{ steps.deployment.outputs.result }} | |
with: | |
script: | | |
const deploymentId = process.env.DEPLOYMENT_ID; | |
const deploymentEnv = process.env.DEPLOYMENT_ENV; | |
const deploymentStage = process.env.DEPLOYMENT_STAGE; | |
await github.rest.repos.createDeploymentStatus({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
deployment_id: deploymentId, | |
state: deploymentStage, | |
log_url: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}', | |
environment_url: '${{ steps.pr_info.outputs.pr_site }}' | |
}); | |
- name: Update Deployment (failure) | |
if: failure() && github.event.workflow_run.event == 'pull_request' && steps.build_context.outputs.result != '' | |
uses: actions/github-script@v6 | |
env: | |
DEPLOYMENT_STAGE: failure | |
DEPLOYMENT_ID: ${{ steps.deployment.outputs.result }} | |
with: | |
script: | | |
const deploymentId = process.env.DEPLOYMENT_ID; | |
const deploymentEnv = process.env.DEPLOYMENT_ENV; | |
const deploymentStage = process.env.DEPLOYMENT_STAGE; | |
await github.rest.repos.createDeploymentStatus({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
deployment_id: deploymentId, | |
state: deploymentStage, | |
log_url: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}', | |
environment_url: '${{ steps.pr_info.outputs.pr_site }}' | |
}); |