cpflow 4.2.0 → 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.yml +5 -0
- data/.overcommit.yml +43 -3
- data/.rubocop.yml +3 -3
- data/CHANGELOG.md +28 -4
- data/CONTRIBUTING.md +6 -0
- data/Gemfile +8 -7
- data/Gemfile.lock +92 -72
- data/README.md +43 -15
- data/cpflow.gemspec +5 -5
- data/docs/ai-github-flow-prompt.md +61 -0
- data/docs/ci-automation.md +335 -28
- data/docs/commands.md +65 -4
- 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/run.rb +1 -1
- 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/controlplane.rb +9 -7
- data/lib/core/controlplane_api_direct.rb +3 -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 +65 -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 +52 -14
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
name: Build Docker Image
|
|
2
|
+
description: Builds and pushes the app image for a Control Plane workload
|
|
3
|
+
|
|
4
|
+
inputs:
|
|
5
|
+
app_name:
|
|
6
|
+
description: Name of the application
|
|
7
|
+
required: true
|
|
8
|
+
org:
|
|
9
|
+
description: Control Plane organization name
|
|
10
|
+
required: true
|
|
11
|
+
commit:
|
|
12
|
+
description: Commit SHA to tag the image with
|
|
13
|
+
required: true
|
|
14
|
+
pr_number:
|
|
15
|
+
description: Pull request number for status messaging
|
|
16
|
+
required: false
|
|
17
|
+
docker_build_extra_args:
|
|
18
|
+
description: Optional newline-delimited extra docker build tokens. Use key=value forms like --build-arg=FOO=bar.
|
|
19
|
+
required: false
|
|
20
|
+
docker_build_ssh_key:
|
|
21
|
+
description: Optional private SSH key used for Docker builds that fetch private dependencies with RUN --mount=type=ssh
|
|
22
|
+
required: false
|
|
23
|
+
docker_build_ssh_known_hosts:
|
|
24
|
+
description: Optional SSH known_hosts entries used with docker_build_ssh_key. Defaults to pinned GitHub.com host keys.
|
|
25
|
+
required: false
|
|
26
|
+
working_directory:
|
|
27
|
+
description: Directory containing the app .controlplane config and Docker build context
|
|
28
|
+
required: false
|
|
29
|
+
default: "."
|
|
30
|
+
|
|
31
|
+
runs:
|
|
32
|
+
using: composite
|
|
33
|
+
steps:
|
|
34
|
+
# Keep SSH key handling in a dedicated step so DOCKER_BUILD_SSH_KEY is never present
|
|
35
|
+
# in the main build step's environment. ACTIONS_STEP_DEBUG=true dumps env before any
|
|
36
|
+
# command runs, so keeping the key out of env there avoids even admin-triggered exposure.
|
|
37
|
+
- name: Prepare SSH agent for Docker build
|
|
38
|
+
if: ${{ inputs.docker_build_ssh_key != '' }}
|
|
39
|
+
shell: bash
|
|
40
|
+
env:
|
|
41
|
+
# Pass the key via env so the file write is a single printf call rather than a
|
|
42
|
+
# heredoc with a fixed terminator (a heredoc would silently truncate the key if
|
|
43
|
+
# any line of the key value happened to match the terminator). Scope is still
|
|
44
|
+
# this step only — the build step below does not receive DOCKER_BUILD_SSH_KEY.
|
|
45
|
+
DOCKER_BUILD_SSH_KEY: ${{ inputs.docker_build_ssh_key }}
|
|
46
|
+
DOCKER_BUILD_SSH_KNOWN_HOSTS: ${{ inputs.docker_build_ssh_known_hosts }}
|
|
47
|
+
run: |
|
|
48
|
+
set -euo pipefail
|
|
49
|
+
|
|
50
|
+
umask 077
|
|
51
|
+
mkdir -p ~/.ssh
|
|
52
|
+
chmod 700 ~/.ssh
|
|
53
|
+
|
|
54
|
+
if [[ -n "${DOCKER_BUILD_SSH_KNOWN_HOSTS}" ]]; then
|
|
55
|
+
printf '%s\n' "${DOCKER_BUILD_SSH_KNOWN_HOSTS}" > ~/.ssh/known_hosts
|
|
56
|
+
else
|
|
57
|
+
printf '%s\n' \
|
|
58
|
+
'github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl' \
|
|
59
|
+
'github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=' \
|
|
60
|
+
'github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=' \
|
|
61
|
+
> ~/.ssh/known_hosts
|
|
62
|
+
fi
|
|
63
|
+
chmod 600 ~/.ssh/known_hosts
|
|
64
|
+
|
|
65
|
+
printf '%s\n' "${DOCKER_BUILD_SSH_KEY}" > ~/.ssh/cpflow_build_key
|
|
66
|
+
chmod 600 ~/.ssh/cpflow_build_key
|
|
67
|
+
|
|
68
|
+
- name: Build Docker image
|
|
69
|
+
shell: bash
|
|
70
|
+
env:
|
|
71
|
+
APP_NAME: ${{ inputs.app_name }}
|
|
72
|
+
COMMIT_SHA: ${{ inputs.commit }}
|
|
73
|
+
CONTROL_PLANE_ORG: ${{ inputs.org }}
|
|
74
|
+
DOCKER_BUILD_EXTRA_ARGS: ${{ inputs.docker_build_extra_args }}
|
|
75
|
+
PR_NUMBER: ${{ inputs.pr_number }}
|
|
76
|
+
WORKING_DIRECTORY: ${{ inputs.working_directory }}
|
|
77
|
+
run: |
|
|
78
|
+
set -euo pipefail
|
|
79
|
+
|
|
80
|
+
PR_INFO=""
|
|
81
|
+
docker_build_args=()
|
|
82
|
+
ssh_agent_started=false
|
|
83
|
+
build_ssh_prepped=false
|
|
84
|
+
|
|
85
|
+
cleanup_build_ssh() {
|
|
86
|
+
if [[ "${ssh_agent_started}" == "true" ]]; then
|
|
87
|
+
ssh-agent -k >/dev/null || true
|
|
88
|
+
fi
|
|
89
|
+
rm -f "${HOME}/.ssh/cpflow_build_key"
|
|
90
|
+
# Only remove known_hosts if this action's prep step wrote it. On self-hosted
|
|
91
|
+
# or reused runners we must not touch a user-managed file we did not create,
|
|
92
|
+
# so the flag is set inside the same prep-detection branch below.
|
|
93
|
+
if [[ "${build_ssh_prepped}" == "true" ]]; then
|
|
94
|
+
rm -f "${HOME}/.ssh/known_hosts"
|
|
95
|
+
fi
|
|
96
|
+
}
|
|
97
|
+
trap cleanup_build_ssh EXIT
|
|
98
|
+
cd "${WORKING_DIRECTORY}"
|
|
99
|
+
|
|
100
|
+
if [[ -n "${PR_NUMBER}" ]]; then
|
|
101
|
+
PR_INFO=" for PR #${PR_NUMBER}"
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
if [[ -n "${DOCKER_BUILD_EXTRA_ARGS}" ]]; then
|
|
105
|
+
while IFS= read -r arg; do
|
|
106
|
+
arg="${arg%$'\r'}"
|
|
107
|
+
[[ -n "${arg}" ]] || continue
|
|
108
|
+
|
|
109
|
+
if [[ "${arg}" =~ [[:space:]] ]]; then
|
|
110
|
+
echo "docker_build_extra_args entries must be single docker-build tokens. " \
|
|
111
|
+
"Use key=value forms like --build-arg=FOO=bar." >&2
|
|
112
|
+
exit 1
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
docker_build_args+=("${arg}")
|
|
116
|
+
done <<< "${DOCKER_BUILD_EXTRA_ARGS}"
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
if [[ -f "${HOME}/.ssh/cpflow_build_key" ]]; then
|
|
120
|
+
# Mark prep-step ownership so cleanup_build_ssh only removes known_hosts
|
|
121
|
+
# when this action wrote it (see trap above).
|
|
122
|
+
build_ssh_prepped=true
|
|
123
|
+
eval "$(ssh-agent -s)"
|
|
124
|
+
ssh_agent_started=true
|
|
125
|
+
ssh-add "${HOME}/.ssh/cpflow_build_key"
|
|
126
|
+
docker_build_args+=("--ssh=default")
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
echo "🏗️ Building Docker image${PR_INFO} (commit ${COMMIT_SHA})..."
|
|
130
|
+
cpflow build-image -a "${APP_NAME}" --commit="${COMMIT_SHA}" --org="${CONTROL_PLANE_ORG}" "${docker_build_args[@]}"
|
|
131
|
+
echo "✅ Docker image build successful${PR_INFO} (commit ${COMMIT_SHA})"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: Delete Control Plane App
|
|
2
|
+
description: Deletes a Control Plane app and all associated resources
|
|
3
|
+
|
|
4
|
+
inputs:
|
|
5
|
+
app_name:
|
|
6
|
+
description: Name of the application to delete
|
|
7
|
+
required: true
|
|
8
|
+
cpln_org:
|
|
9
|
+
description: Control Plane organization name
|
|
10
|
+
required: true
|
|
11
|
+
review_app_prefix:
|
|
12
|
+
description: Prefix used for review app names
|
|
13
|
+
required: true
|
|
14
|
+
|
|
15
|
+
runs:
|
|
16
|
+
using: composite
|
|
17
|
+
steps:
|
|
18
|
+
- name: Delete application
|
|
19
|
+
shell: bash
|
|
20
|
+
run: ${{ github.action_path }}/delete-app.sh
|
|
21
|
+
env:
|
|
22
|
+
APP_NAME: ${{ inputs.app_name }}
|
|
23
|
+
CPLN_ORG: ${{ inputs.cpln_org }}
|
|
24
|
+
REVIEW_APP_PREFIX: ${{ inputs.review_app_prefix }}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
: "${APP_NAME:?APP_NAME environment variable is required}"
|
|
6
|
+
: "${CPLN_ORG:?CPLN_ORG environment variable is required}"
|
|
7
|
+
: "${REVIEW_APP_PREFIX:?REVIEW_APP_PREFIX environment variable is required}"
|
|
8
|
+
|
|
9
|
+
expected_prefix="${REVIEW_APP_PREFIX}-"
|
|
10
|
+
if [[ "$APP_NAME" != "${expected_prefix}"* ]]; then
|
|
11
|
+
echo "❌ ERROR: refusing to delete an app outside the review app prefix" >&2
|
|
12
|
+
echo "App name: $APP_NAME" >&2
|
|
13
|
+
echo "Expected prefix: ${expected_prefix}" >&2
|
|
14
|
+
exit 1
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
echo "🔍 Checking if application exists: $APP_NAME"
|
|
18
|
+
exists_output=""
|
|
19
|
+
set +e
|
|
20
|
+
exists_output="$(cpflow exists -a "$APP_NAME" --org "$CPLN_ORG" 2>&1)"
|
|
21
|
+
exists_status=$?
|
|
22
|
+
set -e
|
|
23
|
+
|
|
24
|
+
case "$exists_status" in
|
|
25
|
+
0)
|
|
26
|
+
;;
|
|
27
|
+
3)
|
|
28
|
+
if [[ -n "$exists_output" ]]; then
|
|
29
|
+
printf '%s\n' "$exists_output"
|
|
30
|
+
fi
|
|
31
|
+
echo "⚠️ Application does not exist: $APP_NAME"
|
|
32
|
+
exit 0
|
|
33
|
+
;;
|
|
34
|
+
*)
|
|
35
|
+
echo "❌ ERROR: failed to determine whether application exists: $APP_NAME" >&2
|
|
36
|
+
if [[ -n "$exists_output" ]]; then
|
|
37
|
+
printf '%s\n' "$exists_output" >&2
|
|
38
|
+
fi
|
|
39
|
+
exit "$exists_status"
|
|
40
|
+
;;
|
|
41
|
+
esac
|
|
42
|
+
|
|
43
|
+
if [[ -n "$exists_output" ]]; then
|
|
44
|
+
printf '%s\n' "$exists_output"
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
echo "🗑️ Deleting application: $APP_NAME"
|
|
48
|
+
cpflow delete -a "$APP_NAME" --org "$CPLN_ORG" --yes
|
|
49
|
+
|
|
50
|
+
echo "✅ Successfully deleted application: $APP_NAME"
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
name: Detect release phase support
|
|
2
|
+
description: >-
|
|
3
|
+
Inspects .controlplane/controlplane.yml for an app and emits `flag=--run-release-phase`
|
|
4
|
+
when a `release_script:` is configured. Outputs an empty `flag` otherwise.
|
|
5
|
+
|
|
6
|
+
inputs:
|
|
7
|
+
app_name:
|
|
8
|
+
description: cpflow app name to inspect
|
|
9
|
+
required: true
|
|
10
|
+
working_directory:
|
|
11
|
+
description: Directory containing .controlplane/controlplane.yml
|
|
12
|
+
required: false
|
|
13
|
+
default: "."
|
|
14
|
+
|
|
15
|
+
outputs:
|
|
16
|
+
flag:
|
|
17
|
+
description: Either `--run-release-phase` or empty
|
|
18
|
+
value: ${{ steps.detect.outputs.flag }}
|
|
19
|
+
|
|
20
|
+
runs:
|
|
21
|
+
using: composite
|
|
22
|
+
steps:
|
|
23
|
+
- name: Detect release phase support
|
|
24
|
+
id: detect
|
|
25
|
+
shell: bash
|
|
26
|
+
env:
|
|
27
|
+
APP_NAME: ${{ inputs.app_name }}
|
|
28
|
+
WORKING_DIRECTORY: ${{ inputs.working_directory }}
|
|
29
|
+
run: |
|
|
30
|
+
set -euo pipefail
|
|
31
|
+
cd "${WORKING_DIRECTORY}"
|
|
32
|
+
|
|
33
|
+
release_script="$(ruby - "${APP_NAME}" <<'RUBY'
|
|
34
|
+
require "yaml"
|
|
35
|
+
|
|
36
|
+
app_name = ARGV.fetch(0)
|
|
37
|
+
data = YAML.safe_load(File.read(".controlplane/controlplane.yml"), aliases: true)
|
|
38
|
+
apps = data["apps"] || {}
|
|
39
|
+
app_config = apps[app_name]
|
|
40
|
+
|
|
41
|
+
unless app_config
|
|
42
|
+
app_config = apps.find do |name, config|
|
|
43
|
+
config.is_a?(Hash) &&
|
|
44
|
+
config["match_if_app_name_starts_with"] &&
|
|
45
|
+
app_name.start_with?(name)
|
|
46
|
+
end&.last
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
unless app_config.is_a?(Hash)
|
|
50
|
+
warn "Error: app '#{app_name}' is not defined under `apps:` in `.controlplane/controlplane.yml`."
|
|
51
|
+
exit 1
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
puts app_config["release_script"].to_s
|
|
55
|
+
RUBY
|
|
56
|
+
)"
|
|
57
|
+
|
|
58
|
+
if [[ -n "${release_script}" ]]; then
|
|
59
|
+
echo "flag=--run-release-phase" >> "$GITHUB_OUTPUT"
|
|
60
|
+
else
|
|
61
|
+
echo "flag=" >> "$GITHUB_OUTPUT"
|
|
62
|
+
fi
|
|
@@ -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
|