cpflow 4.1.1 → 5.0.0.rc.0

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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/commands/update-changelog.md +367 -0
  3. data/.github/workflows/claude-code-review.yml +44 -0
  4. data/.github/workflows/claude.yml +55 -0
  5. data/.gitignore +2 -0
  6. data/.overcommit.yml +43 -3
  7. data/.rubocop.yml +3 -3
  8. data/CHANGELOG.md +39 -3
  9. data/CONTRIBUTING.md +6 -0
  10. data/Gemfile +8 -7
  11. data/Gemfile.lock +93 -73
  12. data/README.md +53 -22
  13. data/cpflow.gemspec +5 -5
  14. data/docs/ai-github-flow-prompt.md +61 -0
  15. data/docs/ci-automation.md +335 -0
  16. data/docs/commands.md +70 -5
  17. data/docs/releasing.md +153 -0
  18. data/lib/command/ai_github_flow_prompt.rb +47 -0
  19. data/lib/command/base.rb +14 -0
  20. data/lib/command/cleanup_images.rb +1 -1
  21. data/lib/command/cleanup_stale_apps.rb +1 -1
  22. data/lib/command/copy_image_from_upstream.rb +14 -3
  23. data/lib/command/exists.rb +13 -2
  24. data/lib/command/generate.rb +153 -4
  25. data/lib/command/generate_github_actions.rb +170 -0
  26. data/lib/command/generator_helpers.rb +31 -0
  27. data/lib/command/github_flow_readiness.rb +37 -0
  28. data/lib/command/ps_wait.rb +5 -1
  29. data/lib/command/run.rb +4 -21
  30. data/lib/command/terraform/generate.rb +1 -0
  31. data/lib/command/version.rb +1 -0
  32. data/lib/constants/exit_code.rb +1 -0
  33. data/lib/core/config.rb +1 -1
  34. data/lib/core/controlplane.rb +13 -10
  35. data/lib/core/controlplane_api_direct.rb +25 -3
  36. data/lib/core/github_flow_readiness/checks.rb +143 -0
  37. data/lib/core/github_flow_readiness_service.rb +453 -0
  38. data/lib/core/repo_introspection.rb +118 -0
  39. data/lib/core/terraform_config/dsl.rb +1 -1
  40. data/lib/core/terraform_config/local_variable.rb +1 -1
  41. data/lib/cpflow/version.rb +1 -1
  42. data/lib/cpflow.rb +66 -3
  43. data/lib/generator_templates/Dockerfile +59 -3
  44. data/lib/generator_templates/controlplane.yml +27 -39
  45. data/lib/generator_templates/entrypoint.sh +1 -1
  46. data/lib/generator_templates/release_script.sh +23 -0
  47. data/lib/generator_templates/templates/app.yml +5 -8
  48. data/lib/generator_templates/templates/rails.yml +2 -11
  49. data/lib/generator_templates_sqlite/controlplane.yml +46 -0
  50. data/lib/generator_templates_sqlite/release_script.sh +25 -0
  51. data/lib/generator_templates_sqlite/templates/app.yml +15 -0
  52. data/lib/generator_templates_sqlite/templates/db.yml +6 -0
  53. data/lib/generator_templates_sqlite/templates/rails.yml +32 -0
  54. data/lib/generator_templates_sqlite/templates/storage.yml +6 -0
  55. data/lib/github_flow_templates/.github/actions/cpflow-build-docker-image/action.yml +131 -0
  56. data/lib/github_flow_templates/.github/actions/cpflow-delete-control-plane-app/action.yml +24 -0
  57. data/lib/github_flow_templates/.github/actions/cpflow-delete-control-plane-app/delete-app.sh +50 -0
  58. data/lib/github_flow_templates/.github/actions/cpflow-detect-release-phase/action.yml +62 -0
  59. data/lib/github_flow_templates/.github/actions/cpflow-setup-environment/action.yml +98 -0
  60. data/lib/github_flow_templates/.github/actions/cpflow-validate-config/action.yml +85 -0
  61. data/lib/github_flow_templates/.github/actions/cpflow-wait-for-health/action.yml +92 -0
  62. data/lib/github_flow_templates/.github/cpflow-help.md +47 -0
  63. data/lib/github_flow_templates/.github/workflows/cpflow-cleanup-stale-review-apps.yml +56 -0
  64. data/lib/github_flow_templates/.github/workflows/cpflow-delete-review-app.yml +142 -0
  65. data/lib/github_flow_templates/.github/workflows/cpflow-deploy-review-app.yml +445 -0
  66. data/lib/github_flow_templates/.github/workflows/cpflow-deploy-staging.yml +140 -0
  67. data/lib/github_flow_templates/.github/workflows/cpflow-help-command.yml +53 -0
  68. data/lib/github_flow_templates/.github/workflows/cpflow-promote-staging-to-production.yml +490 -0
  69. data/lib/github_flow_templates/.github/workflows/cpflow-review-app-help.yml +46 -0
  70. data/rakelib/create_release.rake +662 -37
  71. data/script/check_command_docs +4 -2
  72. data/script/check_cpln_links +25 -11
  73. data/script/precommit/check_command_docs +22 -0
  74. data/script/precommit/check_cpln_links +21 -0
  75. data/script/precommit/check_trailing_newlines +68 -0
  76. data/script/precommit/get_changed_files +49 -0
  77. data/script/precommit/ruby_autofix +52 -0
  78. data/script/precommit/ruby_lint +33 -0
  79. metadata +56 -15
  80. /data/docs/{migrating.md → migrating-heroku-to-control-plane.md} +0 -0
@@ -0,0 +1,98 @@
1
+ name: Setup Control Plane Environment
2
+ description: Sets up Ruby, installs the Control Plane CLI and cpflow gem, 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 auto-detects
14
+ from .ruby-version, .tool-versions, or the Gemfile.
15
+ required: false
16
+ default: ""
17
+ cpln_cli_version:
18
+ description: >-
19
+ @controlplane/cli version. Empty string falls back to the action's pinned default
20
+ so callers can pass `${{ vars.CPLN_CLI_VERSION }}` unconditionally.
21
+ required: false
22
+ default: ""
23
+ cpflow_version:
24
+ description: >-
25
+ cpflow gem version. Empty string falls back to the action's pinned default
26
+ so callers can pass `${{ vars.CPFLOW_VERSION }}` unconditionally.
27
+ required: false
28
+ default: ""
29
+
30
+ runs:
31
+ using: composite
32
+ # Third-party actions are pinned to floating major tags (`@v4`, `@v1`, `@v7`) rather than
33
+ # immutable SHAs. SHA pinning is GitHub's stronger security recommendation, but for
34
+ # generated templates that ship into many downstream repositories floating tags are
35
+ # easier for users to keep current and Dependabot/Renovate already cover the SHA-pinning
36
+ # workflow for repositories that opt in. Repositories with stricter supply-chain
37
+ # requirements should replace each `uses: actions/...@vN` with the corresponding
38
+ # immutable commit SHA.
39
+ steps:
40
+ - name: Set up Ruby
41
+ uses: ruby/setup-ruby@v1
42
+ with:
43
+ ruby-version: ${{ inputs.ruby_version }}
44
+
45
+ - name: Install Control Plane CLI and cpflow gem
46
+ shell: bash
47
+ env:
48
+ CPLN_CLI_VERSION: ${{ inputs.cpln_cli_version }}
49
+ CPFLOW_VERSION: ${{ inputs.cpflow_version }}
50
+ run: |
51
+ set -euo pipefail
52
+
53
+ # Bump these defaults when a new release lands that you want to roll out by default.
54
+ # Override per-repo by setting `CPLN_CLI_VERSION` / `CPFLOW_VERSION` repo variables;
55
+ # an empty input falls back to the action's pinned default below.
56
+ default_cpln_cli_version="3.3.1"
57
+ default_cpflow_version="__CPFLOW_VERSION__"
58
+
59
+ CPLN_CLI_VERSION="${CPLN_CLI_VERSION:-${default_cpln_cli_version}}"
60
+ CPFLOW_VERSION="${CPFLOW_VERSION:-${default_cpflow_version}}"
61
+
62
+ npm_global_prefix="${HOME}/.npm-global"
63
+ mkdir -p "${npm_global_prefix}"
64
+ echo "${npm_global_prefix}/bin" >> "$GITHUB_PATH"
65
+ export PATH="${npm_global_prefix}/bin:${PATH}"
66
+
67
+ npm install --global --prefix "${npm_global_prefix}" "@controlplane/cli@${CPLN_CLI_VERSION}"
68
+ cpln --version
69
+
70
+ gem install cpflow -v "${CPFLOW_VERSION}" --no-document
71
+ cpflow --version
72
+
73
+ - name: Setup Control Plane profile and registry login
74
+ shell: bash
75
+ env:
76
+ # Pass the token via CPLN_TOKEN so cpln picks it up from the environment
77
+ # rather than `--token`, which would leak it into /proc/<pid>/cmdline and ps output.
78
+ CPLN_TOKEN: ${{ inputs.token }}
79
+ ORG: ${{ inputs.org }}
80
+ run: |
81
+ set -euo pipefail
82
+
83
+ if [[ -z "$CPLN_TOKEN" ]]; then
84
+ echo "Error: Control Plane token not provided" >&2
85
+ exit 1
86
+ fi
87
+
88
+ if [[ -z "$ORG" ]]; then
89
+ echo "Error: Control Plane organization not provided" >&2
90
+ exit 1
91
+ fi
92
+
93
+ # `cpln profile update` lists `create` as an alias (cpln profile --help) and is
94
+ # idempotent: it creates the profile if missing and updates it otherwise. Calling
95
+ # update directly avoids parsing the CLI's "already exists" English error text,
96
+ # which would silently swallow a real failure if the wording ever changed.
97
+ cpln profile update default --org "$ORG"
98
+ cpln image docker-login --org "$ORG"
@@ -0,0 +1,85 @@
1
+ name: Validate cpflow GitHub configuration
2
+ description: >-
3
+ Validates that required secrets and repository variables are set before a workflow
4
+ proceeds. Pass each value via `env:` with the same NAME as the secret or variable,
5
+ then list the required entries in `required` as `type:NAME` pairs (type is `secret`
6
+ or `variable`). When `pull_request_friendly: true` and the current event is a
7
+ pull request event, missing config writes a step summary and exits 0 with
8
+ `ready=false` instead of failing the job.
9
+
10
+ inputs:
11
+ required:
12
+ description: |
13
+ Newline-separated `type:NAME` pairs. Type is `secret` or `variable`. The
14
+ caller MUST export the matching values via `env:` using the same NAME.
15
+ required: true
16
+ pull_request_friendly:
17
+ description: When "true" and event is pull_request/pull_request_target, write summary and exit 0 with ready=false.
18
+ required: false
19
+ default: "false"
20
+
21
+ outputs:
22
+ ready:
23
+ description: '"true" when all values are set, "false" when missing in PR-friendly mode.'
24
+ value: ${{ steps.check.outputs.ready }}
25
+
26
+ runs:
27
+ using: composite
28
+ steps:
29
+ - name: Check required secrets and variables
30
+ id: check
31
+ shell: bash
32
+ env:
33
+ CPFLOW_REQUIRED: ${{ inputs.required }}
34
+ CPFLOW_PR_FRIENDLY: ${{ inputs.pull_request_friendly }}
35
+ CPFLOW_EVENT_NAME: ${{ github.event_name }}
36
+ run: |
37
+ set -euo pipefail
38
+
39
+ missing=()
40
+ while IFS= read -r entry; do
41
+ entry="${entry%$'\r'}"
42
+ entry="${entry## }"
43
+ entry="${entry%% }"
44
+ [[ -z "${entry}" ]] && continue
45
+
46
+ type="${entry%%:*}"
47
+ name="${entry#*:}"
48
+
49
+ # Reject names that are not plain SHELL_VAR identifiers before doing the
50
+ # indirect lookup below. Without this guard, ${!name} would expand whatever
51
+ # bash nameref/transformation a hand-edited generated workflow snuck in
52
+ # (e.g. `BASH_FUNC_foo%%`). Callers today are the generated templates, but
53
+ # the generated file lives in the user's repo and can be hand-edited.
54
+ if [[ ! "${name}" =~ ^[A-Z_][A-Z0-9_]*$ ]]; then
55
+ echo "Invalid config entry name: ${name}" >&2
56
+ exit 1
57
+ fi
58
+
59
+ # Indirect bash lookup: reads the env var named by ${name} (e.g. CPLN_TOKEN_STAGING)
60
+ # so the value never has to round-trip through workflow logs.
61
+ if [[ -z "${!name:-}" ]]; then
62
+ missing+=("${type}:${name}")
63
+ fi
64
+ done <<< "${CPFLOW_REQUIRED}"
65
+
66
+ if [[ ${#missing[@]} -eq 0 ]]; then
67
+ echo "ready=true" >> "$GITHUB_OUTPUT"
68
+ exit 0
69
+ fi
70
+
71
+ if [[ "${CPFLOW_PR_FRIENDLY}" == "true" && ( "${CPFLOW_EVENT_NAME}" == "pull_request" || "${CPFLOW_EVENT_NAME}" == "pull_request_target" ) ]]; then
72
+ echo "ready=false" >> "$GITHUB_OUTPUT"
73
+ {
74
+ echo "Control Plane review app automation is not configured yet."
75
+ echo
76
+ echo "Missing required GitHub configuration:"
77
+ printf -- '- `%s`\n' "${missing[@]}"
78
+ echo
79
+ echo "Pushes to this pull request will skip review app deploys until the repository is configured."
80
+ } >> "$GITHUB_STEP_SUMMARY"
81
+ exit 0
82
+ fi
83
+
84
+ printf 'Missing required GitHub configuration:\n- %s\n' "${missing[@]}" >&2
85
+ exit 1
@@ -0,0 +1,92 @@
1
+ name: Wait for Control Plane workload health
2
+ description: >-
3
+ Polls the workload's status endpoint with curl and exits success when the
4
+ HTTP response status is in the accepted list. Fails non-zero (and reports
5
+ `healthy=false`) once retries are exhausted.
6
+
7
+ inputs:
8
+ workload_name:
9
+ description: Workload to query (e.g. `rails`).
10
+ required: true
11
+ app_name:
12
+ description: GVC / Control Plane app name the workload belongs to.
13
+ required: true
14
+ org:
15
+ description: Control Plane organization.
16
+ required: true
17
+ max_retries:
18
+ description: Number of attempts before giving up.
19
+ required: false
20
+ default: "24"
21
+ interval_seconds:
22
+ description: Seconds to sleep between attempts.
23
+ required: false
24
+ default: "15"
25
+ accepted_statuses:
26
+ description: >-
27
+ Space-separated list of HTTP status codes considered healthy. The default
28
+ `200 301 302` accepts redirects because curl is invoked without `-L`, so a
29
+ root path that auth-redirects looks like a redirect, not a failure.
30
+ required: false
31
+ default: "200 301 302"
32
+ curl_max_time:
33
+ description: Per-request curl timeout, seconds.
34
+ required: false
35
+ default: "10"
36
+
37
+ outputs:
38
+ healthy:
39
+ description: '"true" once a healthy response was observed; "false" otherwise.'
40
+ value: ${{ steps.poll.outputs.healthy }}
41
+
42
+ runs:
43
+ using: composite
44
+ steps:
45
+ - name: Poll workload endpoint
46
+ id: poll
47
+ shell: bash
48
+ env:
49
+ CPFLOW_WORKLOAD_NAME: ${{ inputs.workload_name }}
50
+ CPFLOW_APP_NAME: ${{ inputs.app_name }}
51
+ CPFLOW_ORG: ${{ inputs.org }}
52
+ CPFLOW_MAX_RETRIES: ${{ inputs.max_retries }}
53
+ CPFLOW_INTERVAL_SECONDS: ${{ inputs.interval_seconds }}
54
+ CPFLOW_ACCEPTED_STATUSES: ${{ inputs.accepted_statuses }}
55
+ CPFLOW_CURL_MAX_TIME: ${{ inputs.curl_max_time }}
56
+ run: |
57
+ set -euo pipefail
58
+
59
+ read -r -a accepted_statuses <<< "${CPFLOW_ACCEPTED_STATUSES}"
60
+
61
+ for attempt in $(seq 1 "${CPFLOW_MAX_RETRIES}"); do
62
+ echo "Health check attempt ${attempt}/${CPFLOW_MAX_RETRIES}"
63
+
64
+ if ! workload_json="$(cpln workload get "${CPFLOW_WORKLOAD_NAME}" --gvc "${CPFLOW_APP_NAME}" --org "${CPFLOW_ORG}" -o json 2>&1)"; then
65
+ echo "::error::Workload '${CPFLOW_WORKLOAD_NAME}' not found in GVC '${CPFLOW_APP_NAME}'. Set PRIMARY_WORKLOAD to the correct workload name." >&2
66
+ printf '%s\n' "${workload_json}" >&2
67
+ echo "healthy=false" >> "$GITHUB_OUTPUT"
68
+ exit 1
69
+ fi
70
+
71
+ endpoint="$(echo "${workload_json}" | jq -r '.status.endpoint // empty')"
72
+ if [[ -n "${endpoint}" ]]; then
73
+ http_status="$(curl -s -o /dev/null -w '%{http_code}' --max-time "${CPFLOW_CURL_MAX_TIME}" "${endpoint}" 2>/dev/null || echo 000)"
74
+ echo "Endpoint: ${endpoint}, HTTP status: ${http_status}"
75
+
76
+ for accepted in "${accepted_statuses[@]}"; do
77
+ if [[ "${http_status}" == "${accepted}" ]]; then
78
+ echo "healthy=true" >> "$GITHUB_OUTPUT"
79
+ exit 0
80
+ fi
81
+ done
82
+ else
83
+ echo "Workload '${CPFLOW_WORKLOAD_NAME}' has no endpoint yet; waiting for one to be assigned."
84
+ fi
85
+
86
+ if [[ "${attempt}" -lt "${CPFLOW_MAX_RETRIES}" ]]; then
87
+ sleep "${CPFLOW_INTERVAL_SECONDS}"
88
+ fi
89
+ done
90
+
91
+ echo "healthy=false" >> "$GITHUB_OUTPUT"
92
+ exit 1
@@ -0,0 +1,47 @@
1
+ # Control Plane GitHub Flow
2
+
3
+ ## PR commands
4
+
5
+ `/deploy-review-app`
6
+ - Creates the review app if it does not exist
7
+ - Builds the PR commit image
8
+ - Deploys the image and comments with the review URL
9
+ - Comment body must be exactly `/deploy-review-app` — no surrounding text, trailing whitespace, or trailing newline. The trigger uses an exact-equality match, so a comment like `please /deploy-review-app now` or `/deploy-review-app ` (with a trailing space) silently no-ops.
10
+
11
+ `/delete-review-app`
12
+ - Deletes the review app when the PR is done
13
+ - This also runs automatically when the PR closes
14
+ - Same exact-match rule as `/deploy-review-app`: the comment body must be exactly `/delete-review-app`.
15
+
16
+ ## Repository secrets
17
+
18
+ | Name | Required | Notes |
19
+ | --- | --- | --- |
20
+ | `CPLN_TOKEN_STAGING` | yes | Service-account token scoped to the staging org. |
21
+ | `CPLN_TOKEN_PRODUCTION` | yes (for promote) | Service-account token scoped to the production org. |
22
+ | `DOCKER_BUILD_SSH_KEY` | optional | Private SSH key used when Docker builds fetch private deps via `RUN --mount=type=ssh`. |
23
+
24
+ ## Repository variables
25
+
26
+ | Name | Required | Notes |
27
+ | --- | --- | --- |
28
+ | `CPLN_ORG_STAGING` | yes | Control Plane org for staging and review apps. |
29
+ | `CPLN_ORG_PRODUCTION` | yes (for promote) | Control Plane org for production. |
30
+ | `STAGING_APP_NAME` | yes | App name in `controlplane.yml` used as the staging deploy target. |
31
+ | `PRODUCTION_APP_NAME` | yes (for promote) | App name in `controlplane.yml` used as the production deploy target. |
32
+ | `REVIEW_APP_PREFIX` | yes | Prefix for per-PR review app names (e.g. `review-app`). |
33
+ | `STAGING_APP_BRANCH` | optional | Custom staging branch. Custom branches must also appear in `cpflow-deploy-staging.yml`'s push filter. |
34
+ | `PRIMARY_WORKLOAD` | optional | Workload polled for health and rollback (defaults to `rails`). |
35
+ | `DOCKER_BUILD_EXTRA_ARGS` | optional | Newline-delimited extra docker build tokens (e.g. `--build-arg=FOO=bar`). |
36
+ | `DOCKER_BUILD_SSH_KNOWN_HOSTS` | optional | SSH known_hosts entries when SSH build hosts are not GitHub.com. |
37
+ | `HEALTH_CHECK_ACCEPTED_STATUSES` | optional | Space-separated HTTP statuses considered healthy on promote (default `200 301 302`). |
38
+ | `CPLN_CLI_VERSION` | optional | Pin a specific `@controlplane/cli` version; falls back to the action default when unset. |
39
+ | `CPFLOW_VERSION` | optional | Pin a specific cpflow gem version; falls back to the generated default when unset. |
40
+
41
+ ## Workflow behavior
42
+
43
+ - Review apps are opt-in and created with `/deploy-review-app`
44
+ - New commits redeploy existing review apps automatically
45
+ - Pushes to the staging branch deploy staging automatically
46
+ - Promotion to production is manual via the Actions tab
47
+ - A nightly workflow removes stale review apps
@@ -0,0 +1,56 @@
1
+ name: Cleanup Stale Review Apps
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ schedule:
6
+ - cron: "0 0 * * *"
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ concurrency:
12
+ # Single global group: only one cleanup sweep at a time. Independent of review-app
13
+ # deploy/delete groups (different keys), so cleanup will not block per-PR work.
14
+ group: cpflow-cleanup-stale-review-apps
15
+ # A cancelled `cpflow cleanup-stale-apps` can leave half-deleted review apps; let
16
+ # the in-flight run finish before the next scheduled tick begins.
17
+ cancel-in-progress: false
18
+
19
+ jobs:
20
+ cleanup:
21
+ runs-on: ubuntu-latest
22
+ timeout-minutes: 30
23
+ steps:
24
+ - name: Checkout repository
25
+ uses: actions/checkout@v4
26
+ with:
27
+ persist-credentials: false
28
+
29
+ - name: Validate required secrets and variables
30
+ uses: ./.github/actions/cpflow-validate-config
31
+ env:
32
+ CPLN_TOKEN_STAGING: ${{ secrets.CPLN_TOKEN_STAGING }}
33
+ CPLN_ORG_STAGING: ${{ vars.CPLN_ORG_STAGING }}
34
+ REVIEW_APP_PREFIX: ${{ vars.REVIEW_APP_PREFIX }}
35
+ with:
36
+ required: |
37
+ secret:CPLN_TOKEN_STAGING
38
+ variable:CPLN_ORG_STAGING
39
+ variable:REVIEW_APP_PREFIX
40
+
41
+ - name: Setup environment
42
+ uses: ./.github/actions/cpflow-setup-environment
43
+ with:
44
+ token: ${{ secrets.CPLN_TOKEN_STAGING }}
45
+ org: ${{ vars.CPLN_ORG_STAGING }}
46
+ cpln_cli_version: ${{ vars.CPLN_CLI_VERSION }}
47
+ cpflow_version: ${{ vars.CPFLOW_VERSION }}
48
+
49
+ - name: Remove stale review apps
50
+ env:
51
+ REVIEW_APP_PREFIX: ${{ vars.REVIEW_APP_PREFIX }}
52
+ CPLN_ORG_STAGING: ${{ vars.CPLN_ORG_STAGING }}
53
+ shell: bash
54
+ run: |
55
+ set -euo pipefail
56
+ cpflow cleanup-stale-apps -a "${REVIEW_APP_PREFIX}" --org "${CPLN_ORG_STAGING}" --yes
@@ -0,0 +1,142 @@
1
+ name: Delete Review App
2
+
3
+ on:
4
+ pull_request_target:
5
+ types: [closed]
6
+ issue_comment:
7
+ types: [created]
8
+ workflow_dispatch:
9
+ inputs:
10
+ pr_number:
11
+ description: Pull request number targeted for deletion
12
+ required: true
13
+ type: number
14
+
15
+ permissions:
16
+ contents: read
17
+ issues: write
18
+ pull-requests: write
19
+
20
+ concurrency:
21
+ group: cpflow-delete-review-app-${{ github.event.pull_request.number || github.event.issue.number || github.event.inputs.pr_number }}
22
+ # Deletions must not cancel each other mid-flight — a cancelled `cpln` delete can leave
23
+ # partial state behind. Let the in-progress deletion finish before the next run starts.
24
+ cancel-in-progress: false
25
+
26
+ env:
27
+ APP_NAME: ${{ vars.REVIEW_APP_PREFIX }}-${{ github.event.pull_request.number || github.event.issue.number || github.event.inputs.pr_number }}
28
+ CPLN_ORG: ${{ vars.CPLN_ORG_STAGING }}
29
+ PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number || github.event.inputs.pr_number }}
30
+
31
+ jobs:
32
+ delete-review-app:
33
+ if: |
34
+ (github.event_name == 'issue_comment' &&
35
+ github.event.issue.pull_request &&
36
+ github.event.comment.body == '/delete-review-app' &&
37
+ contains(fromJson('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)) ||
38
+ (github.event_name == 'pull_request_target' && github.event.action == 'closed') ||
39
+ github.event_name == 'workflow_dispatch'
40
+ runs-on: ubuntu-latest
41
+ timeout-minutes: 15
42
+
43
+ steps:
44
+ # pull_request_target is intentional: PR-close events from forks need access
45
+ # to staging secrets so this workflow can delete review apps and update PR
46
+ # comments. This checkout is safe because it does not set `ref:`; GitHub checks
47
+ # out the base branch's trusted workflow code, not the fork head. Do not add
48
+ # `ref: ${{ github.event.pull_request.head.sha }}` here without re-evaluating
49
+ # the trust boundary. All local composite actions below are therefore loaded from
50
+ # trusted base-branch code; keep them that way when changing this workflow.
51
+ - name: Checkout repository
52
+ uses: actions/checkout@v4
53
+ with:
54
+ # Delete only invokes `cpln`/`cpflow`; no git push happens, so drop the
55
+ # GITHUB_TOKEN credential helper to keep the token out of .git/config under
56
+ # `pull_request_target`, which has access to repository secrets.
57
+ persist-credentials: false
58
+
59
+ - name: Validate required secrets and variables
60
+ uses: ./.github/actions/cpflow-validate-config
61
+ env:
62
+ CPLN_TOKEN_STAGING: ${{ secrets.CPLN_TOKEN_STAGING }}
63
+ CPLN_ORG_STAGING: ${{ vars.CPLN_ORG_STAGING }}
64
+ REVIEW_APP_PREFIX: ${{ vars.REVIEW_APP_PREFIX }}
65
+ with:
66
+ required: |
67
+ secret:CPLN_TOKEN_STAGING
68
+ variable:CPLN_ORG_STAGING
69
+ variable:REVIEW_APP_PREFIX
70
+ pull_request_friendly: "true"
71
+
72
+ - name: Setup environment
73
+ uses: ./.github/actions/cpflow-setup-environment
74
+ with:
75
+ token: ${{ secrets.CPLN_TOKEN_STAGING }}
76
+ org: ${{ vars.CPLN_ORG_STAGING }}
77
+ cpln_cli_version: ${{ vars.CPLN_CLI_VERSION }}
78
+ cpflow_version: ${{ vars.CPFLOW_VERSION }}
79
+
80
+ - name: Set workflow links
81
+ uses: actions/github-script@v7
82
+ with:
83
+ script: |
84
+ const workflowUrl = `${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
85
+ core.exportVariable("WORKFLOW_URL", workflowUrl);
86
+ core.exportVariable(
87
+ "CONSOLE_URL",
88
+ `https://console.cpln.io/console/org/${process.env.CPLN_ORG}/-info`
89
+ );
90
+
91
+ - name: Create initial PR comment
92
+ id: create-comment
93
+ uses: actions/github-script@v7
94
+ with:
95
+ script: |
96
+ const comment = await github.rest.issues.createComment({
97
+ owner: context.repo.owner,
98
+ repo: context.repo.repo,
99
+ issue_number: Number(process.env.PR_NUMBER),
100
+ body: "🗑️ Deleting Control Plane review app..."
101
+ });
102
+ core.setOutput("comment-id", comment.data.id);
103
+
104
+ - name: Delete review app
105
+ uses: ./.github/actions/cpflow-delete-control-plane-app
106
+ with:
107
+ app_name: ${{ env.APP_NAME }}
108
+ cpln_org: ${{ vars.CPLN_ORG_STAGING }}
109
+ review_app_prefix: ${{ vars.REVIEW_APP_PREFIX }}
110
+
111
+ - name: Finalize delete status
112
+ if: always()
113
+ uses: actions/github-script@v7
114
+ with:
115
+ script: |
116
+ const commentId = Number("${{ steps.create-comment.outputs.comment-id }}");
117
+ const success = "${{ job.status }}" === "success";
118
+ const body = success
119
+ ? [
120
+ `✅ Review app for PR #${process.env.PR_NUMBER} is deleted`,
121
+ "",
122
+ `[Open organization console](${process.env.CONSOLE_URL})`,
123
+ `[View workflow logs](${process.env.WORKFLOW_URL})`
124
+ ].join("\n")
125
+ : [
126
+ `❌ Failed to delete review app for PR #${process.env.PR_NUMBER}`,
127
+ "",
128
+ `[Open organization console](${process.env.CONSOLE_URL})`,
129
+ `[View workflow logs](${process.env.WORKFLOW_URL})`
130
+ ].join("\n");
131
+
132
+ if (!Number.isFinite(commentId) || commentId <= 0) {
133
+ core.warning("Skipping delete status comment update because no comment id was created.");
134
+ return;
135
+ }
136
+
137
+ await github.rest.issues.updateComment({
138
+ owner: context.repo.owner,
139
+ repo: context.repo.repo,
140
+ comment_id: commentId,
141
+ body
142
+ });