diff --git a/.controlplane/controlplane.yml b/.controlplane/controlplane.yml index f394fe1c..53f68730 100644 --- a/.controlplane/controlplane.yml +++ b/.controlplane/controlplane.yml @@ -38,7 +38,7 @@ aliases: release_script: release_script.sh apps: - react-webpack-rails-tutorial-production: + react-webpack-rails-tutorial: # Simulate Production Version <<: *common # Don't allow overriding the org and app by ENV vars b/c production is sensitive! diff --git a/.github/actions/build-docker-image/action.yml b/.github/actions/build-docker-image/action.yml index 45a12434..e1b5df73 100644 --- a/.github/actions/build-docker-image/action.yml +++ b/.github/actions/build-docker-image/action.yml @@ -13,7 +13,7 @@ inputs: required: true PR_NUMBER: description: 'PR number' - required: false + required: true runs: using: "composite" @@ -22,18 +22,11 @@ runs: id: build shell: bash run: | - PR_INFO="" - if [ -n "${PR_NUMBER}" ]; then - PR_INFO=" for PR #${PR_NUMBER}" - fi - - echo "๐Ÿ—๏ธ Building Docker image${PR_INFO} (commit ${{ inputs.commit }})..." + echo "๐Ÿ—๏ธ Building Docker image for PR #${PR_NUMBER} (commit ${{ inputs.commit }})..." if cpflow build-image -a "${{ inputs.app_name }}" --commit="${{ inputs.commit }}" --org="${{ inputs.org }}"; then - image_tag="${{ inputs.org }}/${{ inputs.app_name }}:${{ inputs.commit }}" - echo "image_tag=${image_tag}" >> $GITHUB_OUTPUT - echo "โœ… Docker image build successful${PR_INFO} (commit ${{ inputs.commit }})" + echo "โœ… Docker image build successful for PR #${PR_NUMBER} (commit ${{ inputs.commit }})" else - echo "โŒ Docker image build failed${PR_INFO} (commit ${{ inputs.commit }})" + echo "โŒ Docker image build failed for PR #${PR_NUMBER} (commit ${{ inputs.commit }})" exit 1 fi diff --git a/.github/actions/delete-control-plane-app/action.yml b/.github/actions/delete-control-plane-app/action.yml new file mode 100644 index 00000000..d5d13ef7 --- /dev/null +++ b/.github/actions/delete-control-plane-app/action.yml @@ -0,0 +1,20 @@ +name: Delete Control Plane App +description: 'Deletes a Control Plane application and all its resources' + +inputs: + app_name: + description: 'Name of the application to delete' + required: true + org: + description: 'Organization name' + required: true + +runs: + using: "composite" + steps: + - name: Delete Application + shell: bash + run: ${{ github.action_path }}/../deploy-to-control-plane/scripts/delete-app.sh + env: + APP_NAME: ${{ inputs.app_name }} + CPLN_ORG: ${{ inputs.org }} diff --git a/.github/actions/deploy-to-control-plane/action.yml b/.github/actions/deploy-to-control-plane/action.yml index fcb68439..1e5c9fa7 100644 --- a/.github/actions/deploy-to-control-plane/action.yml +++ b/.github/actions/deploy-to-control-plane/action.yml @@ -17,12 +17,6 @@ inputs: description: 'Timeout in seconds for waiting for workloads to be ready' required: false default: '900' - cpln_token: - description: 'Control Plane token' - required: true - pr_number: - description: 'Pull Request number' - required: true outputs: review_app_url: @@ -32,16 +26,66 @@ outputs: runs: using: "composite" steps: + - name: Setup Environment + uses: ./.github/actions/setup-environment + + - name: Get Commit SHA + id: get_sha + shell: bash + run: ${{ github.action_path }}/scripts/get-commit-sha.sh + env: + GITHUB_TOKEN: ${{ inputs.github_token }} + PR_NUMBER: ${{ env.PR_NUMBER }} + - name: Deploy to Control Plane id: deploy shell: bash - env: - APP_NAME: ${{ inputs.app_name }} - CPLN_ORG: ${{ inputs.org }} - CPLN_TOKEN: ${{ inputs.cpln_token }} - WAIT_TIMEOUT: ${{ inputs.wait_timeout }} run: | - # Run the deployment script - if ! ${{ github.action_path }}/scripts/deploy.sh; then + echo "๐Ÿš€ Deploying app for PR #${PR_NUMBER}..." + + # Create temp file for output + TEMP_OUTPUT=$(mktemp) + trap 'rm -f "${TEMP_OUTPUT}"' EXIT + + # Deploy the application and show output in real-time while capturing it + if ! cpflow deploy-image -a "${{ inputs.app_name }}" --run-release-phase --org "${{ inputs.org }}" 2>&1 | tee "${TEMP_OUTPUT}"; then + echo "โŒ Deployment failed for PR #${PR_NUMBER}" + echo "Error output:" + cat "${TEMP_OUTPUT}" + exit 1 + fi + + # Extract app URL from captured output + REVIEW_APP_URL=$(grep -oP 'https://rails-[^[:space:]]*\.cpln\.app(?=\s|$)' "${TEMP_OUTPUT}" | head -n1) + if [ -z "${REVIEW_APP_URL}" ]; then + echo "โŒ Failed to get app URL from deployment output" + echo "Deployment output:" + cat "${TEMP_OUTPUT}" + exit 1 + fi + + # Wait for all workloads to be ready + WAIT_TIMEOUT=${WAIT_TIMEOUT:-${{ inputs.wait_timeout }}} + if ! [[ "${WAIT_TIMEOUT}" =~ ^[0-9]+$ ]]; then + echo "โŒ Invalid timeout value: ${WAIT_TIMEOUT}" + exit 1 + fi + echo "โณ Waiting for all workloads to be ready (timeout: ${WAIT_TIMEOUT}s)" + + # Use timeout command with ps:wait and show output in real-time + if ! timeout "${WAIT_TIMEOUT}" bash -c "cpflow ps:wait -a \"${{ inputs.app_name }}\"" 2>&1 | tee -a "${TEMP_OUTPUT}"; then + TIMEOUT_EXIT=$? + if [ ${TIMEOUT_EXIT} -eq 124 ]; then + echo "โŒ Timed out waiting for workloads after ${WAIT_TIMEOUT} seconds" + else + echo "โŒ Workloads did not become ready for PR #${PR_NUMBER} (exit code: ${TIMEOUT_EXIT})" + fi + echo "Full output:" + cat "${TEMP_OUTPUT}" exit 1 fi + + echo "โœ… Deployment successful for PR #${PR_NUMBER}" + echo "๐ŸŒ App URL: ${REVIEW_APP_URL}" + echo "review_app_url=${REVIEW_APP_URL}" >> $GITHUB_OUTPUT + echo "REVIEW_APP_URL=${REVIEW_APP_URL}" >> $GITHUB_ENV diff --git a/.github/actions/deploy-to-control-plane/scripts/delete-app.sh b/.github/actions/deploy-to-control-plane/scripts/delete-app.sh new file mode 100755 index 00000000..92e8fbc3 --- /dev/null +++ b/.github/actions/deploy-to-control-plane/scripts/delete-app.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# Script to delete a Control Plane application +# Required environment variables: +# - APP_NAME: Name of the application to delete +# - CPLN_ORG: Organization name + +set -e + +# Validate required environment variables +: "${APP_NAME:?APP_NAME environment variable is required}" +: "${CPLN_ORG:?CPLN_ORG environment variable is required}" + +# Safety check: prevent deletion of production or staging apps +if echo "$APP_NAME" | grep -iqE '(production|staging)'; then + echo "โŒ ERROR: Cannot delete apps containing 'production' or 'staging' in their name" >&2 + echo "๐Ÿ›‘ This is a safety measure to prevent accidental deletion of production or staging environments" >&2 + echo " App name: $APP_NAME" >&2 + exit 1 +fi + +# Check if app exists before attempting to delete +echo "๐Ÿ” Checking if application exists: $APP_NAME" +if ! cpflow exists -a "$APP_NAME"; then + echo "โš ๏ธ Application does not exist: $APP_NAME" + exit 0 +fi + +# Delete the application +echo "๐Ÿ—‘๏ธ Deleting application: $APP_NAME" +if ! cpflow delete -a "$APP_NAME" --force; then + echo "โŒ Failed to delete application: $APP_NAME" >&2 + exit 1 +fi + +echo "โœ… Successfully deleted application: $APP_NAME" diff --git a/.github/actions/deploy-to-control-plane/scripts/deploy.sh b/.github/actions/deploy-to-control-plane/scripts/deploy.sh index f90ede23..9d070b64 100755 --- a/.github/actions/deploy-to-control-plane/scripts/deploy.sh +++ b/.github/actions/deploy-to-control-plane/scripts/deploy.sh @@ -1,7 +1,7 @@ #!/bin/bash # This script handles the deployment to Control Plane and extracts the Rails URL -# +# # Required environment variables: # - APP_NAME: Name of the application to deploy # - CPLN_ORG: Control Plane organization @@ -11,7 +11,7 @@ # Must be a positive integer # # Outputs: -# - ENV APP_URL: URL of the deployed application +# - rails_url: URL of the deployed Rails application set -e @@ -31,34 +31,21 @@ trap 'rm -f "$TEMP_OUTPUT"' EXIT # Deploy the application echo "๐Ÿš€ Deploying to Control Plane (timeout: ${WAIT_TIMEOUT}s)" -if ! timeout "${WAIT_TIMEOUT}" cpflow deploy-image -a "$APP_NAME" --run-release-phase --org "$CPLN_ORG" --verbose 2>&1 | tee "$TEMP_OUTPUT"; then - echo "โŒ Deployment failed" - echo "Full output:" - cat "$TEMP_OUTPUT" - exit 1 -fi - -# Extract app URL from deployment output -APP_URL=$(grep -oP 'https://[^[:space:]]*\.cpln\.app(?=\s|$)' "$TEMP_OUTPUT" | head -n1) -if [ -z "$APP_URL" ]; then - echo "โŒ Error: Could not find app URL in deployment output" - exit 1 +if timeout "$WAIT_TIMEOUT" cpflow deploy-image -a "$APP_NAME" --run-release-phase --org "$CPLN_ORG" --verbose | tee "$TEMP_OUTPUT"; then + # Extract Rails URL from deployment output + RAILS_URL=$(grep -oP 'https://rails-[^[:space:]]*\.cpln\.app(?=\s|$)' "$TEMP_OUTPUT" | head -n1) + if [ -n "$RAILS_URL" ]; then + echo "rails_url=$RAILS_URL" >> "$GITHUB_OUTPUT" + echo "โœ… Deployment successful" + echo "๐Ÿš€ Rails URL: $RAILS_URL" + else + echo "โŒ Failed to extract Rails URL from deployment output" + exit 1 + fi +elif [ $? -eq 124 ]; then + echo "โŒ Deployment timed out after $WAIT_TIMEOUT seconds" + exit 1 +else + echo "โŒ Deployment to Control Plane failed" + exit 1 fi - -# Wait for all workloads to be ready -echo "โณ Waiting for all workloads to be ready (timeout: ${WAIT_TIMEOUT}s)" -if ! timeout "${WAIT_TIMEOUT}" bash -c "cpflow ps:wait -a \"$APP_NAME\"" 2>&1 | tee -a "$TEMP_OUTPUT"; then - TIMEOUT_EXIT=$? - if [ ${TIMEOUT_EXIT} -eq 124 ]; then - echo "โŒ Timed out waiting for workloads after ${WAIT_TIMEOUT} seconds" - else - echo "โŒ Workloads did not become ready" - fi - echo "Full output:" - cat "$TEMP_OUTPUT" - exit 1 -fi - -echo "โœ… Deployment successful" -echo "๐ŸŒ App URL: $APP_URL" -echo "APP_URL=$APP_URL" >> "$GITHUB_OUTPUT" diff --git a/.github/actions/deploy-to-control-plane/scripts/get-commit-sha.sh b/.github/actions/deploy-to-control-plane/scripts/get-commit-sha.sh new file mode 100755 index 00000000..9dd32cd0 --- /dev/null +++ b/.github/actions/deploy-to-control-plane/scripts/get-commit-sha.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +# This script retrieves the commit SHA for deployment +# It handles both PR and direct branch deployments +# +# Required environment variables: +# - PR_NUMBER: Pull request number (optional) +# - GITHUB_TOKEN: GitHub token for API access +# +# Outputs: +# - sha: Full commit SHA +# - sha_short: Short (7 char) commit SHA + +set -e + +if [ -n "${PR_NUMBER}" ]; then + # If PR_NUMBER is set, get the PR's head SHA + if ! PR_SHA=$(gh pr view "${PR_NUMBER}" --json headRefOid --jq '.headRefOid'); then + echo "Failed to get PR head SHA" >&2 + exit 1 + fi + echo "sha=${PR_SHA}" >> "$GITHUB_OUTPUT" + echo "sha_short=${PR_SHA:0:7}" >> "$GITHUB_OUTPUT" + echo "Using PR head commit SHA: ${PR_SHA:0:7}" +else + # For direct branch deployments, use the current commit SHA + if ! CURRENT_SHA=$(git rev-parse HEAD); then + echo "Failed to get current SHA" >&2 + exit 1 + fi + echo "sha=${CURRENT_SHA}" >> "$GITHUB_OUTPUT" + echo "sha_short=${CURRENT_SHA:0:7}" >> "$GITHUB_OUTPUT" + echo "Using branch commit SHA: ${CURRENT_SHA:0:7}" +fi diff --git a/.github/actions/setup-environment/action.yml b/.github/actions/setup-environment/action.yml index 710b6c71..829a9498 100644 --- a/.github/actions/setup-environment/action.yml +++ b/.github/actions/setup-environment/action.yml @@ -3,14 +3,6 @@ name: 'Setup Environment' description: 'Sets up Ruby, installs Control Plane CLI, cpflow gem, and sets up the default profile' -inputs: - token: - description: 'Control Plane token' - required: true - org: - description: 'Control Plane organization' - required: true - runs: using: 'composite' steps: @@ -22,7 +14,7 @@ runs: - name: Install Control Plane CLI and cpflow gem shell: bash run: | - sudo npm install -g @controlplane/cli@3.3.1 + sudo npm install -g @controlplane/cli@3.3.0 cpln --version gem install cpflow -v 4.1.0 cpflow --version @@ -30,22 +22,19 @@ runs: - name: Setup Control Plane Profile shell: bash run: | - TOKEN="${{ inputs.token }}" - ORG="${{ inputs.org }}" - - if [ -z "$TOKEN" ]; then - echo " Error: Control Plane token not provided" + if [ -z "$CPLN_TOKEN" ]; then + echo " Error: CPLN_TOKEN environment variable is not set" exit 1 fi - if [ -z "$ORG" ]; then - echo " Error: Control Plane organization not provided" + if [ -z "$CPLN_ORG" ]; then + echo " Error: CPLN_ORG environment variable is not set" exit 1 fi echo "Setting up Control Plane profile..." - echo "Organization: $ORG" - cpln profile update default --org "$ORG" --token "$TOKEN" + echo "Organization: $CPLN_ORG" + cpln profile update default --org "$CPLN_ORG" --token "$CPLN_TOKEN" echo "Setting up Docker login for Control Plane registry..." - cpln image docker-login --org "$ORG" + cpln image docker-login --org "$CPLN_ORG" diff --git a/.github/actions/validate-required-vars/action.yml b/.github/actions/validate-required-vars/action.yml deleted file mode 100644 index c370039d..00000000 --- a/.github/actions/validate-required-vars/action.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: 'Validate Required Variables' -description: 'Validates that all required secrets and variables for Control Plane operations' - -runs: - using: 'composite' - steps: - - name: Validate Required Secrets and Variables - shell: bash - run: | - missing=() - - # Check required secrets - if [ -z "$CPLN_TOKEN_STAGING" ]; then - missing+=("Secret: CPLN_TOKEN_STAGING") - fi - - # Check required variables - if [ -z "$CPLN_ORG_STAGING" ]; then - missing+=("Variable: CPLN_ORG_STAGING") - fi - if [ -z "$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 diff --git a/.github/readme.md b/.github/readme.md deleted file mode 100644 index 3b10fedc..00000000 --- a/.github/readme.md +++ /dev/null @@ -1,85 +0,0 @@ -# Developing and Testing Github Actions - -Testing Github Actions on an existing repository is tricky. - -The main issue boils down to the fact that Github Actions uses the workflow files in the branch where the event originates. This is fine for push events, but it becomes a problem when you want to test workflows that are triggered by comments on a pull request. - -Here's a summary of the behavior: - -Behavior of push and pull_request Events - 1. Push on a Branch: - โ€ข When you push changes to a branch (e.g., feature-branch), GitHub Actions uses the workflow files in that same branch. - โ€ข This is why changes to workflows work seamlessly when testing with push events. - 2. Pull Request Events: - โ€ข For pull_request events (e.g., a PR from feature-branch into master), GitHub Actions will always use the workflow files from the target branch (e.g., master), not the source branch (e.g., feature-branch). - โ€ข This is a security feature to prevent someone from introducing malicious code in a PR that modifies the workflow files themselves. - -Impact on Comment-Triggered Workflows - -When you want to trigger workflows via comments (issue_comment) in a pull request: - โ€ข The workflow code used will always come from the master branch (or the default branch), regardless of the branch where the PR originates. - โ€ข This means the PRโ€™s changes to the workflow wonโ€™t be used, and the action invoked by the comment will also use code from master. - -Workarounds to Test Comment-Triggered Workflows - -If you want to test workflows in a way that uses the changes in the pull request, here are your options: - -1. Use Push Events for Testing - โ€ข Test your changes on a branch with push triggers. - โ€ข Use workflow_dispatch to simulate the events you need (like invoking actions via comments). - -This allows you to confirm that your changes to the workflow file or actions behave as expected before merging into master. - -2. Merge the Workflow to master Temporarily - -If you absolutely need the workflow to run as part of a pull_request event: - 1. Merge your workflow changes into master temporarily. - 2. Open a PR to test your comment-triggered workflows. - 3. Revert the changes in master if necessary. - -This ensures the workflow changes are active in master while still testing with the pull_request context. - -3. Add Logic to Detect the Source Branch - -Use github.event.pull_request.head.ref to add custom logic in your workflow that behaves differently based on the source branch. - โ€ข Example: - -jobs: - test-pr: - runs-on: ubuntu-latest - if: ${{ github.event.pull_request.head.ref == 'feature-branch' }} - steps: - - name: Checkout Code - uses: actions/checkout@v3 - - - name: Debug - run: echo "Testing workflow changes in feature-branch" - -However, this still requires the workflow itself to exist in master. - -4. Use a Fork or a Temporary Repo - -Create a temporary repository or a fork to test workflows in isolation: - โ€ข Push your workflow changes to master in the test repository. - โ€ข Open a PR in the fork to test how workflows behave with issue_comment events and PR contexts. - -Once confirmed, you can replicate the changes in your main repository. - -6. Alternative Approach: Split Workflows - -If your workflow includes comment-based triggers (issue_comment), consider splitting your workflows: - โ€ข A base workflow in master that handles triggering. - โ€ข A test-specific workflow for validating changes on a branch. - -For example: - 1. The base workflow triggers when a comment like /run-tests is added. - 2. The test-specific workflow runs in response to the base workflow but uses the branchโ€™s code. - -Summary - โ€ข For push events: The branch-specific workflow is used, so testing changes is easy. - โ€ข For pull_request and issue_comment events: GitHub always uses workflows from the master branch, and thereโ€™s no direct way to bypass this. - -To test comment-triggered workflows: - 1. Use push or workflow_dispatch to validate changes. - 2. Merge workflow changes temporarily into master to test with pull_request events. - 3. Use tools like act for local simulation. diff --git a/.github/workflows/add-comment-on-pr-creation.yml b/.github/workflows/add-comment-on-pr-creation.yml index fa686305..bd4b69de 100644 --- a/.github/workflows/add-comment-on-pr-creation.yml +++ b/.github/workflows/add-comment-on-pr-creation.yml @@ -1,30 +1,20 @@ -name: Add Comment on PR Creation +name: Add helper Comment on PR creation on: pull_request: types: [opened] jobs: - add-comment: + comment-on-pr: runs-on: ubuntu-latest - permissions: - pull-requests: write steps: - - uses: actions/github-script@v7 - name: Add GitHub Comment for review app instructions - with: - script: | - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.payload.pull_request.number, - body: [ - "Hi ๐Ÿ‘‹ Here are the commands available for this PR:", - "", - "- `/deploy-review-app`: Deploy your changes to a review environment", - "- `/delete-review-app`: Clean up the review environment when you're done", - "- `/help`: Show detailed information about all commands", - "", - "Use `/help` to see full documentation, including configuration options." - ].join("\n") - }); + - name: Add GitHub Comment for review app instructions + uses: actions/github-script@v7 + with: + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: "Hi ๐Ÿ‘‹ To deploy a review app, please comment `/deploy-review-app`" + }) diff --git a/.github/workflows/debug-workflow.yml b/.github/workflows/debug-workflow.yml deleted file mode 100644 index 4ee0071a..00000000 --- a/.github/workflows/debug-workflow.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Debug Workflow Information - -on: - workflow_call: - inputs: - debug_enabled: - required: false - type: boolean - default: false - description: 'Enable debug logging (defaults to false)' - -jobs: - debug-info: - runs-on: ubuntu-latest - if: inputs.debug_enabled || vars.DEBUG_WORKFLOW == 'true' - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Log Branch Info - run: | - echo "Branch for this run:" - if [ "${{ github.event_name }}" == "pull_request" ]; then - echo "Pull Request Source Branch: ${{ github.head_ref }}" - else - echo "Branch: ${{ github.ref_name }}" - fi - - - name: Debug GitHub Context - run: | - echo "Event name: ${{ github.event_name }}" - echo "Event path: ${{ github.event_path }}" - echo "Repository: ${{ github.repository }}" - echo "Full GitHub context:" - echo '${{ toJson(github) }}' \ No newline at end of file diff --git a/.github/workflows/delete-review-app.yml b/.github/workflows/delete-review-app.yml index a0b3611a..074b7c7e 100644 --- a/.github/workflows/delete-review-app.yml +++ b/.github/workflows/delete-review-app.yml @@ -1,16 +1,8 @@ name: Delete Review App on: - pull_request: - types: [closed] issue_comment: types: [created] - workflow_dispatch: - inputs: - pr_number: - description: 'PR number to delete review app for' - required: true - type: string permissions: contents: read @@ -19,103 +11,67 @@ permissions: issues: write env: - CPLN_ORG: ${{ vars.CPLN_ORG_STAGING }} - CPLN_TOKEN: ${{ secrets.CPLN_TOKEN_STAGING }} - APP_NAME: ${{ vars.REVIEW_APP_PREFIX }}-pr-${{ github.event.pull_request.number || github.event.issue.number || inputs.pr_number }} - PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number || inputs.pr_number }} + CPLN_ORG: ${{ secrets.CPLN_ORG }} + CPLN_TOKEN: ${{ secrets.CPLN_TOKEN }} + APP_NAME: qa-react-webpack-rails-tutorial-pr-${{ github.event.issue.number }} + PR_NUMBER: ${{ github.event.issue.number }} jobs: - debug: - uses: ./.github/workflows/debug-workflow.yml - with: - debug_enabled: false # Will still run if vars.DEBUG_WORKFLOW is true + debug-trigger: + if: always() + runs-on: ubuntu-latest + steps: + - name: Debug Trigger Conditions + env: + EVENT_NAME: ${{ github.event_name }} + IS_PR: ${{ toJSON(github.event.issue.pull_request) }} + COMMENT: ${{ github.event.comment.body }} + run: | + echo "Debug information for delete-review-app command:" + echo "Event name: $EVENT_NAME" + echo "Is PR (raw): $IS_PR" + echo "Comment body: $COMMENT" + echo "Raw event payload:" + echo '${{ toJSON(github.event) }}' + Process-Delete-Command: + needs: debug-trigger if: | - (github.event_name == 'issue_comment' && - github.event.issue.pull_request && - github.event.comment.body == '/delete-review-app') || - (github.event_name == 'pull_request' && - github.event.action == 'closed') || - github.event_name == 'workflow_dispatch' + github.event_name == 'issue_comment' && + github.event.issue.pull_request && + github.event.comment.body == '/delete-review-app' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Validate Required Secrets and Variables - uses: ./.github/actions/validate-required-vars + - name: Validate Required Secrets + run: | + missing_secrets=() + for secret in "CPLN_TOKEN" "CPLN_ORG"; do + if [ -z "${!secret}" ]; then + missing_secrets+=("$secret") + fi + done + + if [ ${#missing_secrets[@]} -ne 0 ]; then + echo " Required secrets are not set: ${missing_secrets[*]}" + exit 1 + fi - name: Setup Environment uses: ./.github/actions/setup-environment - with: - org: ${{ env.CPLN_ORG }} - token: ${{ env.CPLN_TOKEN }} - - - name: Set shared functions - id: shared-functions - uses: actions/github-script@v7 - with: - script: | - core.exportVariable('GET_CONSOLE_LINK', ` - function getConsoleLink(prNumber) { - return '๐ŸŽฎ [Control Plane Console](' + - 'https://console.cpln.io/console/org/' + process.env.CPLN_ORG + '/gvc/' + process.env.APP_NAME + '/-info)'; - } - `); - - - name: Setup Workflow URL - id: setup-workflow-url - uses: actions/github-script@v7 - with: - script: | - async function getWorkflowUrl(runId) { - // Get the current job ID - const jobs = await github.rest.actions.listJobsForWorkflowRun({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: runId - }); - - const currentJob = jobs.data.jobs.find(job => job.status === 'in_progress'); - const jobId = currentJob?.id; - - if (!jobId) { - console.log('Warning: Could not find current job ID'); - return `${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`; - } - - return `${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}/job/${jobId}`; - } - - const workflowUrl = await getWorkflowUrl(context.runId); - core.exportVariable('WORKFLOW_URL', workflowUrl); - return { workflowUrl }; - name: Create Initial Delete Comment - id: create-delete-comment + id: init-delete uses: actions/github-script@v7 with: script: | - eval(process.env.GET_CONSOLE_LINK); - - let message = '๐Ÿ—‘๏ธ Starting app deletion'; - if ('${{ github.event_name }}' === 'pull_request') { - const merged = '${{ github.event.pull_request.merged }}' === 'true'; - message += merged ? ' (PR merged)' : ' (PR closed)'; - } - const comment = await github.rest.issues.createComment({ issue_number: process.env.PR_NUMBER, owner: context.repo.owner, repo: context.repo.repo, - body: '๐Ÿ—‘๏ธ Starting app deletion...' - body: [ - message, - '', - ' ๐Ÿ—‘๏ธ [View Delete Logs](' + process.env.WORKFLOW_URL + ')', - '', - getConsoleLink(process.env.PR_NUMBER) - ].join('\n') + body: ' Starting app deletion...' }); return { commentId: comment.data.id }; @@ -126,8 +82,6 @@ jobs: org: ${{ env.CPLN_ORG }} github_token: ${{ secrets.GITHUB_TOKEN }} env: - APP_NAME: ${{ env.APP_NAME }} - CPLN_ORG: ${{ secrets.CPLN_ORG }} CPLN_TOKEN: ${{ secrets.CPLN_TOKEN }} - name: Update Delete Status @@ -135,31 +89,21 @@ jobs: uses: actions/github-script@v7 with: script: | - eval(process.env.GET_CONSOLE_LINK); - const success = '${{ job.status }}' === 'success'; const prNumber = process.env.PR_NUMBER; const cpConsoleUrl = `https://console.cpln.io/org/${process.env.CPLN_ORG}/workloads/${process.env.APP_NAME}`; - const successMessage = [ - 'โœ… Review app for PR #' + prNumber + ' was successfully deleted', - '', - ' [View Completed Delete Logs](' + process.env.WORKFLOW_URL + ')', - '', - ' [Control Plane Organization](https://console.cpln.io/console/org/' + process.env.CPLN_ORG + '/-info)' - ].join('\n'); - - const failureMessage = [ - 'โŒ Review app for PR #' + prNumber + ' failed to be deleted', - '', - ' [View Delete Logs with Errors](' + process.env.WORKFLOW_URL + ')', - '', - getConsoleLink(prNumber) - ].join('\n'); + const message = success + ? ' Review app for PR #' + prNumber + ' was successfully deleted' + : [ + ' Review app for PR #' + prNumber + ' failed to be deleted', + '', + '[Control Plane Console for Review App with PR #' + prNumber + '](' + cpConsoleUrl + ')' + ].join('\n'); await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, - comment_id: ${{ fromJSON(steps.create-delete-comment.outputs.result).commentId }}, - body: success ? successMessage : failureMessage + comment_id: ${{ fromJSON(steps.init-delete.outputs.result).commentId }}, + body: message }); diff --git a/.github/workflows/deploy-to-control-plane-review-app.yml b/.github/workflows/deploy-to-control-plane-review-app.yml deleted file mode 100644 index ea05f98b..00000000 --- a/.github/workflows/deploy-to-control-plane-review-app.yml +++ /dev/null @@ -1,435 +0,0 @@ -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 - uses: ./.github/actions/validate-required-vars - - - 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: Setup Environment - uses: ./.github/actions/setup-environment - with: - token: ${{ secrets.CPLN_TOKEN_STAGING }} - org: ${{ vars.CPLN_ORG_STAGING }} - - - 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: 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: | - 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 - }); diff --git a/.github/workflows/deploy-to-control-plane-staging.yml b/.github/workflows/deploy-to-control-plane-staging.yml index d789d1ee..095c635a 100644 --- a/.github/workflows/deploy-to-control-plane-staging.yml +++ b/.github/workflows/deploy-to-control-plane-staging.yml @@ -1,96 +1,32 @@ # Control Plane GitHub Action -name: Deploy to Control Plane Staging -run-name: Deploy Control Plane Staging App +name: Deploy Main Branch to Control Plane Staging # Controls when the workflow will run on: + # Uncomment the lines you want actions that will cause the workflow to Triggers the workflow on push or pull request events but only for the main branch push: - branches: - - '*' + branches: [master] + + # Allows you to run this workflow manually from the Actions tab workflow_dispatch: # Convert the GitHub secret variables to environment variables for use by the Control Plane CLI env: - APP_NAME: ${{ vars.STAGING_APP_NAME }} - CPLN_TOKEN: ${{ secrets.CPLN_TOKEN_STAGING }} - CPLN_ORG: ${{ vars.CPLN_ORG_STAGING }} - STAGING_APP_BRANCH: ${{ vars.STAGING_APP_BRANCH }} - -concurrency: - group: deploy-staging - cancel-in-progress: true + CPLN_ORG: ${{secrets.CPLN_ORG_STAGING}} + CPLN_TOKEN: ${{secrets.CPLN_TOKEN_STAGING}} jobs: - debug: - uses: ./.github/workflows/debug-workflow.yml - with: - debug_enabled: false - - validate-branch: + deploy-to-control-plane-staging: runs-on: ubuntu-latest - outputs: - is_deployable: ${{ steps.check_branch.outputs.is_deployable }} - steps: - - name: Check if allowed branch - id: check_branch - run: | - if [[ -n "${STAGING_APP_BRANCH}" ]]; then - if [[ "${GITHUB_REF#refs/heads/}" == "${STAGING_APP_BRANCH}" ]]; then - echo "is_deployable=true" >> $GITHUB_OUTPUT - else - echo "Branch '${GITHUB_REF#refs/heads/}' is not the configured deployment branch '${STAGING_APP_BRANCH}'" - echo "is_deployable=false" >> $GITHUB_OUTPUT - fi - elif [[ "${GITHUB_REF}" == "refs/heads/main" || "${GITHUB_REF}" == "refs/heads/master" ]]; then - echo "is_deployable=true" >> $GITHUB_OUTPUT - else - echo "Branch '${GITHUB_REF#refs/heads/}' is not main/master (no STAGING_APP_BRANCH configured)" - echo "is_deployable=false" >> $GITHUB_OUTPUT - fi - - build: - needs: validate-branch - if: needs.validate-branch.outputs.is_deployable == 'true' - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - 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: ${{ github.sha }} - - deploy: - needs: build - runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Environment - uses: ./.github/actions/setup-environment + - uses: actions/checkout@v4 with: - token: ${{ secrets.CPLN_TOKEN_STAGING }} - org: ${{ vars.CPLN_ORG_STAGING }} + fetch-depth: 0 # Fetch all history for proper SHA handling + ref: master # Explicitly checkout master branch - - name: Deploy to Control Plane - uses: ./.github/actions/deploy-to-control-plane + - uses: ./.github/actions/deploy-to-control-plane with: - app_name: ${{ vars.STAGING_APP_NAME }} - org: ${{ vars.CPLN_ORG_STAGING }} - github_token: ${{ secrets.GITHUB_TOKEN }} - wait_timeout: ${{ vars.WAIT_TIMEOUT || 900 }} - cpln_token: ${{ secrets.CPLN_TOKEN_STAGING }} + app_name: ${{ secrets.APP_NAME_STAGING }} + org: ${{ secrets.CPLN_ORG_STAGING }} diff --git a/.github/workflows/deploy-to-control-plane.yml b/.github/workflows/deploy-to-control-plane.yml new file mode 100644 index 00000000..ad1dbb49 --- /dev/null +++ b/.github/workflows/deploy-to-control-plane.yml @@ -0,0 +1,404 @@ +name: Deploy Review App to Control Plane + +run-name: ${{ (github.event_name == 'pull_request' || (github.event_name == 'issue_comment' && github.event.issue.pull_request)) && 'Deploying Review App' || format('Deploying {0} to Staging App', github.ref_name) }} + +on: + pull_request: + types: [opened, synchronize, reopened] + issue_comment: + types: [created] + +# Use concurrency to cancel in-progress runs +concurrency: + group: deploy-${{ github.event.pull_request.number || github.event.issue.number }} + cancel-in-progress: true + +env: + APP_NAME: qa-react-webpack-rails-tutorial-pr-${{ github.event.pull_request.number || github.event.issue.number }} + CPLN_ORG: ${{ secrets.CPLN_ORG }} + CPLN_TOKEN: ${{ secrets.CPLN_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} + +jobs: + Process-Deployment-Command: + if: | + (github.event_name == 'pull_request') || + (github.event_name == 'issue_comment' && + github.event.issue.pull_request && + github.event.comment.body == '/deploy') + runs-on: ubuntu-latest + permissions: + contents: read + deployments: write + pull-requests: write + issues: write + + steps: + - name: Get PR HEAD Ref + if: github.event_name == 'issue_comment' + id: getRef + run: | + # For PR comments, get the actual PR head commit + PR_DATA=$(gh pr view $PR_NUMBER --repo ${{ github.repository }} --json headRefName,headRefOid) + echo "PR_REF=$(echo "$PR_DATA" | jq -r '.headRefName')" >> $GITHUB_OUTPUT + echo "PR_SHA=$(echo "$PR_DATA" | jq -r '.headRefOid')" >> $GITHUB_OUTPUT + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || steps.getRef.outputs.PR_REF || github.ref }} + + - name: Validate Required Secrets + run: | + missing_secrets=() + for secret in "CPLN_TOKEN" "CPLN_ORG"; do + if [ -z "${!secret}" ]; then + missing_secrets+=("$secret") + fi + done + + if [ ${#missing_secrets[@]} -ne 0 ]; then + echo "Required secrets are not set: ${missing_secrets[*]}" + exit 1 + fi + + - name: Setup Environment + uses: ./.github/actions/setup-environment + + - name: Set shared functions + id: shared-functions + uses: actions/github-script@v7 + with: + script: | + core.exportVariable('GET_CONSOLE_LINK', ` + function getConsoleLink(prNumber) { + return ' [Control Plane Console for Review App with PR #' + prNumber + '](' + + 'https://console.cpln.io/org/' + process.env.CPLN_ORG + '/workloads/' + process.env.APP_NAME + ')'; + } + `); + + - name: Initialize Deployment + id: init-deployment + uses: actions/github-script@v7 + with: + script: | + eval(process.env.GET_CONSOLE_LINK); + + async function getWorkflowUrl(runId) { + // Get the current job ID + const jobs = await github.rest.actions.listJobsForWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: runId + }); + + const currentJob = jobs.data.jobs.find(job => job.status === 'in_progress'); + const jobId = currentJob?.id; + + if (!jobId) { + console.log('Warning: Could not find current job ID'); + return `${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`; + } + + return `${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}/job/${jobId}`; + } + + // Create initial deployment comment + const comment = await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: process.env.PR_NUMBER, + body: ' Initializing deployment...' + }); + + // Create GitHub deployment + const deployment = await github.rest.repos.createDeployment({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: context.sha, + environment: 'review', + auto_merge: false, + required_contexts: [] + }); + + const workflowUrl = await getWorkflowUrl(context.runId); + + return { + deploymentId: deployment.data.id, + commentId: comment.data.id, + workflowUrl + }; + + - name: Set comment ID and workflow URL + run: | + echo "COMMENT_ID=${{ fromJSON(steps.init-deployment.outputs.result).commentId }}" >> $GITHUB_ENV + echo "WORKFLOW_URL=${{ fromJSON(steps.init-deployment.outputs.result).workflowUrl }}" >> $GITHUB_ENV + + - name: Set commit hash + run: | + FULL_COMMIT="${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || steps.getRef.outputs.PR_SHA || github.sha }}" + echo "COMMIT_HASH=${FULL_COMMIT:0:7}" >> $GITHUB_ENV + + - name: Update Status - Building + uses: actions/github-script@v7 + with: + script: | + eval(process.env.GET_CONSOLE_LINK); + + const buildingMessage = [ + ' Building Docker image for PR #' + process.env.PR_NUMBER + ', commit ' + '${{ env.COMMIT_HASH }}', + '', + ' [View Build Logs](' + process.env.WORKFLOW_URL + ')', + '', + getConsoleLink(process.env.PR_NUMBER) + ].join('\n'); + + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: process.env.COMMENT_ID, + body: buildingMessage + }); + + - name: Build Docker Image + uses: ./.github/actions/build-docker-image + with: + app_name: ${{ env.APP_NAME }} + org: ${{ env.CPLN_ORG }} + commit: ${{ env.COMMIT_HASH }} + PR_NUMBER: ${{ env.PR_NUMBER }} + + - name: Update Status - Deploying + uses: actions/github-script@v7 + with: + script: | + eval(process.env.GET_CONSOLE_LINK); + + const deployingMessage = [ + ' Deploying to Control Plane...', + '', + ' Waiting for deployment to be ready...', + '', + ' [View Deploy Logs](' + process.env.WORKFLOW_URL + ')', + '', + getConsoleLink(process.env.PR_NUMBER) + ].join('\n'); + + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: process.env.COMMENT_ID, + body: deployingMessage + }); + + - name: Deploy to Control Plane + uses: ./.github/actions/deploy-to-control-plane + with: + app_name: ${{ env.APP_NAME }} + org: ${{ env.CPLN_ORG }} + github_token: ${{ secrets.GITHUB_TOKEN }} + wait_timeout: ${{ vars.WAIT_TIMEOUT || 900 }} + + - name: Update Status - Deployment Complete + uses: actions/github-script@v7 + with: + script: | + eval(process.env.GET_CONSOLE_LINK); + + const prNumber = process.env.PR_NUMBER; + const appUrl = process.env.REVIEW_APP_URL; + const workflowUrl = process.env.WORKFLOW_URL; + const isSuccess = '${{ job.status }}' === 'success'; + + // Create GitHub deployment status + const deploymentStatus = { + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: ${{ fromJSON(steps.init-deployment.outputs.result).deploymentId }}, + 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.COMMIT_HASH }}', + '', + ' [Review App for PR #' + prNumber + '](' + appUrl + ')', + '', + ' [View Completed Action Build and Deploy Logs](' + workflowUrl + ')', + '', + getConsoleLink(prNumber) + ].join('\n'); + + const failureMessage = [ + ' Deployment failed for PR #' + prNumber + ', commit ' + '${{ env.COMMIT_HASH }}', + '', + ' [View Deployment Logs with Errors](' + workflowUrl + ')', + '', + getConsoleLink(prNumber) + ].join('\n'); + + // Update the existing comment + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: process.env.COMMENT_ID, + body: isSuccess ? successMessage : failureMessage + }); + + debug-help: + if: always() + runs-on: ubuntu-latest + steps: + - name: Debug Trigger Conditions + env: + EVENT_NAME: ${{ github.event_name }} + IS_PR: ${{ toJSON(github.event.issue.pull_request) }} + COMMENT: ${{ github.event.comment.body }} + run: | + echo "Debug information for help command:" + echo "Event name: $EVENT_NAME" + echo "Is PR (raw): $IS_PR" + echo "Comment body: $COMMENT" + echo "Raw event payload:" + echo '${{ toJSON(github.event) }}' + + show-help: + needs: debug-help + if: | + github.event_name == 'issue_comment' && + github.event.issue.pull_request && + github.event.comment.body == '/help' + runs-on: ubuntu-latest + + steps: + - name: Show Available Commands + uses: actions/github-script@v7 + with: + script: | + const helpMessage = [ + '## Available Commands', + '', + '### `/deploy`', + 'Deploys your PR branch to a review environment on Control Plane.', + '- Creates a new review app if one doesn\'t exist', + '- Updates the existing review app if it already exists', + '- Provides a unique URL to preview your changes', + '- Shows build and deployment progress in real-time', + '', + '### `/delete-review-app`', + 'Deletes the review app associated with this PR.', + '- Removes all resources from Control Plane', + '- Helpful for cleaning up when you\'re done testing', + '- Can be re-deployed later using `/deploy`', + '', + '### `/help`', + 'Shows this help message explaining available commands.', + '', + '---', + '_Note: These commands only work in pull request comments._' + ].join('\n'); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.issue.number, + body: helpMessage + }); + + debug-delete: + if: always() + runs-on: ubuntu-latest + steps: + - name: Debug Trigger Conditions + env: + EVENT_NAME: ${{ github.event_name }} + IS_PR: ${{ toJSON(github.event.issue.pull_request) }} + COMMENT: ${{ github.event.comment.body }} + run: | + echo "Debug information for delete-review-app command:" + echo "Event name: $EVENT_NAME" + echo "Is PR (raw): $IS_PR" + echo "Comment body: $COMMENT" + echo "Raw event payload:" + echo '${{ toJSON(github.event) }}' + + Process-Delete-Command: + needs: debug-delete + if: | + github.event_name == 'issue_comment' && + github.event.issue.pull_request && + github.event.comment.body == '/delete-review-app' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Validate Required Secrets + run: | + missing_secrets=() + for secret in "CPLN_TOKEN" "CPLN_ORG"; do + if [ -z "${!secret}" ]; then + missing_secrets+=("$secret") + fi + done + + if [ ${#missing_secrets[@]} -ne 0 ]; then + echo "Required secrets are not set: ${missing_secrets[*]}" + exit 1 + fi + + - name: Setup Environment + uses: ./.github/actions/setup-environment + + - name: Create Initial Delete Comment + id: init-delete + uses: actions/github-script@v7 + with: + script: | + const comment = await github.rest.issues.createComment({ + issue_number: process.env.PR_NUMBER, + owner: context.repo.owner, + repo: context.repo.repo, + body: ' Starting app deletion...' + }); + return { commentId: comment.data.id }; + + - name: Delete Review App + uses: ./.github/actions/delete-control-plane-app + with: + app_name: ${{ env.APP_NAME }} + org: ${{ env.CPLN_ORG }} + github_token: ${{ secrets.GITHUB_TOKEN }} + env: + CPLN_TOKEN: ${{ secrets.CPLN_TOKEN }} + + - name: Update Delete Status + if: always() + uses: actions/github-script@v7 + with: + script: | + const success = '${{ job.status }}' === 'success'; + const prNumber = process.env.PR_NUMBER; + const cpConsoleUrl = `https://console.cpln.io/org/${process.env.CPLN_ORG}/workloads/${process.env.APP_NAME}`; + + const message = success + ? ' Review app for PR #' + prNumber + ' was successfully deleted' + : [ + ' Review app for PR #' + prNumber + ' failed to be deleted', + '', + ' [Control Plane Console for Review App with PR #' + prNumber + '](' + cpConsoleUrl + ')' + ].join('\n'); + + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: ${{ fromJSON(steps.init-delete.outputs.result).commentId }}, + body: message + }); \ No newline at end of file diff --git a/.github/workflows/help-command.yml b/.github/workflows/help-command.yml index 51ce2566..20a0e828 100644 --- a/.github/workflows/help-command.yml +++ b/.github/workflows/help-command.yml @@ -1,151 +1,90 @@ -name: Help Command +name: Show Help for Commands on: issue_comment: types: [created] - workflow_dispatch: - inputs: - pr_number: - description: 'Pull Request number to post help comment on' - required: true - type: string permissions: issues: write pull-requests: write jobs: - help: - if: ${{ (github.event.issue.pull_request && github.event.comment.body == '/help') || github.event_name == 'workflow_dispatch' }} + debug-trigger: + if: always() runs-on: ubuntu-latest - + steps: + - name: Debug Trigger Conditions + env: + EVENT_NAME: ${{ github.event_name }} + IS_PR: ${{ toJSON(github.event.issue.pull_request) }} + COMMENT: ${{ github.event.comment.body }} + run: | + echo "Debug information for help command:" + echo "Event name: $EVENT_NAME" + echo "Is PR (raw): $IS_PR" + echo "Comment body: $COMMENT" + echo "Raw event payload:" + echo '${{ toJSON(github.event) }}' + + show-help: + needs: debug-trigger + if: | + github.event_name == 'issue_comment' && + github.event.issue.pull_request && + github.event.comment.body == '/help' + runs-on: ubuntu-latest + steps: - name: Show Available Commands uses: actions/github-script@v7 with: script: | - const sections = { - commands: { - deploy: { - title: '## `/deploy`', - purpose: '**Purpose:** Deploy a review app for your pull request', - details: [ - '**What it does:**', - '- Creates a new review app in Control Plane', - '- Deploys your changes to the review environment', - '- Provides a unique URL to preview your changes', - '- Shows build and deployment progress in real-time', - '', - '**Optional Configuration:**', - '- `WAIT_TIMEOUT`: Deployment timeout in seconds (default: 900)', - ' - Must be a positive integer', - ' - Example: `/deploy timeout=1800`' - ] - }, - destroy: { - title: '## `/destroy`', - purpose: '**Purpose:** Remove the review app for your pull request', - details: [ - '**What it does:**', - '- Deletes the review app from Control Plane', - '- Cleans up associated resources', - '- Updates PR with deletion status' - ] - } - }, - setup: { - title: '## Environment Setup', - sections: [ - { - title: '**Required Environment Secrets:**', - items: [ - '- `CPLN_TOKEN_STAGING`: Control Plane authentication token', - '- `CPLN_TOKEN_PRODUCTION`: Control Plane authentication token' - ] - }, - { - title: '**Required GitHub Actions Variables:**', - items: [ - '- `CPLN_ORG_STAGING`: Control Plane authentication token', - '- `CPLN_ORG_PRODUCTION`: Control Plane authentication token' - ] - }, - { - title: '**Required GitHub Actions Variables (these need to match your control_plane.yml file:**', - items: [ - '- `PRODUCTION_APP_NAME`: Control Plane production app name', - '- `STAGING_APP_NAME`: Control Plane staging app name', - '- `REVIEW_APP_PREFIX`: Control Plane review app prefix' - ] - } - ], - note: 'Optional: Configure `WAIT_TIMEOUT` in GitHub Actions variables to customize deployment timeout' - }, - integration: { - title: '## Control Plane Integration', - details: [ - '1. Review app naming convention:', - ' ```', - ' ${{ vars.REVIEW_APP_PREFIX }}-', - ' ```', - '2. Console URL: `https://console.cpln.io/console/org/{CPLN_ORG}/gvc/{APP_NAME}/-info`' - ] - }, - cleanup: { - title: '## Automatic Cleanup', - details: [ - 'Review apps are automatically destroyed when:', - '1. The pull request is closed', - '2. The `/destroy` command is used', - '3. A new deployment is requested (old one is cleaned up first)' - ] - }, - help: { - title: '## Need Help?', - details: [ - 'For additional assistance:', - '1. Check the [Control Plane documentation](https://docs.controlplane.com/)', - '2. Contact the infrastructure team', - '3. Open an issue in this repository' - ] - } - }; - - const generateHelpText = () => { - const parts = ['# Available Commands', '']; - - // Add commands - Object.values(sections.commands).forEach(cmd => { - parts.push(cmd.title, cmd.purpose, '', ...cmd.details, ''); - }); - - parts.push('---'); - - // Add setup section - parts.push(sections.setup.title, ''); - sections.setup.sections.forEach(section => { - parts.push(section.title, ...section.items, ''); - }); - parts.push(sections.setup.note, ''); - - // Add remaining sections - ['integration', 'cleanup', 'help'].forEach(section => { - parts.push(sections[section].title, '', ...sections[section].details, ''); - }); - - return parts.join('\n'); - }; - - const helpText = generateHelpText(); - - const prNumber = context.eventName === 'workflow_dispatch' - ? parseInt(context.payload.inputs.pr_number) - : context.issue.number; + const helpMessage = [ + '## ๐Ÿ“š Available Commands', + '', + '### `/deploy`', + 'Deploys your PR branch to a review environment on Control Plane.', + '- Creates a new review app if one doesn\'t exist', + '- Updates the existing review app if it already exists', + '- Provides a unique URL to preview your changes', + '- Shows build and deployment progress in real-time', + '', + '**Required Environment Variables:**', + '- `CPLN_TOKEN`: Control Plane authentication token', + '- `CPLN_ORG`: Control Plane organization name', + '', + '**Optional Configuration:**', + '- `WAIT_TIMEOUT`: Deployment timeout in seconds (default: 900)', + ' - Must be a positive integer', + ' - Can be set in GitHub Actions variables', + ' - Applies to both deployment and workload readiness checks', + '', + '### `/delete-review-app`', + 'Deletes the review app associated with this PR.', + '- Removes all resources from Control Plane', + '- Helpful for cleaning up when you\'re done testing', + '- Can be re-deployed later using `/deploy`', + '', + '**Required Environment Variables:**', + '- `CPLN_TOKEN`: Control Plane authentication token', + '- `CPLN_ORG`: Control Plane organization name', + '', + '### `/help`', + 'Shows this help message explaining available commands and configuration.', + '', + '---', + '**Note:** These commands only work in pull request comments.', + '', + '**Environment Setup:**', + '1. Set required secrets in your repository settings:', + ' - `CPLN_TOKEN`', + ' - `CPLN_ORG`', + '2. Optional: Configure `WAIT_TIMEOUT` in GitHub Actions variables to customize deployment timeout' + ].join('\n'); await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, - issue_number: prNumber, - body: helpText + issue_number: context.payload.issue.number, + body: helpMessage }); - \ No newline at end of file diff --git a/.github/workflows/review-app-help.yml b/.github/workflows/review-app-help.yml deleted file mode 100644 index d5eed23c..00000000 --- a/.github/workflows/review-app-help.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: Show Quick Help on PR Creation - -on: - pull_request: - types: [opened] - -permissions: - issues: write - pull-requests: write - -jobs: - debug: - uses: ./.github/workflows/debug-workflow.yml - with: - debug_enabled: false # Will still run if vars.DEBUG_WORKFLOW is true - - show-quick-help: - if: github.event_name == 'pull_request' - runs-on: ubuntu-latest - - steps: - - name: Show Quick Reference - uses: actions/github-script@v7 - with: - script: | - try { - console.log('Creating quick reference message...'); - const helpMessage = [ - '# ๐Ÿš€ Quick Review App Commands', - '', - 'Welcome! Here are the commands you can use in this PR:', - '', - '### `/deploy-review-app`', - 'Deploy your PR branch for testing', - '', - '### `/delete-review-app`', - 'Remove the review app when done', - '', - '### `/help`', - 'Show detailed instructions, environment setup, and configuration options.', - '', - '---' - ].join('\n'); - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: helpMessage - }); - - console.log('Quick reference posted successfully'); - } catch (error) { - console.error('Error posting quick reference:', error); - core.setFailed(`Failed to post quick reference: ${error.message}`); - }