cpflow 5.0.0.rc.1 → 5.0.0.rc.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/{lib/github_flow_templates/.github → .github}/actions/cpflow-delete-control-plane-app/action.yml +5 -0
  3. data/{lib/github_flow_templates/.github → .github}/actions/cpflow-detect-release-phase/action.yml +7 -0
  4. data/.github/actions/cpflow-setup-environment/action.yml +161 -0
  5. data/.github/workflows/cpflow-cleanup-stale-review-apps.yml +69 -0
  6. data/.github/workflows/cpflow-delete-review-app.yml +182 -0
  7. data/.github/workflows/cpflow-deploy-review-app.yml +507 -0
  8. data/.github/workflows/cpflow-deploy-staging.yml +168 -0
  9. data/.github/workflows/cpflow-help-command.yml +78 -0
  10. data/.github/workflows/cpflow-promote-staging-to-production.yml +510 -0
  11. data/.github/workflows/cpflow-review-app-help.yml +51 -0
  12. data/.github/workflows/rspec-shared.yml +3 -0
  13. data/.github/workflows/trigger-docs-site.yml +90 -0
  14. data/.rubocop.yml +14 -1
  15. data/CHANGELOG.md +28 -1
  16. data/CONTRIBUTING.md +27 -0
  17. data/Gemfile.lock +2 -2
  18. data/README.md +7 -3
  19. data/cpflow.gemspec +1 -1
  20. data/docs/ai-github-flow-prompt.md +1 -1
  21. data/docs/assets/cpflow-deploying.svg +46 -0
  22. data/docs/ci-automation.md +111 -8
  23. data/docs/commands.md +11 -5
  24. data/docs/thruster.md +149 -0
  25. data/docs/troubleshooting.md +8 -0
  26. data/lib/command/apply_template.rb +6 -2
  27. data/lib/command/base.rb +1 -0
  28. data/lib/command/cleanup_stale_apps.rb +53 -14
  29. data/lib/command/delete.rb +3 -1
  30. data/lib/command/deploy_image.rb +5 -2
  31. data/lib/command/generate.rb +7 -3
  32. data/lib/command/generate_github_actions.rb +21 -9
  33. data/lib/command/generator_helpers.rb +5 -1
  34. data/lib/command/info.rb +3 -1
  35. data/lib/command/run.rb +16 -1
  36. data/lib/core/controlplane.rb +17 -6
  37. data/lib/core/controlplane_api.rb +3 -1
  38. data/lib/core/controlplane_api_direct.rb +50 -27
  39. data/lib/core/doctor_service.rb +2 -2
  40. data/lib/core/github_flow_readiness_service.rb +26 -2
  41. data/lib/core/repo_introspection.rb +41 -3
  42. data/lib/core/shell.rb +3 -1
  43. data/lib/core/terraform_config/policy.rb +1 -1
  44. data/lib/cpflow/version.rb +1 -1
  45. data/lib/cpflow.rb +27 -13
  46. data/lib/generator_templates/templates/rails.yml +4 -0
  47. data/lib/generator_templates_sqlite/templates/rails.yml +4 -0
  48. data/lib/github_flow_templates/.github/cpflow-help.md +30 -1
  49. data/lib/github_flow_templates/.github/workflows/cpflow-cleanup-stale-review-apps.yml +10 -44
  50. data/lib/github_flow_templates/.github/workflows/cpflow-delete-review-app.yml +15 -114
  51. data/lib/github_flow_templates/.github/workflows/cpflow-deploy-review-app.yml +10 -413
  52. data/lib/github_flow_templates/.github/workflows/cpflow-deploy-staging.yml +12 -123
  53. data/lib/github_flow_templates/.github/workflows/cpflow-help-command.yml +10 -33
  54. data/lib/github_flow_templates/.github/workflows/cpflow-promote-staging-to-production.yml +13 -475
  55. data/lib/github_flow_templates/.github/workflows/cpflow-review-app-help.yml +12 -30
  56. data/lib/github_flow_templates/bin/pin-cpflow-github-ref +72 -0
  57. data/lib/github_flow_templates/bin/test-cpflow-github-flow +89 -0
  58. data/rakelib/create_release.rake +4 -4
  59. metadata +26 -17
  60. data/lib/github_flow_templates/.github/actions/cpflow-setup-environment/action.yml +0 -98
  61. /data/{lib/github_flow_templates/.github → .github}/actions/cpflow-build-docker-image/action.yml +0 -0
  62. /data/{lib/github_flow_templates/.github → .github}/actions/cpflow-delete-control-plane-app/delete-app.sh +0 -0
  63. /data/{lib/github_flow_templates/.github → .github}/actions/cpflow-validate-config/action.yml +0 -0
  64. /data/{lib/github_flow_templates/.github → .github}/actions/cpflow-wait-for-health/action.yml +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 789ef352119854bd9daa4adba73e82eb156ac81c830ecb128506df228a83c8d5
4
- data.tar.gz: '039a0591791642b586b0658235c1ce61bd87b79e9a638c8c1c0a7bb7c99438ba'
3
+ metadata.gz: 8841c9270ad57971f444915cadf8c8572c815423a82871c8c03d85195a70764f
4
+ data.tar.gz: 9e480ccf6654b9bdd6229816d32ee5a2961607104c71c3b9ead8e263d10b529d
5
5
  SHA512:
6
- metadata.gz: ef26969b2637b60bed3ab00a88b3d087a700ceef4299b6e9722ab61ef542fdb4a44af2684f50a4fda4e236e81c2192f2bff419c802efed20456401948c0e3fe7
7
- data.tar.gz: 867da23be041326b04c4c793d8e67d14b903a1f17198b70148c408068d31e326fa338f6ac2eedcba7c8c72582cb962799cdc050ad8f2423de56739767850b830
6
+ metadata.gz: 6b83fb9da2665e47b6f5e518ab857883d4607f99f401573b5a4967f5050bdf79bd018ff5411b3f01336a26a06274cec42fe10353d83d28d1f725b7f60557df94
7
+ data.tar.gz: fb52b9cc15d8d89685b5830a67ad7bc95f08e128539c6c002a74fcde72691883a5afd40ddb61b3b7eff3695684f7816a2f2db9ca082085bb720e7a3399a98f7e
@@ -11,12 +11,17 @@ inputs:
11
11
  review_app_prefix:
12
12
  description: Prefix used for review app names
13
13
  required: true
14
+ working_directory:
15
+ description: Directory containing the downstream project's .controlplane/controlplane.yml
16
+ required: false
17
+ default: "."
14
18
 
15
19
  runs:
16
20
  using: composite
17
21
  steps:
18
22
  - name: Delete application
19
23
  shell: bash
24
+ working-directory: ${{ inputs.working_directory }}
20
25
  run: ${{ github.action_path }}/delete-app.sh
21
26
  env:
22
27
  APP_NAME: ${{ inputs.app_name }}
@@ -34,6 +34,13 @@ runs:
34
34
  require "yaml"
35
35
 
36
36
  app_name = ARGV.fetch(0)
37
+
38
+ unless File.file?(".controlplane/controlplane.yml")
39
+ warn "Error: `.controlplane/controlplane.yml` is missing. " \
40
+ "cpflow-detect-release-phase must be invoked from a cpflow-configured project."
41
+ exit 1
42
+ end
43
+
37
44
  data = YAML.safe_load(File.read(".controlplane/controlplane.yml"), aliases: true)
38
45
  apps = data["apps"] || {}
39
46
  app_config = apps[app_name]
@@ -0,0 +1,161 @@
1
+ name: Setup Control Plane Environment
2
+ description: Sets up Ruby, installs the Control Plane CLI and cpflow, and configures a default profile
3
+
4
+ inputs:
5
+ token:
6
+ description: Control Plane token
7
+ required: true
8
+ org:
9
+ description: Control Plane organization
10
+ required: true
11
+ ruby_version:
12
+ description: >-
13
+ Ruby version used for cpflow. When empty (the default), ruby/setup-ruby
14
+ auto-detects from .ruby-version, .tool-versions, mise.toml, or a Gemfile
15
+ ruby directive, then falls back to the action's pinned default.
16
+ required: false
17
+ default: ""
18
+ working_directory:
19
+ description: Directory where ruby/setup-ruby should detect Ruby version files.
20
+ required: false
21
+ default: "."
22
+ # GitHub parses double-brace expression snippets inside action metadata (including
23
+ # `description:`) while loading the composite action, and the `vars` context is not
24
+ # available in that phase. Keep these descriptions in plain prose - reference repo
25
+ # variables by NAME only, never with literal GitHub Actions expression syntax.
26
+ cpln_cli_version:
27
+ description: >-
28
+ @controlplane/cli version. Empty string falls back to the action's pinned default,
29
+ so callers can wire this input to the CPLN_CLI_VERSION repository variable
30
+ unconditionally.
31
+ required: false
32
+ default: ""
33
+ cpflow_version:
34
+ description: >-
35
+ cpflow gem version to install from RubyGems. Empty string installs cpflow from
36
+ the checked-out control-plane-flow repository, so callers can test a GitHub ref
37
+ without publishing a release.
38
+ required: false
39
+ default: ""
40
+
41
+ runs:
42
+ using: composite
43
+ # ruby/setup-ruby tracks Ruby/toolcache updates on a major tag. The privileged
44
+ # reusable workflows pin GitHub-owned actions to immutable SHAs.
45
+ steps:
46
+ - name: Resolve Ruby setup version
47
+ id: ruby-version
48
+ shell: bash
49
+ env:
50
+ INPUT_RUBY_VERSION: ${{ inputs.ruby_version }}
51
+ INPUT_WORKING_DIRECTORY: ${{ inputs.working_directory }}
52
+ run: |
53
+ set -euo pipefail
54
+
55
+ ruby_version="${INPUT_RUBY_VERSION}"
56
+ working_directory="${INPUT_WORKING_DIRECTORY:-.}"
57
+ # Bump when the project's minimum-supported Ruby advances.
58
+ default_ruby_version="3.2"
59
+
60
+ if [[ -z "${ruby_version}" ]]; then
61
+ if [[ -f "${working_directory}/.ruby-version" ]] ||
62
+ { [[ -f "${working_directory}/.tool-versions" ]] && grep -Eq "^[[:space:]]*ruby[[:space:]]+" "${working_directory}/.tool-versions"; } ||
63
+ { [[ -f "${working_directory}/mise.toml" ]] && grep -Eq "^[[:space:]]*ruby[[:space:]]*=" "${working_directory}/mise.toml"; } ||
64
+ { [[ -f "${working_directory}/.mise.toml" ]] && grep -Eq "^[[:space:]]*ruby[[:space:]]*=" "${working_directory}/.mise.toml"; }; then
65
+ : # keep empty; ruby/setup-ruby will auto-detect
66
+ elif [[ -f "${working_directory}/Gemfile" ]] && grep -Eq "^[[:space:]]*ruby[[:space:]]*(\(|file:|['\"])" "${working_directory}/Gemfile"; then
67
+ : # keep empty; ruby/setup-ruby will read Gemfile
68
+ else
69
+ ruby_version="${default_ruby_version}"
70
+ fi
71
+ fi
72
+
73
+ echo "ruby_version=${ruby_version}" >> "$GITHUB_OUTPUT"
74
+
75
+ - name: Set up Ruby
76
+ # ruby/setup-ruby intentionally tracks the v1 tag so Ruby/toolcache updates
77
+ # roll out automatically; Dependabot/Renovate can manage this non-GitHub action.
78
+ uses: ruby/setup-ruby@v1
79
+ with:
80
+ ruby-version: ${{ steps.ruby-version.outputs.ruby_version }}
81
+ working-directory: ${{ inputs.working_directory }}
82
+
83
+ - name: Install Control Plane CLI and cpflow gem
84
+ shell: bash
85
+ env:
86
+ CPLN_CLI_VERSION: ${{ inputs.cpln_cli_version }}
87
+ CPFLOW_VERSION: ${{ inputs.cpflow_version }}
88
+ CPFLOW_SOURCE_DIR: ${{ github.action_path }}/../../..
89
+ run: |
90
+ set -euo pipefail
91
+
92
+ # Bump this default when a new Control Plane CLI release should roll out by default.
93
+ # Override per-repo by setting the `CPLN_CLI_VERSION` repo variable.
94
+ default_cpln_cli_version="3.10.2"
95
+
96
+ CPLN_CLI_VERSION="${CPLN_CLI_VERSION:-${default_cpln_cli_version}}"
97
+
98
+ npm_global_prefix="${HOME}/.npm-global"
99
+ mkdir -p "${npm_global_prefix}"
100
+ echo "${npm_global_prefix}/bin" >> "$GITHUB_PATH"
101
+ export PATH="${npm_global_prefix}/bin:${PATH}"
102
+
103
+ npm install --global --prefix "${npm_global_prefix}" "@controlplane/cli@${CPLN_CLI_VERSION}"
104
+ cpln --version
105
+
106
+ if [[ -n "${CPFLOW_VERSION}" ]]; then
107
+ gem install cpflow -v "${CPFLOW_VERSION}" --no-document
108
+ else
109
+ cpflow_source_dir="$(cd "${CPFLOW_SOURCE_DIR}" && pwd)"
110
+ if [[ ! -f "${cpflow_source_dir}/cpflow.gemspec" ]]; then
111
+ echo "::error::CPFLOW_SOURCE_DIR (${cpflow_source_dir}) does not contain cpflow.gemspec" >&2
112
+ exit 1
113
+ fi
114
+
115
+ cpflow_gem="$(mktemp -t cpflow-XXXXXX.gem)"
116
+ trap 'rm -f "${cpflow_gem}"' EXIT
117
+ (
118
+ cd "${cpflow_source_dir}"
119
+ gem build cpflow.gemspec --output "${cpflow_gem}"
120
+ )
121
+ gem install "${cpflow_gem}" --no-document
122
+ fi
123
+
124
+ cpflow --version
125
+
126
+ - name: Setup Control Plane profile and registry login
127
+ shell: bash
128
+ env:
129
+ # Pass the token via CPLN_TOKEN so cpln picks it up from the environment
130
+ # rather than `--token`, which would leak it into /proc/<pid>/cmdline and ps output.
131
+ CPLN_TOKEN: ${{ inputs.token }}
132
+ ORG: ${{ inputs.org }}
133
+ run: |
134
+ set -euo pipefail
135
+
136
+ if [[ -z "$CPLN_TOKEN" ]]; then
137
+ echo "Error: Control Plane token not provided" >&2
138
+ exit 1
139
+ fi
140
+
141
+ if [[ -z "$ORG" ]]; then
142
+ echo "Error: Control Plane organization not provided" >&2
143
+ exit 1
144
+ fi
145
+
146
+ # `cpln profile update` lists `create` as an alias (cpln profile --help) and is
147
+ # idempotent: it creates the profile if missing and updates it otherwise. Calling
148
+ # update directly avoids parsing the CLI's "already exists" English error text,
149
+ # which would silently swallow a real failure if the wording ever changed.
150
+ cpln profile update default --org "$ORG"
151
+ cpln image docker-login --org "$ORG"
152
+
153
+ # Keep the token available to later cpflow/cpln steps without passing it
154
+ # on the command line. GitHub masks secret values in logs, and the env file
155
+ # itself is not echoed.
156
+ delim="CPLN_TOKEN_DELIM_$(openssl rand -hex 8)"
157
+ {
158
+ echo "CPLN_TOKEN<<${delim}"
159
+ echo "${CPLN_TOKEN}"
160
+ echo "${delim}"
161
+ } >> "$GITHUB_ENV"
@@ -0,0 +1,69 @@
1
+ name: Cleanup Stale Review Apps
2
+
3
+ on:
4
+ workflow_call:
5
+ inputs:
6
+ control_plane_flow_ref:
7
+ description: Git ref used to load shared cpflow composite actions.
8
+ required: false
9
+ type: string
10
+ default: main
11
+
12
+ permissions:
13
+ contents: read
14
+
15
+ concurrency:
16
+ # Single global group: only one cleanup sweep at a time. Independent of review-app
17
+ # deploy/delete groups (different keys), so cleanup will not block per-PR work.
18
+ group: cpflow-cleanup-stale-review-apps
19
+ # A cancelled `cpflow cleanup-stale-apps` can leave half-deleted review apps; let
20
+ # the in-flight run finish before the next scheduled tick begins.
21
+ cancel-in-progress: false
22
+
23
+ jobs:
24
+ cleanup:
25
+ runs-on: ubuntu-latest
26
+ timeout-minutes: 30
27
+ steps:
28
+ - name: Checkout control-plane-flow actions
29
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
30
+ with:
31
+ repository: shakacode/control-plane-flow
32
+ ref: ${{ inputs.control_plane_flow_ref }}
33
+ path: .cpflow
34
+ persist-credentials: false
35
+
36
+ - name: Validate required secrets and variables
37
+ uses: ./.cpflow/.github/actions/cpflow-validate-config
38
+ env:
39
+ CPLN_TOKEN_STAGING: ${{ secrets.CPLN_TOKEN_STAGING }}
40
+ CPLN_ORG_STAGING: ${{ vars.CPLN_ORG_STAGING }}
41
+ REVIEW_APP_PREFIX: ${{ vars.REVIEW_APP_PREFIX }}
42
+ with:
43
+ required: |
44
+ secret:CPLN_TOKEN_STAGING
45
+ variable:CPLN_ORG_STAGING
46
+ variable:REVIEW_APP_PREFIX
47
+
48
+ - name: Setup environment
49
+ uses: ./.cpflow/.github/actions/cpflow-setup-environment
50
+ with:
51
+ token: ${{ secrets.CPLN_TOKEN_STAGING }}
52
+ org: ${{ vars.CPLN_ORG_STAGING }}
53
+ working_directory: .cpflow
54
+ cpln_cli_version: ${{ vars.CPLN_CLI_VERSION }}
55
+ cpflow_version: ${{ vars.CPFLOW_VERSION }}
56
+
57
+ - name: Checkout caller repository
58
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
59
+ with:
60
+ persist-credentials: false
61
+
62
+ - name: Remove stale review apps
63
+ env:
64
+ REVIEW_APP_PREFIX: ${{ vars.REVIEW_APP_PREFIX }}
65
+ CPLN_ORG_STAGING: ${{ vars.CPLN_ORG_STAGING }}
66
+ shell: bash
67
+ run: |
68
+ set -euo pipefail
69
+ cpflow cleanup-stale-apps -a "${REVIEW_APP_PREFIX}" --org "${CPLN_ORG_STAGING}" --yes
@@ -0,0 +1,182 @@
1
+ name: Delete Review App
2
+
3
+ on:
4
+ workflow_call:
5
+ inputs:
6
+ control_plane_flow_ref:
7
+ description: Git ref used to load shared cpflow composite actions.
8
+ required: false
9
+ type: string
10
+ default: main
11
+
12
+ permissions:
13
+ contents: read
14
+ issues: write
15
+ pull-requests: write
16
+
17
+ concurrency:
18
+ group: cpflow-delete-review-app-${{ github.event.pull_request.number || github.event.issue.number || github.event.inputs.pr_number }}
19
+ # Deletions must not cancel each other mid-flight — a cancelled `cpln` delete can leave
20
+ # partial state behind. Let the in-progress deletion finish before the next run starts.
21
+ cancel-in-progress: false
22
+
23
+ env:
24
+ APP_NAME: ${{ vars.REVIEW_APP_PREFIX }}-${{ github.event.pull_request.number || github.event.issue.number || github.event.inputs.pr_number }}
25
+ CPLN_ORG: ${{ vars.CPLN_ORG_STAGING }}
26
+ PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number || github.event.inputs.pr_number }}
27
+
28
+ jobs:
29
+ delete-review-app:
30
+ if: |
31
+ (github.event_name == 'issue_comment' &&
32
+ github.event.issue.pull_request &&
33
+ contains(fromJson('["+review-app-delete","+review-app-delete\n","+review-app-delete\r\n"]'), github.event.comment.body) &&
34
+ contains(fromJson('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)) ||
35
+ (github.event_name == 'pull_request_target' && github.event.action == 'closed') ||
36
+ github.event_name == 'workflow_dispatch'
37
+ runs-on: ubuntu-latest
38
+ timeout-minutes: 15
39
+
40
+ steps:
41
+ - name: React to delete command
42
+ if: github.event_name == 'issue_comment'
43
+ continue-on-error: true
44
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd
45
+ with:
46
+ script: |
47
+ try {
48
+ await github.rest.reactions.createForIssueComment({
49
+ owner: context.repo.owner,
50
+ repo: context.repo.repo,
51
+ comment_id: context.payload.comment.id,
52
+ content: "eyes"
53
+ });
54
+ } catch (error) {
55
+ if (error.status === 422) {
56
+ core.info("Delete command reaction already exists.");
57
+ } else {
58
+ throw error;
59
+ }
60
+ }
61
+
62
+ - name: Checkout control-plane-flow actions
63
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
64
+ with:
65
+ repository: shakacode/control-plane-flow
66
+ ref: ${{ inputs.control_plane_flow_ref }}
67
+ path: .cpflow
68
+ persist-credentials: false
69
+
70
+ - name: Validate required secrets and variables
71
+ id: config
72
+ uses: ./.cpflow/.github/actions/cpflow-validate-config
73
+ env:
74
+ CPLN_TOKEN_STAGING: ${{ secrets.CPLN_TOKEN_STAGING }}
75
+ CPLN_ORG_STAGING: ${{ vars.CPLN_ORG_STAGING }}
76
+ REVIEW_APP_PREFIX: ${{ vars.REVIEW_APP_PREFIX }}
77
+ with:
78
+ required: |
79
+ secret:CPLN_TOKEN_STAGING
80
+ variable:CPLN_ORG_STAGING
81
+ variable:REVIEW_APP_PREFIX
82
+ pull_request_friendly: "true"
83
+
84
+ - name: Checkout repository
85
+ if: steps.config.outputs.ready == 'true'
86
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
87
+ with:
88
+ path: app
89
+ persist-credentials: false
90
+
91
+ - name: Setup environment
92
+ if: steps.config.outputs.ready == 'true'
93
+ uses: ./.cpflow/.github/actions/cpflow-setup-environment
94
+ with:
95
+ token: ${{ secrets.CPLN_TOKEN_STAGING }}
96
+ org: ${{ vars.CPLN_ORG_STAGING }}
97
+ working_directory: app
98
+ cpln_cli_version: ${{ vars.CPLN_CLI_VERSION }}
99
+ cpflow_version: ${{ vars.CPFLOW_VERSION }}
100
+
101
+ - name: Set workflow links
102
+ if: steps.config.outputs.ready == 'true'
103
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd
104
+ with:
105
+ script: |
106
+ const workflowUrl = `${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
107
+ core.exportVariable("WORKFLOW_URL", workflowUrl);
108
+ core.exportVariable(
109
+ "CONSOLE_URL",
110
+ `https://console.cpln.io/console/org/${process.env.CPLN_ORG}/-info`
111
+ );
112
+
113
+ - name: Create initial PR comment
114
+ if: steps.config.outputs.ready == 'true'
115
+ id: create-comment
116
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd
117
+ with:
118
+ script: |
119
+ const body = [
120
+ "## 🗑️ Deleting review app...",
121
+ "",
122
+ `_Removing the review app for PR #${process.env.PR_NUMBER}_`
123
+ ].join("\n");
124
+
125
+ const comment = await github.rest.issues.createComment({
126
+ owner: context.repo.owner,
127
+ repo: context.repo.repo,
128
+ issue_number: Number(process.env.PR_NUMBER),
129
+ body
130
+ });
131
+ core.setOutput("comment-id", comment.data.id);
132
+
133
+ - name: Delete review app
134
+ if: steps.config.outputs.ready == 'true'
135
+ uses: ./.cpflow/.github/actions/cpflow-delete-control-plane-app
136
+ with:
137
+ app_name: ${{ env.APP_NAME }}
138
+ cpln_org: ${{ vars.CPLN_ORG_STAGING }}
139
+ review_app_prefix: ${{ vars.REVIEW_APP_PREFIX }}
140
+ working_directory: app
141
+
142
+ # Finalizer still runs after delete failures, but only after config validation
143
+ # created the initial PR comment and workflow link env vars it updates.
144
+ - name: Finalize delete status
145
+ if: always() && steps.config.outputs.ready == 'true'
146
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd
147
+ env:
148
+ COMMENT_ID: ${{ steps.create-comment.outputs.comment-id }}
149
+ JOB_STATUS: ${{ job.status }}
150
+ with:
151
+ script: |
152
+ const commentId = Number(process.env.COMMENT_ID);
153
+ const success = process.env.JOB_STATUS === "success";
154
+ const body = success
155
+ ? [
156
+ "## ✅ Review App Deleted",
157
+ "",
158
+ `_Review app for PR #${process.env.PR_NUMBER} is deleted_`,
159
+ "",
160
+ `🎮 [Control Plane Console](${process.env.CONSOLE_URL})`,
161
+ `📋 [View Workflow Logs](${process.env.WORKFLOW_URL})`
162
+ ].join("\n")
163
+ : [
164
+ "## ❌ Failed to Delete Review App",
165
+ "",
166
+ `_Failed to delete review app for PR #${process.env.PR_NUMBER}_`,
167
+ "",
168
+ `🎮 [Control Plane Console](${process.env.CONSOLE_URL})`,
169
+ `📋 [View Workflow Logs](${process.env.WORKFLOW_URL})`
170
+ ].join("\n");
171
+
172
+ if (!Number.isFinite(commentId) || commentId <= 0) {
173
+ core.warning("Skipping delete status comment update because no comment id was created.");
174
+ return;
175
+ }
176
+
177
+ await github.rest.issues.updateComment({
178
+ owner: context.repo.owner,
179
+ repo: context.repo.repo,
180
+ comment_id: commentId,
181
+ body
182
+ });