diff --git a/.controlplane/controlplane.yml b/.controlplane/controlplane.yml index f394fe1cb..1d6c75f9f 100644 --- a/.controlplane/controlplane.yml +++ b/.controlplane/controlplane.yml @@ -39,23 +39,24 @@ aliases: apps: react-webpack-rails-tutorial-production: - # Simulate Production Version + # Simulate Production Version. Create with this command: + # cpflow apply-template app postgres redis daily-task rails -a react-webpack-rails-tutorial-production -o shakacode-open-source-examples-production <<: *common # Don't allow overriding the org and app by ENV vars b/c production is sensitive! allow_org_override_by_env: false allow_app_override_by_env: false - # Use a different organization for production. - cpln_org: shakacode-open-source-examples + # Use a different organization only for production. + cpln_org: shakacode-open-source-examples-production upstream: react-webpack-rails-tutorial-staging react-webpack-rails-tutorial-staging: <<: *common - # QA Apps are like Heroku review apps, but the use `prefix` so you can run a commmand like - # this to create a QA app for the tutorial app. - # `cpflow setup gvc postgres redis rails -a qa-react-webpack-rails-tutorial-pr-1234` qa-react-webpack-rails-tutorial: + # Review Apps are like Heroku review apps, but the use `prefix` so you can run a command like + # this to create a QA app for the tutorial app. + # `cpflow setup-app -a qa-react-webpack-rails-tutorial-pr-1234` <<: *common # Order matters! setup_app_templates: diff --git a/.controlplane/templates/app.yml b/.controlplane/templates/app.yml index 09249b7d0..aeba0e44c 100644 --- a/.controlplane/templates/app.yml +++ b/.controlplane/templates/app.yml @@ -28,3 +28,17 @@ spec: # Identity is needed to access secrets kind: identity name: {{APP_IDENTITY}} + + I stay at 3% and … believe it or not - I recommend that the Buyer’s Agent also gets 3%! + + Here’s WHY: + + -Buyers are still ’SCARCE’… and ANY Buyer's Agent, that actually has one of these rare things called Beachfront Buyers, is looking to earn a full 3% of the Sales Price. + + NOTE: They are all going to put 3% in their Buyer Agency Agreements… just ‘in case’ that 3% 'hoped for' commission is offered. + + Therefore - if you lower that commission - the Buyer has to come up with the difference “In Cash” … and, that’s not good for the Buyer who wants to finance 80%, or more, of that commission he’s agreed to pay his Agent. + + SO + + Why not OFFER that 3% and stick to this Asking Price (or near it)!? diff --git a/.github/actions/validate-required-vars/action.yml b/.github/actions/validate-required-vars/action.yml index c370039d0..98ff5b9f9 100644 --- a/.github/actions/validate-required-vars/action.yml +++ b/.github/actions/validate-required-vars/action.yml @@ -1,6 +1,29 @@ name: 'Validate Required Variables' description: 'Validates that all required secrets and variables for Control Plane operations' +inputs: + CPLN_TOKEN_STAGING: + required: true + description: 'Control Plane Staging Token' + CPLN_TOKEN_PRODUCTION: + required: true + description: 'Control Plane Production Token' + CPLN_ORG_STAGING: + required: true + description: 'Control Plane Staging Organization' + CPLN_ORG_PRODUCTION: + required: true + description: 'Control Plane Production Organization' + REVIEW_APP_PREFIX: + required: true + description: 'Review App Prefix' + PRODUCTION_APP_NAME: + required: true + description: 'Production App Name' + STAGING_APP_NAME: + required: true + description: 'Staging App Name' + runs: using: 'composite' steps: @@ -10,19 +33,37 @@ runs: missing=() # Check required secrets - if [ -z "$CPLN_TOKEN_STAGING" ]; then + if [ -z "${{ inputs.CPLN_TOKEN_STAGING }}" ]; then missing+=("Secret: CPLN_TOKEN_STAGING") fi + + if [ -z "${{ inputs.CPLN_TOKEN_PRODUCTION }}" ]; then + missing+=("Secret: CPLN_TOKEN_PRODUCTION") + fi # Check required variables - if [ -z "$CPLN_ORG_STAGING" ]; then + if [ -z "${{ inputs.CPLN_ORG_STAGING }}" ]; then missing+=("Variable: CPLN_ORG_STAGING") fi - if [ -z "$REVIEW_APP_PREFIX" ]; then + + if [ -z "${{ inputs.CPLN_ORG_PRODUCTION }}" ]; then + missing+=("Variable: CPLN_ORG_PRODUCTION") + fi + + if [ -z "${{ inputs.REVIEW_APP_PREFIX }}" ]; then missing+=("Variable: REVIEW_APP_PREFIX") fi + + if [ -z "${{ inputs.PRODUCTION_APP_NAME }}" ]; then + missing+=("Variable: PRODUCTION_APP_NAME") + fi + + if [ -z "${{ inputs.STAGING_APP_NAME }}" ]; then + missing+=("Variable: STAGING_APP_NAME") + fi if [ ${#missing[@]} -ne 0 ]; then - echo "Required secrets/variables are not set: ${missing[*]}" + echo "Missing required secrets/variables:" + printf '%s\n' "${missing[@]}" exit 1 fi diff --git a/.github/workflows/delete-review-app.yml b/.github/workflows/delete-review-app.yml index a0b3611a8..bbfda93c2 100644 --- a/.github/workflows/delete-review-app.yml +++ b/.github/workflows/delete-review-app.yml @@ -44,6 +44,14 @@ jobs: - name: Validate Required Secrets and Variables uses: ./.github/actions/validate-required-vars + with: + CPLN_TOKEN_STAGING: ${{ secrets.CPLN_TOKEN_STAGING }} + CPLN_TOKEN_PRODUCTION: ${{ secrets.CPLN_TOKEN_PRODUCTION }} + CPLN_ORG_STAGING: ${{ vars.CPLN_ORG_STAGING }} + CPLN_ORG_PRODUCTION: ${{ vars.CPLN_ORG_PRODUCTION }} + REVIEW_APP_PREFIX: ${{ vars.REVIEW_APP_PREFIX }} + PRODUCTION_APP_NAME: ${{ vars.PRODUCTION_APP_NAME }} + STAGING_APP_NAME: ${{ vars.STAGING_APP_NAME }} - name: Setup Environment uses: ./.github/actions/setup-environment @@ -108,7 +116,6 @@ jobs: issue_number: process.env.PR_NUMBER, owner: context.repo.owner, repo: context.repo.repo, - body: '🗑️ Starting app deletion...' body: [ message, '', @@ -125,10 +132,6 @@ jobs: app_name: ${{ env.APP_NAME }} 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 if: always() @@ -163,3 +166,15 @@ jobs: comment_id: ${{ fromJSON(steps.create-delete-comment.outputs.result).commentId }}, body: success ? successMessage : failureMessage }); + + - name: Debug Environment + run: | + echo "Organization: ${{ env.CPLN_ORG }}" + echo "App Name: ${{ env.APP_NAME }}" + echo "PR Number: ${{ env.PR_NUMBER }}" + # Don't echo the actual token, but verify it exists + if [ -n "${{ env.CPLN_TOKEN }}" ]; then + echo "CPLN_TOKEN is set" + else + echo "CPLN_TOKEN is empty" + fi diff --git a/.github/workflows/deploy-to-control-plane-review-app.yml b/.github/workflows/deploy-to-control-plane-review-app.yml index ea05f98b0..0a0188a5f 100644 --- a/.github/workflows/deploy-to-control-plane-review-app.yml +++ b/.github/workflows/deploy-to-control-plane-review-app.yml @@ -5,11 +5,6 @@ run-name: Deploy PR Review App - PR #${{ github.event.pull_request.number || git on: pull_request: types: [opened, synchronize, reopened] - push: - branches: - - '**' # Any branch - - '!main' # Except main - - '!master' # Except master issue_comment: types: [created] workflow_dispatch: @@ -71,6 +66,14 @@ jobs: - name: Validate Required Secrets and Variables uses: ./.github/actions/validate-required-vars + with: + CPLN_TOKEN_STAGING: ${{ secrets.CPLN_TOKEN_STAGING }} + CPLN_TOKEN_PRODUCTION: ${{ secrets.CPLN_TOKEN_PRODUCTION }} + CPLN_ORG_STAGING: ${{ vars.CPLN_ORG_STAGING }} + CPLN_ORG_PRODUCTION: ${{ vars.CPLN_ORG_PRODUCTION }} + REVIEW_APP_PREFIX: ${{ vars.REVIEW_APP_PREFIX }} + PRODUCTION_APP_NAME: ${{ vars.PRODUCTION_APP_NAME }} + STAGING_APP_NAME: ${{ vars.STAGING_APP_NAME }} - name: Get PR HEAD Ref id: getRef @@ -131,9 +134,9 @@ jobs: fi fi - # Extract and set PR data + # Set PR_NUMBER and override APP_NAME with validated PR number echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV - echo "APP_NAME=${{ vars.REVIEW_APP_PREFIX }}-$PR_NUMBER" >> $GITHUB_ENV + echo "APP_NAME=${{ vars.REVIEW_APP_PREFIX }}-pr-$PR_NUMBER" >> $GITHUB_ENV echo "PR_REF=$(echo $PR_DATA | jq -r .headRefName)" >> $GITHUB_OUTPUT echo "PR_SHA=$(echo $PR_DATA | jq -r .headRefOid)" >> $GITHUB_ENV @@ -170,42 +173,16 @@ jobs: exit 0 fi + # Validate supported event types if ! [[ "${{ github.event_name }}" == "workflow_dispatch" || \ "${{ github.event_name }}" == "issue_comment" || \ - "${{ github.event_name }}" == "pull_request" || \ - "${{ github.event_name }}" == "push" ]]; then + "${{ github.event_name }}" == "pull_request" ]]; 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' + if: env.DO_DEPLOY != 'false' && env.APP_EXISTS == 'false' env: CPLN_TOKEN: ${{ secrets.CPLN_TOKEN_STAGING }} run: | @@ -222,7 +199,17 @@ jobs: owner: context.repo.owner, repo: context.repo.repo, issue_number: process.env.PR_NUMBER, - body: '🚀 Starting deployment process...\n\n' + process.env.CONSOLE_LINK + body: [ + `🏗️ Building Docker image for PR [#${process.env.PR_NUMBER}](${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/pull/${process.env.PR_NUMBER}), commit [${context.sha.substring(0, 7)}](${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/commit/${context.sha})`, + '', + '🚀 Deploying to Control Plane...', + '', + '⏳ Waiting for deployment to be ready...', + '', + `📝 [View Build and Deploy Logs](${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}/job/${context.job})`, + '', + process.env.CONSOLE_LINK + ].join('\n') }); core.setOutput('comment-id', result.data.id); @@ -252,7 +239,7 @@ jobs: }; const workflowUrl = await getWorkflowUrl(context.runId); - core.exportVariable('WORKFLOW_URL', workflowUrl); + core.exportVariable('BUILD_LOGS_URL', workflowUrl); core.exportVariable('CONSOLE_LINK', '🎮 [Control Plane Console](' + 'https://console.cpln.io/console/org/' + process.env.CPLN_ORG + '/gvc/' + process.env.APP_NAME + '/-info)' @@ -309,25 +296,6 @@ jobs: 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 @@ -355,21 +323,43 @@ jobs: 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'); + // Create deployment status for deploying state + await github.rest.repos.createDeploymentStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: ${{ needs.process-deployment.outputs.deployment_id }}, + state: 'in_progress', + description: 'Deployment in progress', + log_url: process.env.BUILD_LOGS_URL + }); + + // Get the current job URL and ID + const { data: jobs } = await github.rest.actions.listJobsForWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.runId + }); + const currentJob = jobs.jobs.find(job => job.name === context.job); + const currentJobUrl = `${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}/job/${currentJob.id}`; + + // Update the PR comment with correct job URLs await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, - comment_id: ${{ steps.create-comment.outputs.comment-id }}, - body: deployingMessage + comment_id: ${{ needs.build.outputs.comment_id }}, + body: [ + `🏗️ Built Docker image for PR [#${process.env.PR_NUMBER}](${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/pull/${process.env.PR_NUMBER}), commit [${process.env.PR_SHA.substring(0, 7)}](${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/commit/${process.env.PR_SHA})`, + '', + '🚀 Deploying to Control Plane...', + '', + '⏳ Waiting for deployment to be ready...', + '', + process.env.CONSOLE_LINK, + '', + `📝 [View Build Logs](${process.env.BUILD_LOGS_URL})`, + `📝 [View Deploy Logs](${currentJobUrl})` + ].join('\n') }); - name: Deploy to Control Plane @@ -395,6 +385,16 @@ jobs: const consoleLink = process.env.CONSOLE_LINK; + // Get current job ID for accurate logs URL + const { data: jobs } = await github.rest.actions.listJobsForWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.runId + }); + + const currentJob = jobs.jobs.find(job => job.name === context.job); + const logsUrl = `${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}/job/${currentJob.id}`; + // Create GitHub deployment status const deploymentStatus = { owner: context.repo.owner, @@ -402,7 +402,7 @@ jobs: deployment_id: ${{ steps.init-deployment.outputs.result }}, state: isSuccess ? 'success' : 'failure', environment_url: isSuccess ? appUrl : undefined, - log_url: workflowUrl, + log_url: logsUrl, environment: 'review' }; @@ -415,7 +415,7 @@ jobs: '🚀 [Review App for PR #' + prNumber + '](' + appUrl + ')', consoleLink, '', - '📋 [View Completed Action Build and Deploy Logs](' + workflowUrl + ')' + `📝 [View Build and Deploy Logs](${logsUrl})`, ].join('\n'); const failureMessage = [ @@ -423,13 +423,13 @@ jobs: '', consoleLink, '', - '📋 [View Deployment Logs with Errors](' + workflowUrl + ')' + `📝 [View Build and Deploy Logs with Errors](${logsUrl})`, ].join('\n'); // Update the existing comment await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, - comment_id: ${{ steps.create-comment.outputs.comment-id }}, + comment_id: ${{ needs.build.outputs.comment_id }}, body: isSuccess ? successMessage : failureMessage }); diff --git a/.github/workflows/help-command.yml b/.github/workflows/help-command.yml index 51ce25663..3d578ad2e 100644 --- a/.github/workflows/help-command.yml +++ b/.github/workflows/help-command.yml @@ -18,7 +18,7 @@ jobs: help: if: ${{ (github.event.issue.pull_request && github.event.comment.body == '/help') || github.event_name == 'workflow_dispatch' }} runs-on: ubuntu-latest - + steps: - name: Show Available Commands uses: actions/github-script@v7 @@ -26,8 +26,8 @@ jobs: script: | const sections = { commands: { - deploy: { - title: '## `/deploy`', + "deploy-review-app": { + title: '## `/deploy-review-app`', purpose: '**Purpose:** Deploy a review app for your pull request', details: [ '**What it does:**', @@ -42,8 +42,8 @@ jobs: ' - Example: `/deploy timeout=1800`' ] }, - destroy: { - title: '## `/destroy`', + "delete-review-app: { + title: '## `/delete-review-app`', purpose: '**Purpose:** Remove the review app for your pull request', details: [ '**What it does:**', @@ -96,17 +96,18 @@ jobs: details: [ 'Review apps are automatically destroyed when:', '1. The pull request is closed', - '2. The `/destroy` command is used', + '2. The `/delete-review-app` 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/)', + 'For additional assistance, ', + '1. Check the [Control Plane Flow documentation](https://www.shakacode.com/control-plane-flow/docs/)', '2. Contact the infrastructure team', - '3. Open an issue in this repository' + '3. [Open an issue in this repository](https://github.com/shakacode/control-plane-flow/issues/new)', + '4. [Contact ShakaCode support](mailto:justin@shakacode.com)' ] } }; @@ -148,4 +149,3 @@ jobs: issue_number: prNumber, body: helpText }); - \ No newline at end of file diff --git a/.github/workflows/promote-staging-to-production.yml b/.github/workflows/promote-staging-to-production.yml index 041480671..dc57c7307 100644 --- a/.github/workflows/promote-staging-to-production.yml +++ b/.github/workflows/promote-staging-to-production.yml @@ -9,36 +9,44 @@ on: type: string jobs: + debug: + uses: ./.github/workflows/debug-workflow.yml + with: + debug_enabled: false + promote-to-production: runs-on: ubuntu-latest if: github.event.inputs.confirm_promotion == 'promote' - + env: - APP_NAME: react-webpack-rails-tutorial - CPLN_ORG: ${{ secrets.CPLN_ORG }} - UPSTREAM_TOKEN: ${{ secrets.STAGING_TOKEN }} - + APP_NAME: ${{ vars.PRODUCTION_APP_NAME }} + CPLN_ORG: ${{ vars.CPLN_ORG_PRODUCTION }} + CPLN_TOKEN: ${{ secrets.CPLN_TOKEN_PRODUCTION }} + UPSTREAM_TOKEN: ${{ secrets.CPLN_TOKEN_STAGING }} + steps: - name: Checkout code uses: actions/checkout@v4 - + - name: Setup Environment uses: ./.github/actions/setup-environment - env: - CPLN_TOKEN: ${{ secrets.CPLN_TOKEN }} - + with: + token: ${{ secrets.CPLN_TOKEN_PRODUCTION }} + org: ${{ vars.CPLN_ORG_PRODUCTION }} + - name: Promote Staging to Production id: promote run: | - echo "🚀 Starting promotion from staging to production..." - - if ! cpflow promote-app-from-upstream -a "${APP_NAME}" -t "${UPSTREAM_TOKEN}" --org "${CPLN_ORG}"; then + echo "🚀 Starting promotion from staging to production for app ${APP_NAME}" + + echo "upstream_token is ${UPSTREAM_TOKEN}" + if ! cpflow promote-app-from-upstream -a "${APP_NAME}" -t "${UPSTREAM_TOKEN}" --org "${CPLN_ORG}" --verbose --trace ; then echo "❌ Failed to promote staging to production" exit 1 fi echo "✅ Successfully promoted staging to production" - + - name: Create GitHub Release if: success() env: