Deploy PR Review App - PR #58
Workflow file for this run
This file contains hidden or 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 PR Review App to Control Plane | |
run-name: Deploy PR Review App - PR #${{ github.event.pull_request.number || github.event.issue.number || github.event.inputs.pr_number }} | |
on: | |
pull_request: | |
types: [opened, synchronize, reopened] | |
issue_comment: | |
types: [created] | |
workflow_dispatch: | |
inputs: | |
pr_number: | |
description: 'Pull Request number to deploy' | |
required: true | |
type: number | |
concurrency: | |
group: deploy-pr-${{ github.event.pull_request.number || github.event.issue.number || github.event.inputs.pr_number }} | |
cancel-in-progress: true | |
env: | |
APP_NAME: ${{ vars.REVIEW_APP_PREFIX }}-pr-${{ github.event.pull_request.number || github.event.issue.number || github.event.inputs.pr_number }} | |
CPLN_TOKEN: ${{ secrets.CPLN_TOKEN_STAGING }} | |
CPLN_ORG: ${{ vars.CPLN_ORG_STAGING }} | |
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number || github.event.inputs.pr_number }} | |
jobs: | |
debug: | |
uses: ./.github/workflows/debug-workflow.yml | |
with: | |
debug_enabled: false | |
process-deployment: | |
needs: debug | |
if: | | |
(github.event_name == 'pull_request') || | |
(github.event_name == 'push') || | |
(github.event_name == 'workflow_dispatch') || | |
(github.event_name == 'issue_comment' && | |
github.event.issue.pull_request && | |
contains(github.event.comment.body, '/deploy-review-app')) | |
runs-on: ubuntu-latest | |
outputs: | |
pr_number: ${{ env.PR_NUMBER }} | |
pr_sha: ${{ env.PR_SHA }} | |
pr_ref: ${{ steps.getRef.outputs.PR_REF }} | |
do_deploy: ${{ env.DO_DEPLOY }} | |
comment_id: ${{ steps.create-comment.outputs.comment-id }} | |
deployment_id: ${{ steps.init-deployment.outputs.result }} | |
steps: | |
# Initial checkout only for pull_request and push events | |
- name: Checkout code | |
if: github.event_name == 'pull_request' || github.event_name == 'push' | |
uses: actions/checkout@v4 | |
with: | |
fetch-depth: 0 | |
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }} | |
# Basic checkout for other events (workflow_dispatch, issue_comment) | |
# We'll do proper checkout after getting PR info | |
- name: Initial checkout | |
if: github.event_name == 'workflow_dispatch' || github.event_name == 'issue_comment' | |
uses: actions/checkout@v4 | |
with: | |
fetch-depth: 0 | |
- name: Validate Required Secrets and Variables | |
uses: ./.github/actions/validate-required-vars | |
with: | |
CPLN_TOKEN_STAGING: ${{ secrets.CPLN_TOKEN_STAGING }} | |
CPLN_TOKEN_PRODUCTION: ${{ secrets.CPLN_TOKEN_PRODUCTION }} | |
CPLN_ORG_STAGING: ${{ vars.CPLN_ORG_STAGING }} | |
CPLN_ORG_PRODUCTION: ${{ vars.CPLN_ORG_PRODUCTION }} | |
REVIEW_APP_PREFIX: ${{ vars.REVIEW_APP_PREFIX }} | |
PRODUCTION_APP_NAME: ${{ vars.PRODUCTION_APP_NAME }} | |
STAGING_APP_NAME: ${{ vars.STAGING_APP_NAME }} | |
- name: Get PR HEAD Ref | |
id: getRef | |
env: | |
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
run: | | |
# For push events, try to find associated PR first | |
if [[ "${{ github.event_name }}" == "push" ]]; then | |
PR_DATA=$(gh pr list --head "${{ github.ref_name }}" --json number,headRefName,headRefOid --jq '.[0]') | |
if [[ -n "$PR_DATA" ]]; then | |
PR_NUMBER=$(echo "$PR_DATA" | jq -r .number) | |
else | |
echo "No PR found for branch ${{ github.ref_name }}, skipping deployment" | |
echo "DO_DEPLOY=false" >> $GITHUB_ENV | |
exit 0 | |
fi | |
else | |
# Get PR number based on event type | |
case "${{ github.event_name }}" in | |
"workflow_dispatch") | |
PR_NUMBER="${{ github.event.inputs.pr_number }}" | |
;; | |
"issue_comment") | |
PR_NUMBER="${{ github.event.issue.number }}" | |
;; | |
"pull_request") | |
PR_NUMBER="${{ github.event.pull_request.number }}" | |
;; | |
*) | |
echo "Error: Unsupported event type ${{ github.event_name }}" | |
exit 1 | |
;; | |
esac | |
fi | |
if [[ -z "$PR_NUMBER" ]]; then | |
echo "Error: Could not determine PR number" | |
echo "Event type: ${{ github.event_name }}" | |
echo "Event action: ${{ github.event.action }}" | |
echo "Ref name: ${{ github.ref_name }}" | |
echo "Available event data:" | |
echo "- PR number from inputs: ${{ github.event.inputs.pr_number }}" | |
echo "- PR number from issue: ${{ github.event.issue.number }}" | |
echo "- PR number from pull_request: ${{ github.event.pull_request.number }}" | |
exit 1 | |
fi | |
# Get PR data | |
if [[ -z "$PR_DATA" ]]; then | |
PR_DATA=$(gh pr view "$PR_NUMBER" --json headRefName,headRefOid) | |
if [[ -z "$PR_DATA" ]]; then | |
echo "Error: PR DATA for PR #$PR_NUMBER not found" | |
echo "Event type: ${{ github.event_name }}" | |
echo "Event action: ${{ github.event.action }}" | |
echo "Ref name: ${{ github.ref_name }}" | |
echo "Attempted to fetch PR data with: gh pr view $PR_NUMBER" | |
exit 1 | |
fi | |
fi | |
# Set PR_NUMBER and override APP_NAME with validated PR number | |
echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV | |
echo "APP_NAME=${{ vars.REVIEW_APP_PREFIX }}-pr-$PR_NUMBER" >> $GITHUB_ENV | |
echo "PR_REF=$(echo $PR_DATA | jq -r .headRefName)" >> $GITHUB_OUTPUT | |
echo "PR_SHA=$(echo $PR_DATA | jq -r .headRefOid)" >> $GITHUB_ENV | |
- name: Setup Environment | |
uses: ./.github/actions/setup-environment | |
with: | |
token: ${{ secrets.CPLN_TOKEN_STAGING }} | |
org: ${{ vars.CPLN_ORG_STAGING }} | |
- name: Check if Review App Exists | |
id: check-app | |
env: | |
CPLN_TOKEN: ${{ secrets.CPLN_TOKEN_STAGING }} | |
run: | | |
# First check if cpflow exists | |
if ! command -v cpflow &> /dev/null; then | |
echo "Error: cpflow command not found" | |
exit 1 | |
fi | |
# Check if app exists and save state | |
if ! cpflow exists -a ${{ env.APP_NAME }}; then | |
echo "APP_EXISTS=false" >> $GITHUB_ENV | |
else | |
echo "APP_EXISTS=true" >> $GITHUB_ENV | |
fi | |
- name: Validate Deployment Request | |
id: validate | |
run: | | |
# Skip validation if deployment is already disabled | |
if [[ "${{ env.DO_DEPLOY }}" == "false" ]]; then | |
echo "Skipping validation - deployment already disabled" | |
exit 0 | |
fi | |
# Validate supported event types | |
if ! [[ "${{ github.event_name }}" == "workflow_dispatch" || \ | |
"${{ github.event_name }}" == "issue_comment" || \ | |
"${{ github.event_name }}" == "pull_request" ]]; then | |
echo "Error: Unsupported event type ${{ github.event_name }}" | |
exit 1 | |
fi | |
- name: Setup Control Plane App if Not Existing | |
if: env.DO_DEPLOY != 'false' && env.APP_EXISTS == 'false' | |
env: | |
CPLN_TOKEN: ${{ secrets.CPLN_TOKEN_STAGING }} | |
run: | | |
echo "🔧 Setting up new Control Plane app..." | |
cpflow setup-app -a ${{ env.APP_NAME }} --org ${{ vars.CPLN_ORG_STAGING }} | |
- name: Create Initial Comment | |
if: env.DO_DEPLOY != 'false' | |
uses: actions/github-script@v7 | |
id: create-comment | |
with: | |
script: | | |
const result = await github.rest.issues.createComment({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
issue_number: process.env.PR_NUMBER, | |
body: [ | |
`🏗️ Building Docker image for PR [#${process.env.PR_NUMBER}](${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/pull/${process.env.PR_NUMBER}), commit [${context.sha.substring(0, 7)}](${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/commit/${context.sha})`, | |
'', | |
'🚀 Deploying to Control Plane...', | |
'', | |
'⏳ Waiting for deployment to be ready...', | |
'', | |
`📝 [View Build and Deploy Logs](${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}/job/${context.job})`, | |
'', | |
process.env.CONSOLE_LINK | |
].join('\n') | |
}); | |
core.setOutput('comment-id', result.data.id); | |
- name: Set Deployment URLs | |
id: set-urls | |
if: env.DO_DEPLOY != 'false' | |
uses: actions/github-script@v7 | |
with: | |
script: | | |
// Set workflow URL for logs | |
const getWorkflowUrl = async (runId) => { | |
const { data: run } = await github.rest.actions.getWorkflowRun({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
run_id: runId | |
}); | |
// Get the job ID for this specific job | |
const { data: jobs } = await github.rest.actions.listJobsForWorkflowRun({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
run_id: runId | |
}); | |
const currentJob = jobs.jobs.find(job => job.name === context.job); | |
return `${run.html_url}/job/${currentJob.id}`; | |
}; | |
const workflowUrl = await getWorkflowUrl(context.runId); | |
core.exportVariable('BUILD_LOGS_URL', workflowUrl); | |
core.exportVariable('CONSOLE_LINK', | |
'🎮 [Control Plane Console](' + | |
'https://console.cpln.io/console/org/' + process.env.CPLN_ORG + '/gvc/' + process.env.APP_NAME + '/-info)' | |
); | |
- name: Initialize GitHub Deployment | |
if: env.DO_DEPLOY != 'false' | |
uses: actions/github-script@v7 | |
id: init-deployment | |
with: | |
script: | | |
const ref = process.env.PR_SHA; | |
const environment = process.env.ENVIRONMENT_NAME || 'review-app'; | |
const deployment = await github.rest.repos.createDeployment({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
ref: ref, | |
environment: environment, | |
auto_merge: false, | |
required_contexts: [], | |
description: `Deployment for PR #${process.env.PR_NUMBER}` | |
}); | |
// Create initial deployment status | |
await github.rest.repos.createDeploymentStatus({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
deployment_id: deployment.data.id, | |
state: 'in_progress', | |
description: 'Deployment started' | |
}); | |
return deployment.data.id; | |
build: | |
needs: process-deployment | |
if: needs.process-deployment.outputs.do_deploy != 'false' | |
runs-on: ubuntu-latest | |
outputs: | |
image_tag: ${{ steps.build.outputs.image_tag }} | |
comment_id: ${{ needs.process-deployment.outputs.comment_id }} | |
pr_number: ${{ needs.process-deployment.outputs.pr_number }} | |
do_deploy: ${{ needs.process-deployment.outputs.do_deploy }} | |
steps: | |
- name: Checkout code | |
uses: actions/checkout@v4 | |
with: | |
ref: ${{ needs.process-deployment.outputs.pr_ref }} | |
- name: Setup Environment | |
uses: ./.github/actions/setup-environment | |
with: | |
token: ${{ secrets.CPLN_TOKEN_STAGING }} | |
org: ${{ vars.CPLN_ORG_STAGING }} | |
- name: Build Docker Image | |
id: build | |
uses: ./.github/actions/build-docker-image | |
with: | |
app_name: ${{ env.APP_NAME }} | |
org: ${{ vars.CPLN_ORG_STAGING }} | |
commit: ${{ needs.process-deployment.outputs.pr_sha }} | |
PR_NUMBER: ${{ needs.process-deployment.outputs.pr_number }} | |
deploy: | |
needs: build | |
if: needs.build.outputs.do_deploy != 'false' | |
runs-on: ubuntu-latest | |
steps: | |
- name: Checkout code | |
uses: actions/checkout@v4 | |
- name: Setup Environment | |
uses: ./.github/actions/setup-environment | |
with: | |
token: ${{ secrets.CPLN_TOKEN_STAGING }} | |
org: ${{ vars.CPLN_ORG_STAGING }} | |
- name: Update Status - Deploying | |
uses: actions/github-script@v7 | |
with: | |
script: | | |
// Create deployment status for deploying state | |
await github.rest.repos.createDeploymentStatus({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
deployment_id: ${{ needs.process-deployment.outputs.deployment_id }}, | |
state: 'in_progress', | |
description: 'Deployment in progress', | |
log_url: process.env.BUILD_LOGS_URL | |
}); | |
// Get the current job URL and ID | |
const { data: jobs } = await github.rest.actions.listJobsForWorkflowRun({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
run_id: context.runId | |
}); | |
const currentJob = jobs.jobs.find(job => job.name === context.job); | |
const currentJobUrl = `${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}/job/${currentJob.id}`; | |
// Update the PR comment with correct job URLs | |
await github.rest.issues.updateComment({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
comment_id: ${{ needs.build.outputs.comment_id }}, | |
body: [ | |
`🏗️ Built Docker image for PR [#${process.env.PR_NUMBER}](${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/pull/${process.env.PR_NUMBER}), commit [${process.env.PR_SHA.substring(0, 7)}](${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/commit/${process.env.PR_SHA})`, | |
'', | |
'🚀 Deploying to Control Plane...', | |
'', | |
'⏳ Waiting for deployment to be ready...', | |
'', | |
process.env.CONSOLE_LINK, | |
'', | |
`📝 [View Build Logs](${process.env.BUILD_LOGS_URL})`, | |
`📝 [View Deploy Logs](${currentJobUrl})` | |
].join('\n') | |
}); | |
- name: Deploy to Control Plane | |
if: env.DO_DEPLOY != 'false' | |
uses: ./.github/actions/deploy-to-control-plane | |
with: | |
app_name: ${{ env.APP_NAME }} | |
org: ${{ vars.CPLN_ORG_STAGING }} | |
github_token: ${{ secrets.GITHUB_TOKEN }} | |
wait_timeout: ${{ vars.WAIT_TIMEOUT || 900 }} | |
cpln_token: ${{ secrets.CPLN_TOKEN_STAGING }} | |
pr_number: ${{ env.PR_NUMBER }} | |
- name: Update Status - Deployment Complete | |
if: env.DO_DEPLOY != 'false' | |
uses: actions/github-script@v7 | |
with: | |
script: | | |
const prNumber = process.env.PR_NUMBER; | |
const appUrl = process.env.APP_URL; | |
const workflowUrl = process.env.WORKFLOW_URL; | |
const isSuccess = '${{ job.status }}' === 'success'; | |
const consoleLink = process.env.CONSOLE_LINK; | |
// Get current job ID for accurate logs URL | |
const { data: jobs } = await github.rest.actions.listJobsForWorkflowRun({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
run_id: context.runId | |
}); | |
const currentJob = jobs.jobs.find(job => job.name === context.job); | |
const logsUrl = `${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}/job/${currentJob.id}`; | |
// Create GitHub deployment status | |
const deploymentStatus = { | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
deployment_id: ${{ steps.init-deployment.outputs.result }}, | |
state: isSuccess ? 'success' : 'failure', | |
environment_url: isSuccess ? appUrl : undefined, | |
log_url: logsUrl, | |
environment: 'review' | |
}; | |
await github.rest.repos.createDeploymentStatus(deploymentStatus); | |
// Define messages based on deployment status | |
const successMessage = [ | |
'✅ Deployment complete for PR #' + prNumber + ', commit ' + '${{ env.PR_SHA }}', | |
'', | |
'🚀 [Review App for PR #' + prNumber + '](' + appUrl + ')', | |
consoleLink, | |
'', | |
`📝 [View Build and Deploy Logs](${logsUrl})`, | |
].join('\n'); | |
const failureMessage = [ | |
'❌ Deployment failed for PR #' + prNumber + ', commit ' + '${{ env.PR_SHA }}', | |
'', | |
consoleLink, | |
'', | |
`📝 [View Build and Deploy Logs with Errors](${logsUrl})`, | |
].join('\n'); | |
// Update the existing comment | |
await github.rest.issues.updateComment({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
comment_id: ${{ needs.build.outputs.comment_id }}, | |
body: isSuccess ? successMessage : failureMessage | |
}); |