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.
- checksums.yaml +4 -4
- data/.claude/commands/update-changelog.md +367 -0
- data/.github/workflows/claude-code-review.yml +44 -0
- data/.github/workflows/claude.yml +55 -0
- data/.gitignore +2 -0
- data/.overcommit.yml +43 -3
- data/.rubocop.yml +3 -3
- data/CHANGELOG.md +39 -3
- data/CONTRIBUTING.md +6 -0
- data/Gemfile +8 -7
- data/Gemfile.lock +93 -73
- data/README.md +53 -22
- data/cpflow.gemspec +5 -5
- data/docs/ai-github-flow-prompt.md +61 -0
- data/docs/ci-automation.md +335 -0
- data/docs/commands.md +70 -5
- data/docs/releasing.md +153 -0
- data/lib/command/ai_github_flow_prompt.rb +47 -0
- data/lib/command/base.rb +14 -0
- data/lib/command/cleanup_images.rb +1 -1
- data/lib/command/cleanup_stale_apps.rb +1 -1
- data/lib/command/copy_image_from_upstream.rb +14 -3
- data/lib/command/exists.rb +13 -2
- data/lib/command/generate.rb +153 -4
- data/lib/command/generate_github_actions.rb +170 -0
- data/lib/command/generator_helpers.rb +31 -0
- data/lib/command/github_flow_readiness.rb +37 -0
- data/lib/command/ps_wait.rb +5 -1
- data/lib/command/run.rb +4 -21
- data/lib/command/terraform/generate.rb +1 -0
- data/lib/command/version.rb +1 -0
- data/lib/constants/exit_code.rb +1 -0
- data/lib/core/config.rb +1 -1
- data/lib/core/controlplane.rb +13 -10
- data/lib/core/controlplane_api_direct.rb +25 -3
- data/lib/core/github_flow_readiness/checks.rb +143 -0
- data/lib/core/github_flow_readiness_service.rb +453 -0
- data/lib/core/repo_introspection.rb +118 -0
- data/lib/core/terraform_config/dsl.rb +1 -1
- data/lib/core/terraform_config/local_variable.rb +1 -1
- data/lib/cpflow/version.rb +1 -1
- data/lib/cpflow.rb +66 -3
- data/lib/generator_templates/Dockerfile +59 -3
- data/lib/generator_templates/controlplane.yml +27 -39
- data/lib/generator_templates/entrypoint.sh +1 -1
- data/lib/generator_templates/release_script.sh +23 -0
- data/lib/generator_templates/templates/app.yml +5 -8
- data/lib/generator_templates/templates/rails.yml +2 -11
- data/lib/generator_templates_sqlite/controlplane.yml +46 -0
- data/lib/generator_templates_sqlite/release_script.sh +25 -0
- data/lib/generator_templates_sqlite/templates/app.yml +15 -0
- data/lib/generator_templates_sqlite/templates/db.yml +6 -0
- data/lib/generator_templates_sqlite/templates/rails.yml +32 -0
- data/lib/generator_templates_sqlite/templates/storage.yml +6 -0
- data/lib/github_flow_templates/.github/actions/cpflow-build-docker-image/action.yml +131 -0
- data/lib/github_flow_templates/.github/actions/cpflow-delete-control-plane-app/action.yml +24 -0
- data/lib/github_flow_templates/.github/actions/cpflow-delete-control-plane-app/delete-app.sh +50 -0
- data/lib/github_flow_templates/.github/actions/cpflow-detect-release-phase/action.yml +62 -0
- data/lib/github_flow_templates/.github/actions/cpflow-setup-environment/action.yml +98 -0
- data/lib/github_flow_templates/.github/actions/cpflow-validate-config/action.yml +85 -0
- data/lib/github_flow_templates/.github/actions/cpflow-wait-for-health/action.yml +92 -0
- data/lib/github_flow_templates/.github/cpflow-help.md +47 -0
- data/lib/github_flow_templates/.github/workflows/cpflow-cleanup-stale-review-apps.yml +56 -0
- data/lib/github_flow_templates/.github/workflows/cpflow-delete-review-app.yml +142 -0
- data/lib/github_flow_templates/.github/workflows/cpflow-deploy-review-app.yml +445 -0
- data/lib/github_flow_templates/.github/workflows/cpflow-deploy-staging.yml +140 -0
- data/lib/github_flow_templates/.github/workflows/cpflow-help-command.yml +53 -0
- data/lib/github_flow_templates/.github/workflows/cpflow-promote-staging-to-production.yml +490 -0
- data/lib/github_flow_templates/.github/workflows/cpflow-review-app-help.yml +46 -0
- data/rakelib/create_release.rake +662 -37
- data/script/check_command_docs +4 -2
- data/script/check_cpln_links +25 -11
- data/script/precommit/check_command_docs +22 -0
- data/script/precommit/check_cpln_links +21 -0
- data/script/precommit/check_trailing_newlines +68 -0
- data/script/precommit/get_changed_files +49 -0
- data/script/precommit/ruby_autofix +52 -0
- data/script/precommit/ruby_lint +33 -0
- metadata +56 -15
- /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
|
+
});
|