Skip to content

Deploy PR Review App - PR #210

Deploy PR Review App - PR

Deploy PR Review App - PR #210

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 }}-${{ 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 # Will still run if vars.DEBUG_WORKFLOW is true
Process-Deployment-Command:
needs: debug # Add this to ensure debug runs first
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
permissions:
contents: read
deployments: write
pull-requests: write
issues: write
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: Checkout PR code
if: github.event_name == 'workflow_dispatch' || github.event_name == 'issue_comment'
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ steps.getRef.outputs.PR_SHA }}
- 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: Update Comment - Building
if: env.DO_DEPLOY != 'false'
uses: actions/github-script@v7
with:
script: |
const buildingMessage = [
`🏗️ Building Docker image for PR #${process.env.PR_NUMBER}, commit ${process.env.PR_SHA}`,
'',
`📝 [View Build Logs](${process.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: buildingMessage
});
- 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: Update Status - Building
if: env.DO_DEPLOY != 'false'
uses: actions/github-script@v7
with:
script: |
const buildingMessage = [
'🏗️ Building Docker image for PR #' + process.env.PR_NUMBER + ', commit ' + '${{ env.PR_SHA }}',
'',
'📝 [View Build Logs](' + process.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: buildingMessage
});
- name: Checkout PR Branch
if: env.DO_DEPLOY != 'false'
run: git checkout ${{ steps.getRef.outputs.PR_REF }}
- 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;
- name: Build Docker Image
if: env.DO_DEPLOY != 'false'
uses: ./.github/actions/build-docker-image
with:
app_name: ${{ env.APP_NAME }}
org: ${{ vars.CPLN_ORG_STAGING }}
commit: ${{ env.PR_SHA }}
PR_NUMBER: ${{ env.PR_NUMBER }}
- name: Update Status - Deploying
if: env.DO_DEPLOY != 'false'
uses: actions/github-script@v7
with:
script: |
const deployingMessage = [
'🚀 Deploying to Control Plane...',
'',
'⏳ Waiting for deployment to be ready...',
'',
'📝 [View Deploy Logs](' + process.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
});