Deploy PR Review App - PR #5
Workflow file for this run
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 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] | |
push: | |
branches: | |
- '**' # Any branch | |
- '!main' # Except main | |
- '!master' # Except master | |
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 | |
shell: bash | |
run: | | |
missing=() | |
# Check secrets | |
if [ -z "${{ secrets.CPLN_TOKEN_STAGING }}" ]; then | |
missing+=("Secret: CPLN_TOKEN_STAGING") | |
fi | |
# Check variables | |
if [ -z "${{ vars.CPLN_ORG_STAGING }}" ]; then | |
missing+=("Variable: CPLN_ORG_STAGING") | |
fi | |
if [ -z "${{ vars.REVIEW_APP_PREFIX }}" ]; then | |
missing+=("Variable: REVIEW_APP_PREFIX") | |
fi | |
if [ ${#missing[@]} -ne 0 ]; then | |
echo "Required secrets/variables are not set: ${missing[*]}" | |
exit 1 | |
fi | |
- 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 | |
# Extract and set PR data | |
echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV | |
echo "APP_NAME=${{ vars.REVIEW_APP_PREFIX }}-$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 | |
if ! [[ "${{ github.event_name }}" == "workflow_dispatch" || \ | |
"${{ github.event_name }}" == "issue_comment" || \ | |
"${{ github.event_name }}" == "pull_request" || \ | |
"${{ github.event_name }}" == "push" ]]; then | |
echo "Error: Unsupported event type ${{ github.event_name }}" | |
exit 1 | |
fi | |
# Set DO_DEPLOY based on event type and conditions | |
if [[ "${{ github.event_name }}" == "pull_request" && \ | |
("${{ github.event.action }}" == "opened" || \ | |
"${{ github.event.action }}" == "synchronize" || \ | |
"${{ github.event.action }}" == "reopened") ]]; then | |
echo "DO_DEPLOY=true" >> $GITHUB_ENV | |
elif [[ "${{ github.event_name }}" == "push" ]]; then | |
echo "DO_DEPLOY=true" >> $GITHUB_ENV | |
elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then | |
echo "DO_DEPLOY=true" >> $GITHUB_ENV | |
elif [[ "${{ github.event_name }}" == "issue_comment" ]]; then | |
if [[ "${{ github.event.issue.pull_request }}" ]]; then | |
# Trim spaces and check for exact command | |
COMMENT_BODY=$(echo "${{ github.event.comment.body }}" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') | |
if [[ "$COMMENT_BODY" == "/deploy-review-app" ]]; then | |
echo "DO_DEPLOY=true" >> $GITHUB_ENV | |
else | |
echo "DO_DEPLOY=false" >> $GITHUB_ENV | |
echo "Skipping deployment - comment '$COMMENT_BODY' does not match '/deploy-review-app'" | |
fi | |
else | |
echo "DO_DEPLOY=false" >> $GITHUB_ENV | |
echo "Skipping deployment for non-PR comment" | |
fi | |
fi | |
- name: Setup Control Plane App if Not Existing | |
if: env.DO_DEPLOY == 'true' && 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: '🚀 Starting deployment process...\n\n' + process.env.CONSOLE_LINK | |
}); | |
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('WORKFLOW_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: Update Status - Building | |
uses: actions/github-script@v7 | |
with: | |
script: | | |
const buildingMessage = [ | |
'🏗️ Building Docker image for PR #${{ needs.process-deployment.outputs.pr_number }}, commit ${{ needs.process-deployment.outputs.pr_sha }}', | |
'', | |
'📝 [View Build Logs](${{ env.WORKFLOW_URL }})', | |
'', | |
process.env.CONSOLE_LINK | |
].join('\n'); | |
await github.rest.issues.updateComment({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
comment_id: ${{ needs.process-deployment.outputs.comment_id }}, | |
body: buildingMessage | |
}); | |
- 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: Update Status - Deploying | |
uses: actions/github-script@v7 | |
with: | |
script: | | |
const deployingMessage = [ | |
'🚀 Deploying to Control Plane...', | |
'', | |
'⏳ Waiting for deployment to be ready...', | |
'', | |
'📝 [View Deploy Logs](${{ env.WORKFLOW_URL }})', | |
'', | |
process.env.CONSOLE_LINK | |
].join('\n'); | |
await github.rest.issues.updateComment({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
comment_id: ${{ steps.create-comment.outputs.comment-id }}, | |
body: deployingMessage | |
}); | |
- 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; | |
// 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: workflowUrl, | |
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 Completed Action Build and Deploy Logs](' + workflowUrl + ')' | |
].join('\n'); | |
const failureMessage = [ | |
'❌ Deployment failed for PR #' + prNumber + ', commit ' + '${{ env.PR_SHA }}', | |
'', | |
consoleLink, | |
'', | |
'📋 [View Deployment Logs with Errors](' + workflowUrl + ')' | |
].join('\n'); | |
// Update the existing comment | |
await github.rest.issues.updateComment({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
comment_id: ${{ steps.create-comment.outputs.comment-id }}, | |
body: isSuccess ? successMessage : failureMessage | |
}); |