cpflow 6.0.0.rc.0 → 6.0.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/.agents/agent-workflow.yml +16 -38
- data/.agents/bin/README.md +7 -8
- data/.agents/legacy-workflow-policy.yml +11 -0
- data/.agents/shaka.md +20 -0
- data/.agents/trusted-actions.yml +12 -0
- data/.coderabbit.yaml +1 -0
- data/.github/actions/cpflow-build-docker-image/action.yml +153 -22
- data/.github/actions/cpflow-delete-control-plane-app/delete-app.sh +3 -0
- data/.github/actions/cpflow-preview-promotion/action.yml +82 -0
- data/.github/actions/cpflow-preview-promotion/preview-promotion.sh +90 -0
- data/.github/workflows/cpflow-deploy-review-app.yml +13 -1
- data/.github/workflows/cpflow-deploy-staging.yml +12 -1
- data/.github/workflows/cpflow-promote-staging-to-production.yml +128 -9
- data/.github/workflows/rspec.yml +24 -0
- data/AGENTS.md +18 -3
- data/CHANGELOG.md +18 -3
- data/CONTRIBUTING.md +1 -1
- data/Gemfile.lock +1 -1
- data/README.md +5 -2
- data/docs/ai-github-flow-prompt.md +2 -2
- data/docs/ci-automation.md +178 -14
- data/docs/commands.md +26 -13
- data/docs/postgres-review-apps.md +90 -0
- data/docs/postgres.md +233 -375
- data/docs/secrets-and-env-values.md +42 -0
- data/docs/sidebars.ts +1 -0
- data/lib/command/ai_github_flow_prompt.rb +2 -2
- data/lib/command/apply_template.rb +36 -4
- data/lib/command/base.rb +56 -3
- data/lib/command/delete.rb +174 -4
- data/lib/command/deploy_image.rb +6 -0
- data/lib/command/generate.rb +247 -3
- data/lib/command/generate_github_actions.rb +7 -1
- data/lib/command/setup_app.rb +92 -10
- data/lib/command/update_github_actions.rb +81 -43
- data/lib/core/config.rb +73 -0
- data/lib/core/controlplane.rb +22 -2
- data/lib/core/controlplane_api.rb +28 -2
- data/lib/core/controlplane_api_direct.rb +17 -0
- data/lib/core/disposable_postgres_helpers.rb +167 -0
- data/lib/core/doctor_service.rb +7 -0
- data/lib/core/github_flow_readiness/checks.rb +2 -1
- data/lib/core/github_flow_readiness_service.rb +4 -0
- data/lib/core/repo_introspection.rb +112 -2
- data/lib/cpflow/version.rb +1 -1
- data/lib/generator_templates/.dockerignore +16 -0
- data/lib/generator_templates/Dockerfile +9 -5
- data/lib/generator_templates/entrypoint.sh +2 -0
- data/lib/generator_templates/templates/postgres.yml +9 -0
- data/lib/generator_templates_sqlite/controlplane.yml +2 -1
- data/lib/generator_templates_sqlite/release_script.sh +1 -1
- data/lib/generator_templates_sqlite/templates/rails.yml +2 -1
- data/lib/github_flow_templates/.github/cpflow-help.md +39 -12
- data/lib/github_flow_templates/.github/workflows/cpflow-deploy-review-app.yml +1 -0
- data/lib/github_flow_templates/.github/workflows/cpflow-deploy-staging.yml +1 -0
- data/lib/github_flow_templates/.github/workflows/cpflow-promote-staging-to-production.yml +128 -9
- data/lib/github_flow_templates/bin/pin-cpflow-github-ref +37 -13
- data/lib/github_flow_templates/bin/test-cpflow-github-flow +62 -43
- metadata +11 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2abf58d5c1099a9f57164ebe03843c014180098e23551d73822419b05191070a
|
|
4
|
+
data.tar.gz: ab7adb1a83dbea1215c5686748e1438006a3a58ce419d1797a53172a670ce86e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4fcb5b12cc9b4ce2edec7afc583bdd37a62812a40e5ea478e1efc09a29e82d3d0bfe47386001b3978a11bcc3deb342044596c54d52eecc605e5a04077fa322be
|
|
7
|
+
data.tar.gz: e5f33d39830fffc5fcb66e9f22aeb5d519c1ed18b51ae1ec77fe7a65c9e26b42426bac48aeaa5e62bff5c218c6cc2f5e9f131fdfd211cd2343a9c0671afa7880
|
data/.agents/agent-workflow.yml
CHANGED
|
@@ -1,39 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
# AGENTS.md directs portable skills here; it does not duplicate or override them.
|
|
4
|
-
# Commands live as scripts in .agents/bin/ (see .agents/bin/README.md).
|
|
1
|
+
---
|
|
2
|
+
version: 1
|
|
5
3
|
base_branch: main
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
# Consumed by the portable untrusted-contributor-intake skill. These values must
|
|
21
|
-
# be read from the trusted base ref, never from contributor-controlled PR content.
|
|
22
|
-
untrusted_contributor_intake:
|
|
23
|
-
trusted_github_host: github.com
|
|
24
|
-
trusted_github_scheme: https
|
|
25
|
-
trusted_github_repo: shakacode/control-plane-flow
|
|
26
|
-
repo_prefix: CPF
|
|
27
|
-
|
|
28
|
-
# External actions already used by this repository and reviewed for the closed
|
|
29
|
-
# allowlist enforced by the GitHub Actions security scanner. Every use still
|
|
30
|
-
# requires an immutable commit SHA and a same-line release-version comment.
|
|
31
|
-
trusted_actions:
|
|
32
|
-
- actions/checkout
|
|
33
|
-
- actions/create-github-app-token
|
|
34
|
-
- actions/github-script
|
|
35
|
-
- actions/upload-artifact
|
|
36
|
-
- anthropics/claude-code-action
|
|
37
|
-
- docker/setup-buildx-action
|
|
38
|
-
- peter-evans/repository-dispatch
|
|
39
|
-
- ruby/setup-ruby
|
|
4
|
+
review:
|
|
5
|
+
required: none
|
|
6
|
+
# Local reviewer preference; independent from hosted review workflows.
|
|
7
|
+
local_review_agents:
|
|
8
|
+
- provider: anthropic
|
|
9
|
+
model_family: claude
|
|
10
|
+
- provider: openai
|
|
11
|
+
model_family: codex
|
|
12
|
+
merge:
|
|
13
|
+
preference: ask
|
|
14
|
+
branches:
|
|
15
|
+
name: "{login}-{host}/{issue}-{description}"
|
|
16
|
+
wip:
|
|
17
|
+
include_locations: false
|
data/.agents/bin/README.md
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
# Agent Workflow Scripts
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
means that capability is n/a here.
|
|
3
|
+
Thin, repo-owned command wrappers. Shaka's typed contract uses `setup`,
|
|
4
|
+
`validate`, and `test`; the remaining scripts are additional repository helpers.
|
|
5
|
+
A script that is **absent** means that capability is n/a here.
|
|
7
6
|
|
|
8
7
|
| Script | Purpose | This repo runs |
|
|
9
8
|
| --- | --- | --- |
|
|
@@ -25,7 +24,7 @@ update that array and its inventory regression test; use a `.sh` or `.bash`
|
|
|
25
24
|
suffix for automatic inclusion. Declared paths must remain tracked and readable.
|
|
26
25
|
Unlisted extensionless files and non-shell files are outside this check.
|
|
27
26
|
|
|
28
|
-
Canonical
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
Canonical typed Shaka policy lives in
|
|
28
|
+
[`../agent-workflow.yml`](../agent-workflow.yml). The GitHub Actions dependency allowlist lives in
|
|
29
|
+
[`../trusted-actions.yml`](../trusted-actions.yml). Additional agent-binding repository policy,
|
|
30
|
+
including the release-QA runbook reference, lives in [`../../AGENTS.md`](../../AGENTS.md).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
# Trusted policy retained for tools that have not migrated to Shaka's typed
|
|
3
|
+
# repository contract. This file is not part of the Shaka seam. Read every
|
|
4
|
+
# value from a trusted base ref established before reading candidate content.
|
|
5
|
+
# Missing, unreadable, or invalid values must fail closed.
|
|
6
|
+
untrusted_contributor_intake:
|
|
7
|
+
trusted_github_host: github.com
|
|
8
|
+
trusted_github_scheme: https
|
|
9
|
+
trusted_github_repo: shakacode/control-plane-flow
|
|
10
|
+
repo_prefix: CPF
|
|
11
|
+
coordination_backend: "private shakacode/agent-coordination (claims/heartbeats namespaced by full repo name)"
|
data/.agents/shaka.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Shaka configuration
|
|
2
|
+
|
|
3
|
+
Shaka package / skill release: `0.1.0.pre.1`.
|
|
4
|
+
|
|
5
|
+
## Migration reference
|
|
6
|
+
|
|
7
|
+
This repo pins its Shaka reader source to the revision containing the [migration
|
|
8
|
+
procedure at `8431a718cfd91e9ce7cb4276baae076848e05d13`](https://github.com/shakacode/shaka/blob/8431a718cfd91e9ce7cb4276baae076848e05d13/skills/shaka/references/migration.md).
|
|
9
|
+
At that Shaka revision, `skills/shaka/lib/shaka/version.rb` defines the package
|
|
10
|
+
release and `docs/settings.md` documents the typed seam. The migration procedure
|
|
11
|
+
describes key changes and the two-step validation used for first adoption.
|
|
12
|
+
|
|
13
|
+
## Check configuration edits
|
|
14
|
+
|
|
15
|
+
Run from the repository root:
|
|
16
|
+
|
|
17
|
+
`shaka seam check --root . --local`
|
|
18
|
+
|
|
19
|
+
Checks the current checkout's YAML and fixed-script paths and executable bits. It
|
|
20
|
+
does not execute the wrappers and grants no trusted policy authority.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
# External GitHub Actions repositories reviewed for use by this repository.
|
|
3
|
+
# Keep this list synchronized with the workflow dependency policy spec.
|
|
4
|
+
trusted_actions:
|
|
5
|
+
- actions/checkout
|
|
6
|
+
- actions/create-github-app-token
|
|
7
|
+
- actions/github-script
|
|
8
|
+
- actions/upload-artifact
|
|
9
|
+
- anthropics/claude-code-action
|
|
10
|
+
- docker/setup-buildx-action
|
|
11
|
+
- peter-evans/repository-dispatch
|
|
12
|
+
- ruby/setup-ruby
|
data/.coderabbit.yaml
CHANGED
|
@@ -17,6 +17,9 @@ inputs:
|
|
|
17
17
|
docker_build_extra_args:
|
|
18
18
|
description: Optional newline-delimited extra docker build tokens. Use key=value forms like --build-arg=FOO=bar.
|
|
19
19
|
required: false
|
|
20
|
+
docker_build_secrets:
|
|
21
|
+
description: Optional newline-delimited id=value secrets, passed through BuildKit secret mounts.
|
|
22
|
+
required: false
|
|
20
23
|
docker_build_ssh_key:
|
|
21
24
|
description: Optional private SSH key used for Docker builds that fetch private dependencies with RUN --mount=type=ssh
|
|
22
25
|
required: false
|
|
@@ -31,17 +34,56 @@ inputs:
|
|
|
31
34
|
runs:
|
|
32
35
|
using: composite
|
|
33
36
|
steps:
|
|
37
|
+
- name: Prepare Docker build secrets
|
|
38
|
+
id: build-secrets
|
|
39
|
+
if: ${{ inputs.docker_build_secrets != '' }}
|
|
40
|
+
shell: bash
|
|
41
|
+
env:
|
|
42
|
+
DOCKER_BUILD_SECRETS: ${{ inputs.docker_build_secrets }}
|
|
43
|
+
run: |
|
|
44
|
+
set -euo pipefail
|
|
45
|
+
umask 077
|
|
46
|
+
secret_directory="$(mktemp -d "${RUNNER_TEMP}/cpflow-build-secrets.XXXXXX")"
|
|
47
|
+
secrets_ready=false
|
|
48
|
+
trap 'if [[ "${secrets_ready}" != "true" ]]; then rm -rf "${secret_directory}"; fi' EXIT
|
|
49
|
+
while IFS= read -r entry || [[ -n "${entry}" ]]; do
|
|
50
|
+
entry="${entry%$'\r'}"
|
|
51
|
+
[[ -n "${entry}" ]] || continue
|
|
52
|
+
if [[ "${entry}" != *=* ]]; then
|
|
53
|
+
echo "Docker build secrets must use id=value entries." >&2
|
|
54
|
+
exit 1
|
|
55
|
+
fi
|
|
56
|
+
secret_id="${entry%%=*}"
|
|
57
|
+
secret_value="${entry#*=}"
|
|
58
|
+
if [[ ! "${secret_id}" =~ ^[a-zA-Z0-9][a-zA-Z0-9_-]*$ ]] || [[ -z "${secret_value}" ]]; then
|
|
59
|
+
echo "Docker build secrets require a valid id and a nonempty value." >&2
|
|
60
|
+
exit 1
|
|
61
|
+
fi
|
|
62
|
+
if [[ -e "${secret_directory}/${secret_id}" ]]; then
|
|
63
|
+
echo "Duplicate Docker build secret id." >&2
|
|
64
|
+
exit 1
|
|
65
|
+
fi
|
|
66
|
+
# GitHub masks the stored id=value entry; register the extracted value too.
|
|
67
|
+
masked_value="${secret_value//%/%25}"
|
|
68
|
+
masked_value="${masked_value//$'\r'/%0D}"
|
|
69
|
+
masked_value="${masked_value//$'\n'/%0A}"
|
|
70
|
+
printf '::add-mask::%s\n' "${masked_value}"
|
|
71
|
+
printf '%s' "${secret_value}" > "${secret_directory}/${secret_id}"
|
|
72
|
+
done <<< "${DOCKER_BUILD_SECRETS}"
|
|
73
|
+
echo "directory=${secret_directory}" >> "${GITHUB_OUTPUT}"
|
|
74
|
+
secrets_ready=true
|
|
75
|
+
|
|
34
76
|
# Keep SSH key handling in a dedicated step so DOCKER_BUILD_SSH_KEY is never present
|
|
35
77
|
# in the main build step's environment. ACTIONS_STEP_DEBUG=true dumps env before any
|
|
36
78
|
# command runs, so keeping the key out of env there avoids even admin-triggered exposure.
|
|
37
79
|
- name: Prepare SSH agent for Docker build
|
|
80
|
+
id: prepare-build-ssh
|
|
38
81
|
if: ${{ inputs.docker_build_ssh_key != '' }}
|
|
39
82
|
shell: bash
|
|
40
83
|
env:
|
|
41
|
-
# Pass the key via env so
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
# this step only — the build step below does not receive DOCKER_BUILD_SSH_KEY.
|
|
84
|
+
# Pass the key via env so ssh-add can read it from stdin without persisting it to
|
|
85
|
+
# disk. Scope is this step only — the build step below receives the agent socket,
|
|
86
|
+
# not DOCKER_BUILD_SSH_KEY.
|
|
45
87
|
DOCKER_BUILD_SSH_KEY: ${{ inputs.docker_build_ssh_key }}
|
|
46
88
|
DOCKER_BUILD_SSH_KNOWN_HOSTS: ${{ inputs.docker_build_ssh_known_hosts }}
|
|
47
89
|
run: |
|
|
@@ -51,6 +93,49 @@ runs:
|
|
|
51
93
|
mkdir -p ~/.ssh
|
|
52
94
|
chmod 700 ~/.ssh
|
|
53
95
|
|
|
96
|
+
known_hosts_backup="${HOME}/.ssh/cpflow_known_hosts_backup"
|
|
97
|
+
if [[ -e "${known_hosts_backup}" ]]; then
|
|
98
|
+
echo "Refusing to replace SSH known_hosts: a prior cpflow backup exists at ${known_hosts_backup}." >&2
|
|
99
|
+
echo "If no build is currently running on this runner, restore or remove that backup, then retry." >&2
|
|
100
|
+
exit 1
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
known_hosts_backup_ready=false
|
|
104
|
+
known_hosts_replaced=false
|
|
105
|
+
ssh_agent_started=false
|
|
106
|
+
restore_known_hosts_on_prep_failure() {
|
|
107
|
+
status=$?
|
|
108
|
+
if [[ "${status}" -ne 0 ]]; then
|
|
109
|
+
if [[ "${ssh_agent_started}" == "true" ]]; then
|
|
110
|
+
ssh-agent -k >/dev/null || true
|
|
111
|
+
fi
|
|
112
|
+
if [[ "${known_hosts_backup_ready}" == "true" ]]; then
|
|
113
|
+
mv -f -- "${known_hosts_backup}" "${HOME}/.ssh/known_hosts"
|
|
114
|
+
else
|
|
115
|
+
rm -f "${known_hosts_backup}"
|
|
116
|
+
if [[ "${known_hosts_replaced}" == "true" ]]; then
|
|
117
|
+
rm -f "${HOME}/.ssh/known_hosts"
|
|
118
|
+
fi
|
|
119
|
+
fi
|
|
120
|
+
fi
|
|
121
|
+
exit "${status}"
|
|
122
|
+
}
|
|
123
|
+
trap restore_known_hosts_on_prep_failure EXIT
|
|
124
|
+
|
|
125
|
+
if [[ -L "${HOME}/.ssh/known_hosts" ]]; then
|
|
126
|
+
echo "Refusing to replace symlinked SSH known_hosts." >&2
|
|
127
|
+
exit 1
|
|
128
|
+
fi
|
|
129
|
+
if [[ -e "${HOME}/.ssh/known_hosts" ]]; then
|
|
130
|
+
if [[ ! -f "${HOME}/.ssh/known_hosts" ]]; then
|
|
131
|
+
echo "Refusing to replace non-file SSH known_hosts." >&2
|
|
132
|
+
exit 1
|
|
133
|
+
fi
|
|
134
|
+
cp -p -- "${HOME}/.ssh/known_hosts" "${known_hosts_backup}"
|
|
135
|
+
known_hosts_backup_ready=true
|
|
136
|
+
fi
|
|
137
|
+
|
|
138
|
+
known_hosts_replaced=true
|
|
54
139
|
if [[ -n "${DOCKER_BUILD_SSH_KNOWN_HOSTS}" ]]; then
|
|
55
140
|
printf '%s\n' "${DOCKER_BUILD_SSH_KNOWN_HOSTS}" > ~/.ssh/known_hosts
|
|
56
141
|
else
|
|
@@ -62,8 +147,15 @@ runs:
|
|
|
62
147
|
fi
|
|
63
148
|
chmod 600 ~/.ssh/known_hosts
|
|
64
149
|
|
|
65
|
-
|
|
66
|
-
|
|
150
|
+
eval "$(ssh-agent -s)"
|
|
151
|
+
ssh_agent_started=true
|
|
152
|
+
printf '%s\n' "${DOCKER_BUILD_SSH_KEY}" | ssh-add -
|
|
153
|
+
{
|
|
154
|
+
echo "prepared=true"
|
|
155
|
+
echo "known_hosts_had_original=${known_hosts_backup_ready}"
|
|
156
|
+
echo "ssh_auth_sock=${SSH_AUTH_SOCK}"
|
|
157
|
+
echo "ssh_agent_pid=${SSH_AGENT_PID}"
|
|
158
|
+
} >> "${GITHUB_OUTPUT}"
|
|
67
159
|
|
|
68
160
|
- name: Build Docker image
|
|
69
161
|
shell: bash
|
|
@@ -71,27 +163,34 @@ runs:
|
|
|
71
163
|
APP_NAME: ${{ inputs.app_name }}
|
|
72
164
|
COMMIT_SHA: ${{ inputs.commit }}
|
|
73
165
|
CONTROL_PLANE_ORG: ${{ inputs.org }}
|
|
166
|
+
DOCKER_BUILD_SECRET_DIRECTORY: ${{ steps.build-secrets.outputs.directory }}
|
|
74
167
|
DOCKER_BUILD_EXTRA_ARGS: ${{ inputs.docker_build_extra_args }}
|
|
75
168
|
PR_NUMBER: ${{ inputs.pr_number }}
|
|
169
|
+
CPFLOW_BUILD_SSH_PREPPED: ${{ steps.prepare-build-ssh.outputs.prepared }}
|
|
170
|
+
CPFLOW_KNOWN_HOSTS_HAD_ORIGINAL: ${{ steps.prepare-build-ssh.outputs.known_hosts_had_original }}
|
|
171
|
+
SSH_AUTH_SOCK: ${{ steps.prepare-build-ssh.outputs.ssh_auth_sock }}
|
|
172
|
+
SSH_AGENT_PID: ${{ steps.prepare-build-ssh.outputs.ssh_agent_pid }}
|
|
76
173
|
WORKING_DIRECTORY: ${{ inputs.working_directory }}
|
|
77
174
|
run: |
|
|
78
175
|
set -euo pipefail
|
|
79
176
|
|
|
80
177
|
PR_INFO=""
|
|
81
178
|
docker_build_args=()
|
|
82
|
-
|
|
83
|
-
build_ssh_prepped=false
|
|
179
|
+
build_ssh_prepped="${CPFLOW_BUILD_SSH_PREPPED:-false}"
|
|
84
180
|
|
|
85
181
|
cleanup_build_ssh() {
|
|
86
|
-
if [[ "${
|
|
87
|
-
|
|
182
|
+
if [[ -n "${DOCKER_BUILD_SECRET_DIRECTORY:-}" ]]; then
|
|
183
|
+
rm -rf -- "${DOCKER_BUILD_SECRET_DIRECTORY}"
|
|
88
184
|
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
185
|
if [[ "${build_ssh_prepped}" == "true" ]]; then
|
|
94
|
-
|
|
186
|
+
ssh-agent -k >/dev/null || true
|
|
187
|
+
# Restore a pre-existing known_hosts file on reused runners. When there was
|
|
188
|
+
# no original file, remove only the file created by this action.
|
|
189
|
+
if [[ -f "${HOME}/.ssh/cpflow_known_hosts_backup" ]]; then
|
|
190
|
+
mv -f -- "${HOME}/.ssh/cpflow_known_hosts_backup" "${HOME}/.ssh/known_hosts"
|
|
191
|
+
else
|
|
192
|
+
rm -f "${HOME}/.ssh/known_hosts"
|
|
193
|
+
fi
|
|
95
194
|
fi
|
|
96
195
|
}
|
|
97
196
|
trap cleanup_build_ssh EXIT
|
|
@@ -116,16 +215,48 @@ runs:
|
|
|
116
215
|
done <<< "${DOCKER_BUILD_EXTRA_ARGS}"
|
|
117
216
|
fi
|
|
118
217
|
|
|
119
|
-
if [[
|
|
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"
|
|
218
|
+
if [[ "${build_ssh_prepped}" == "true" ]]; then
|
|
126
219
|
docker_build_args+=("--ssh=default")
|
|
127
220
|
fi
|
|
128
221
|
|
|
222
|
+
if [[ -n "${DOCKER_BUILD_SECRET_DIRECTORY:-}" ]]; then
|
|
223
|
+
for secret_path in "${DOCKER_BUILD_SECRET_DIRECTORY}"/*; do
|
|
224
|
+
[[ -f "${secret_path}" ]] || continue
|
|
225
|
+
docker_build_args+=("--secret=id=${secret_path##*/},src=${secret_path}")
|
|
226
|
+
done
|
|
227
|
+
fi
|
|
228
|
+
|
|
129
229
|
echo "🏗️ Building Docker image${PR_INFO} (commit ${COMMIT_SHA})..."
|
|
130
230
|
cpflow build-image -a "${APP_NAME}" --commit="${COMMIT_SHA}" --org="${CONTROL_PLANE_ORG}" "${docker_build_args[@]}"
|
|
131
231
|
echo "✅ Docker image build successful${PR_INFO} (commit ${COMMIT_SHA})"
|
|
232
|
+
|
|
233
|
+
- name: Clean up SSH build state
|
|
234
|
+
if: ${{ always() && inputs.docker_build_ssh_key != '' }}
|
|
235
|
+
shell: bash
|
|
236
|
+
env:
|
|
237
|
+
CPFLOW_BUILD_SSH_PREPPED: ${{ steps.prepare-build-ssh.outputs.prepared }}
|
|
238
|
+
CPFLOW_KNOWN_HOSTS_HAD_ORIGINAL: ${{ steps.prepare-build-ssh.outputs.known_hosts_had_original }}
|
|
239
|
+
SSH_AUTH_SOCK: ${{ steps.prepare-build-ssh.outputs.ssh_auth_sock }}
|
|
240
|
+
SSH_AGENT_PID: ${{ steps.prepare-build-ssh.outputs.ssh_agent_pid }}
|
|
241
|
+
run: |
|
|
242
|
+
set +e
|
|
243
|
+
|
|
244
|
+
if [[ "${CPFLOW_BUILD_SSH_PREPPED:-false}" == "true" ]]; then
|
|
245
|
+
ssh-agent -k >/dev/null || true
|
|
246
|
+
if [[ -f "${HOME}/.ssh/cpflow_known_hosts_backup" ]]; then
|
|
247
|
+
mv -f -- "${HOME}/.ssh/cpflow_known_hosts_backup" "${HOME}/.ssh/known_hosts"
|
|
248
|
+
elif [[ "${CPFLOW_KNOWN_HOSTS_HAD_ORIGINAL:-}" == "false" ]]; then
|
|
249
|
+
rm -f "${HOME}/.ssh/known_hosts"
|
|
250
|
+
fi
|
|
251
|
+
fi
|
|
252
|
+
|
|
253
|
+
- name: Clean up Docker build secrets
|
|
254
|
+
if: ${{ always() && inputs.docker_build_secrets != '' }}
|
|
255
|
+
shell: bash
|
|
256
|
+
env:
|
|
257
|
+
DOCKER_BUILD_SECRET_DIRECTORY: ${{ steps.build-secrets.outputs.directory }}
|
|
258
|
+
run: |
|
|
259
|
+
set -euo pipefail
|
|
260
|
+
if [[ -n "${DOCKER_BUILD_SECRET_DIRECTORY:-}" ]]; then
|
|
261
|
+
rm -rf -- "${DOCKER_BUILD_SECRET_DIRECTORY}"
|
|
262
|
+
fi
|
|
@@ -29,6 +29,9 @@ case "$exists_status" in
|
|
|
29
29
|
printf '%s\n' "$exists_output"
|
|
30
30
|
fi
|
|
31
31
|
echo "⚠️ Application does not exist: $APP_NAME"
|
|
32
|
+
echo "🧹 Checking for disposable review app resources: $APP_NAME"
|
|
33
|
+
cpflow delete -a "$APP_NAME" --org "$CPLN_ORG" --yes
|
|
34
|
+
echo "✅ Finished disposable review app cleanup: $APP_NAME"
|
|
32
35
|
exit 0
|
|
33
36
|
;;
|
|
34
37
|
*)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
name: Preview Production Promotion
|
|
2
|
+
description: Compare the deployed staging image to the last recorded production release before approval
|
|
3
|
+
inputs:
|
|
4
|
+
staging_app_name:
|
|
5
|
+
required: true
|
|
6
|
+
description: Staging app to read
|
|
7
|
+
staging_org:
|
|
8
|
+
required: true
|
|
9
|
+
description: Normalized staging org
|
|
10
|
+
primary_workload:
|
|
11
|
+
required: false
|
|
12
|
+
default: ""
|
|
13
|
+
description: Optional primary workload override
|
|
14
|
+
outputs:
|
|
15
|
+
primary_workload:
|
|
16
|
+
description: Workload whose staging image was previewed
|
|
17
|
+
value: ${{ steps.workloads.outputs.primary }}
|
|
18
|
+
staging_image:
|
|
19
|
+
description: Captured staging image for approval
|
|
20
|
+
value: ${{ steps.preview.outputs.staging_image }}
|
|
21
|
+
production_commit:
|
|
22
|
+
description: Last recorded production release commit, if available
|
|
23
|
+
value: ${{ steps.preview.outputs.production_commit }}
|
|
24
|
+
runs:
|
|
25
|
+
using: composite
|
|
26
|
+
steps:
|
|
27
|
+
- name: Resolve primary workload
|
|
28
|
+
id: workloads
|
|
29
|
+
env:
|
|
30
|
+
STAGING_APP_NAME: ${{ inputs.staging_app_name }}
|
|
31
|
+
PRIMARY_WORKLOAD: ${{ inputs.primary_workload }}
|
|
32
|
+
shell: bash
|
|
33
|
+
run: |
|
|
34
|
+
set -euo pipefail
|
|
35
|
+
|
|
36
|
+
ruby - "${STAGING_APP_NAME}" "${PRIMARY_WORKLOAD}" >> "$GITHUB_OUTPUT" <<'RUBY'
|
|
37
|
+
require "yaml"
|
|
38
|
+
|
|
39
|
+
app = ARGV.fetch(0)
|
|
40
|
+
requested_primary = ARGV.fetch(1, "").to_s.strip
|
|
41
|
+
data = YAML.safe_load(File.read(".controlplane/controlplane.yml"), aliases: true)
|
|
42
|
+
apps = data["apps"] || {}
|
|
43
|
+
app_config = apps[app]
|
|
44
|
+
|
|
45
|
+
unless app_config
|
|
46
|
+
warn "Error: app '#{app}' is not defined under `apps:` in `.controlplane/controlplane.yml`."
|
|
47
|
+
warn " Fix the STAGING_APP_NAME repository variable or add the app to controlplane.yml."
|
|
48
|
+
exit 1
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
workloads = Array(app_config["app_workloads"]).map(&:to_s).reject(&:empty?)
|
|
52
|
+
workloads = ["rails"] if workloads.empty?
|
|
53
|
+
|
|
54
|
+
primary =
|
|
55
|
+
if requested_primary.empty?
|
|
56
|
+
if workloads.length == 1
|
|
57
|
+
workloads.first
|
|
58
|
+
elsif workloads.include?("rails")
|
|
59
|
+
"rails"
|
|
60
|
+
else
|
|
61
|
+
warn "::error::PRIMARY_WORKLOAD is not configured and app '#{app}' has multiple workloads: #{workloads.join(', ')}."
|
|
62
|
+
warn " Set the PRIMARY_WORKLOAD repository variable to one of these workloads."
|
|
63
|
+
exit 1
|
|
64
|
+
end
|
|
65
|
+
elsif workloads.include?(requested_primary)
|
|
66
|
+
requested_primary
|
|
67
|
+
else
|
|
68
|
+
warn "::error::PRIMARY_WORKLOAD '#{requested_primary}' is not one of: #{workloads.join(', ')}."
|
|
69
|
+
exit 1
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
puts "primary=#{primary}"
|
|
73
|
+
RUBY
|
|
74
|
+
- name: Compare recorded production release to staging
|
|
75
|
+
id: preview
|
|
76
|
+
env:
|
|
77
|
+
ACTION_PATH: ${{ github.action_path }}
|
|
78
|
+
STAGING_APP_NAME: ${{ inputs.staging_app_name }}
|
|
79
|
+
CPLN_ORG_STAGING: ${{ inputs.staging_org }}
|
|
80
|
+
PRIMARY_WORKLOAD: ${{ steps.workloads.outputs.primary }}
|
|
81
|
+
shell: bash
|
|
82
|
+
run: bash "${ACTION_PATH}/preview-promotion.sh"
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# This job has only the staging token. Production provenance comes from the
|
|
5
|
+
# successful production-* release record and is checked against the live image
|
|
6
|
+
# by the protected deployment job after approval.
|
|
7
|
+
export GH_HOST="${GITHUB_SERVER_URL#https://}"
|
|
8
|
+
export GH_ENTERPRISE_TOKEN="${GH_TOKEN:-}"
|
|
9
|
+
workload_json="$(cpln workload get "$PRIMARY_WORKLOAD" --gvc "$STAGING_APP_NAME" --org "$CPLN_ORG_STAGING" -o json)"
|
|
10
|
+
staging_image="$(jq -r '.spec.containers[0].image // empty' <<< "$workload_json")"
|
|
11
|
+
if [[ "$staging_image" == /org/*/image/* ]]; then
|
|
12
|
+
staging_image="${staging_image##*/image/}"
|
|
13
|
+
elif [[ "$staging_image" == *.registry.cpln.io/* ]]; then
|
|
14
|
+
staging_image="${staging_image#*.registry.cpln.io/}"
|
|
15
|
+
fi
|
|
16
|
+
staging_tag="${staging_image%%@*}"
|
|
17
|
+
staging_commit="${staging_tag##*_}"
|
|
18
|
+
if [[ "$staging_image" == *$'\n'* || "$staging_image" == *$'\r'* ]] || ! [[ "$staging_commit" =~ ^[0-9a-f]{40}$ ]]; then
|
|
19
|
+
echo "::error::The deployed staging image must include a full lowercase commit SHA suffix."
|
|
20
|
+
exit 1
|
|
21
|
+
fi
|
|
22
|
+
echo "staging_image=$staging_image" >> "$GITHUB_OUTPUT"
|
|
23
|
+
{
|
|
24
|
+
echo "## Commits ready for production"
|
|
25
|
+
echo
|
|
26
|
+
echo "Preview captured before production approval. Baseline: the last recorded successful production release; live production is verified after approval."
|
|
27
|
+
echo
|
|
28
|
+
} >> "$GITHUB_STEP_SUMMARY"
|
|
29
|
+
|
|
30
|
+
# Limit release discovery to the latest 100 entries. Missing provenance is
|
|
31
|
+
# explicit rather than falling back to a moving branch or an unrelated release.
|
|
32
|
+
if ! releases="$(timeout 30s gh api "repos/${GH_REPO}/releases?per_page=100")"; then
|
|
33
|
+
echo "Comparison unavailable: GitHub could not read the production release history." >> "$GITHUB_STEP_SUMMARY"
|
|
34
|
+
exit 0
|
|
35
|
+
fi
|
|
36
|
+
production_release="$(jq -r '[.[] | select(.draft == false and .prerelease == false) | select(.tag_name | test("^production-[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{6}-[0-9]+$"))] | sort_by(.published_at // "") | reverse | .[0].tag_name // empty' <<< "$releases")"
|
|
37
|
+
if [[ -z "$production_release" ]]; then
|
|
38
|
+
echo "Comparison unavailable: no successful production release was found in the latest 100 releases." >> "$GITHUB_STEP_SUMMARY"
|
|
39
|
+
exit 0
|
|
40
|
+
fi
|
|
41
|
+
# Resolve the tag itself: target_commitish can be a branch name on older releases.
|
|
42
|
+
if ! production_json="$(timeout 30s gh api "repos/${GH_REPO}/commits/${production_release}")"; then
|
|
43
|
+
echo "Comparison unavailable: GitHub could not resolve the recorded production release." >> "$GITHUB_STEP_SUMMARY"
|
|
44
|
+
exit 0
|
|
45
|
+
fi
|
|
46
|
+
production_commit="$(jq -r '.sha // empty' <<< "$production_json")"
|
|
47
|
+
if ! [[ "$production_commit" =~ ^[0-9a-f]{40}$ ]]; then
|
|
48
|
+
echo "Comparison unavailable: the recorded production release has no full commit SHA." >> "$GITHUB_STEP_SUMMARY"
|
|
49
|
+
exit 0
|
|
50
|
+
fi
|
|
51
|
+
echo "production_commit=$production_commit" >> "$GITHUB_OUTPUT"
|
|
52
|
+
|
|
53
|
+
repository_url="${GITHUB_SERVER_URL}/${GH_REPO}"
|
|
54
|
+
echo "[Recorded production release](${repository_url}/releases/tag/${production_release})" >> "$GITHUB_STEP_SUMMARY"
|
|
55
|
+
{
|
|
56
|
+
echo "Recorded production release: [\`${production_commit:0:7}\`](${repository_url}/commit/${production_commit})"
|
|
57
|
+
echo "Staging: [\`${staging_commit:0:7}\`](${repository_url}/commit/${staging_commit})"
|
|
58
|
+
echo
|
|
59
|
+
echo "[Compare production to staging](${repository_url}/compare/${production_commit}...${staging_commit})"
|
|
60
|
+
echo
|
|
61
|
+
} >> "$GITHUB_STEP_SUMMARY"
|
|
62
|
+
if [[ "${production_commit}" == "${staging_commit}" ]]; then
|
|
63
|
+
echo "Production and staging use the same commit; there are no commits to promote." >> "$GITHUB_STEP_SUMMARY"
|
|
64
|
+
exit 0
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
comparison="$(mktemp)"
|
|
68
|
+
trap 'rm -f "$comparison" "${comparison}.summary"' EXIT
|
|
69
|
+
# One bounded page; the compare link provides the complete diff and history.
|
|
70
|
+
if ! timeout 30s gh api "repos/${GH_REPO}/compare/${production_commit}...${staging_commit}?per_page=100&page=1" > "$comparison"; then
|
|
71
|
+
echo "Commit list unavailable: GitHub could not compare these deployed commits. Use the compare link above." >> "$GITHUB_STEP_SUMMARY"
|
|
72
|
+
exit 0
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
if ! jq -r --arg repository_url "$repository_url" '
|
|
76
|
+
"\(.ahead_by) commit(s) on staging that are not in the recorded production release.", "",
|
|
77
|
+
(if .status == "diverged" or .status == "behind" then
|
|
78
|
+
"Warning: staging is \(.status); production has \(.behind_by) commit(s) that are not on staging. Review the comparison before treating this as a forward promotion."
|
|
79
|
+
else empty end), "",
|
|
80
|
+
"<ul>",
|
|
81
|
+
(.commits[:100][] |
|
|
82
|
+
"<li><a href=\"\($repository_url)/commit/\(.sha)\">\(.sha[:7])</a> " +
|
|
83
|
+
(.commit.message | split("\n")[0] | @html) + "</li>"),
|
|
84
|
+
"</ul>", "",
|
|
85
|
+
(if .ahead_by > 100 then "Showing the first 100 commits; use the compare link for the complete list." else empty end)
|
|
86
|
+
' "$comparison" > "${comparison}.summary"; then
|
|
87
|
+
echo "Commit list unavailable: GitHub returned incomplete comparison data." >> "$GITHUB_STEP_SUMMARY"
|
|
88
|
+
exit 0
|
|
89
|
+
fi
|
|
90
|
+
cat "${comparison}.summary" >> "$GITHUB_STEP_SUMMARY"
|
|
@@ -4,6 +4,12 @@ run-name: "Deploy Review App - PR #${{ github.event.pull_request.number || githu
|
|
|
4
4
|
|
|
5
5
|
on:
|
|
6
6
|
workflow_call:
|
|
7
|
+
inputs:
|
|
8
|
+
docker_build_extra_args:
|
|
9
|
+
description: Optional newline-delimited extra docker build tokens, appended to the repository variable.
|
|
10
|
+
required: false
|
|
11
|
+
type: string
|
|
12
|
+
default: ""
|
|
7
13
|
outputs:
|
|
8
14
|
image_built:
|
|
9
15
|
description: Whether this run successfully built the application image.
|
|
@@ -12,6 +18,9 @@ on:
|
|
|
12
18
|
CPLN_TOKEN_STAGING:
|
|
13
19
|
description: Control Plane token for the staging org that owns review apps.
|
|
14
20
|
required: true
|
|
21
|
+
REVIEW_APP_DOCKER_BUILD_SECRETS:
|
|
22
|
+
description: Optional newline-delimited id=value secrets passed to review-app builds through BuildKit secret mounts.
|
|
23
|
+
required: false
|
|
15
24
|
DOCKER_BUILD_SSH_KEY:
|
|
16
25
|
description: Optional SSH key used by Docker builds that fetch private dependencies.
|
|
17
26
|
required: false
|
|
@@ -928,7 +937,10 @@ jobs:
|
|
|
928
937
|
org: ${{ steps.review-config.outputs.cpln_org }}
|
|
929
938
|
commit: ${{ env.PR_SHA }}
|
|
930
939
|
pr_number: ${{ env.PR_NUMBER }}
|
|
931
|
-
docker_build_extra_args:
|
|
940
|
+
docker_build_extra_args: |
|
|
941
|
+
${{ vars.DOCKER_BUILD_EXTRA_ARGS }}
|
|
942
|
+
${{ inputs.docker_build_extra_args }}
|
|
943
|
+
docker_build_secrets: ${{ secrets.REVIEW_APP_DOCKER_BUILD_SECRETS }}
|
|
932
944
|
docker_build_ssh_key: ${{ secrets.DOCKER_BUILD_SSH_KEY }}
|
|
933
945
|
docker_build_ssh_known_hosts: ${{ vars.DOCKER_BUILD_SSH_KNOWN_HOSTS }}
|
|
934
946
|
working_directory: app
|
|
@@ -5,6 +5,11 @@ run-name: Deploy Control Plane staging app
|
|
|
5
5
|
on:
|
|
6
6
|
workflow_call:
|
|
7
7
|
inputs:
|
|
8
|
+
docker_build_extra_args:
|
|
9
|
+
description: Optional newline-delimited extra docker build tokens, appended to the repository variable.
|
|
10
|
+
required: false
|
|
11
|
+
type: string
|
|
12
|
+
default: ""
|
|
8
13
|
staging_app_branch_default:
|
|
9
14
|
description: Fallback branch name baked into the generated caller workflow.
|
|
10
15
|
required: false
|
|
@@ -14,6 +19,9 @@ on:
|
|
|
14
19
|
CPLN_TOKEN_STAGING:
|
|
15
20
|
description: Control Plane token for the staging org.
|
|
16
21
|
required: true
|
|
22
|
+
DOCKER_BUILD_SECRETS:
|
|
23
|
+
description: Optional newline-delimited id=value secrets passed through BuildKit secret mounts.
|
|
24
|
+
required: false
|
|
17
25
|
DOCKER_BUILD_SSH_KEY:
|
|
18
26
|
description: Optional SSH key used by Docker builds that fetch private dependencies.
|
|
19
27
|
required: false
|
|
@@ -123,7 +131,10 @@ jobs:
|
|
|
123
131
|
app_name: ${{ env.APP_NAME }}
|
|
124
132
|
org: ${{ vars.CPLN_ORG_STAGING }}
|
|
125
133
|
commit: ${{ github.sha }}
|
|
126
|
-
docker_build_extra_args:
|
|
134
|
+
docker_build_extra_args: |
|
|
135
|
+
${{ vars.DOCKER_BUILD_EXTRA_ARGS }}
|
|
136
|
+
${{ inputs.docker_build_extra_args }}
|
|
137
|
+
docker_build_secrets: ${{ secrets.DOCKER_BUILD_SECRETS }}
|
|
127
138
|
docker_build_ssh_key: ${{ secrets.DOCKER_BUILD_SSH_KEY }}
|
|
128
139
|
docker_build_ssh_known_hosts: ${{ vars.DOCKER_BUILD_SSH_KNOWN_HOSTS }}
|
|
129
140
|
|