cpflow 5.1.0 → 5.2.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 +15 -0
- data/.agents/bin/README.md +19 -0
- data/.agents/bin/docs +5 -0
- data/.agents/bin/lint +5 -0
- data/.agents/bin/setup +5 -0
- data/.agents/bin/test +5 -0
- data/.agents/bin/validate +5 -0
- data/.agents/trusted-github-actors.yml +32 -0
- data/.agents/workflows/ai-rollout-e2e-test.md +166 -0
- data/.github/actions/cpflow-wait-for-health/action.yml +11 -4
- data/.github/workflows/claude-code-review.yml +2 -0
- data/.github/workflows/claude.yml +2 -0
- data/.github/workflows/cpflow-deploy-review-app.yml +16 -1
- data/.github/workflows/cpflow-promote-staging-to-production.yml +224 -37
- data/.github/workflows/rspec-shared.yml +15 -1
- data/.github/workflows/rspec-specific.yml +1 -0
- data/.github/workflows/rspec.yml +58 -1
- data/AGENTS.md +57 -0
- data/CHANGELOG.md +36 -1
- data/CLAUDE.md +3 -0
- data/CONTRIBUTING.md +6 -2
- data/Gemfile.lock +1 -1
- data/README.md +25 -7
- data/docs/ai-github-flow-prompt.md +18 -16
- data/docs/assets/logo/favicon.ico +0 -0
- data/docs/assets/logo/icon-1024.png +0 -0
- data/docs/assets/logo/icon-128.png +0 -0
- data/docs/assets/logo/icon-16.png +0 -0
- data/docs/assets/logo/icon-192.png +0 -0
- data/docs/assets/logo/icon-24.png +0 -0
- data/docs/assets/logo/icon-32.png +0 -0
- data/docs/assets/logo/icon-48.png +0 -0
- data/docs/assets/logo/icon-512.png +0 -0
- data/docs/assets/logo/icon-64.png +0 -0
- data/docs/assets/logo/icon-tile.svg +17 -0
- data/docs/assets/logo/mark-transparent.svg +16 -0
- data/docs/ci-automation.md +203 -15
- data/docs/commands.md +16 -1
- data/docs/grafana-opentelemetry.md +699 -0
- data/docs/secrets-and-env-values.md +29 -2
- data/docs/sidebars.ts +70 -0
- data/docs/telemetry/application-instrumentation.md +161 -0
- data/docs/telemetry/collector.md +297 -0
- data/docs/telemetry/index.md +152 -0
- data/docs/telemetry/pipelines.md +98 -0
- data/docs/telemetry/review-apps.md +55 -0
- data/docs/telemetry/troubleshooting.md +92 -0
- data/docs/terraform/example/.controlplane/controlplane.yml +0 -1
- data/docs/terraform/overview.md +11 -0
- data/docs/tips.md +458 -29
- data/examples/controlplane.yml +2 -0
- data/lib/command/ai_github_flow_prompt.rb +2 -2
- data/lib/command/base.rb +17 -2
- data/lib/command/deploy_image.rb +77 -5
- data/lib/command/maintenance_off.rb +1 -0
- data/lib/command/maintenance_on.rb +1 -0
- data/lib/command/promote_app_from_upstream.rb +1 -0
- data/lib/command/ps_wait.rb +2 -10
- data/lib/command/run.rb +25 -5
- data/lib/core/config.rb +94 -0
- data/lib/core/doctor_service.rb +44 -3
- data/lib/core/maintenance_mode.rb +93 -6
- data/lib/core/template_parser.rb +43 -9
- data/lib/cpflow/version.rb +1 -1
- data/lib/generator_templates/controlplane.yml +1 -2
- data/lib/github_flow_templates/.github/cpflow-help.md +23 -1
- data/lib/github_flow_templates/.github/workflows/cpflow-promote-staging-to-production.yml +224 -39
- metadata +33 -2
|
@@ -23,11 +23,33 @@ For the normal generated review-app path, GitHub needs one repository secret:
|
|
|
23
23
|
| --- | --- | --- |
|
|
24
24
|
| `CPLN_TOKEN_STAGING` | Repository secret | Control Plane service-account token for the staging/review org. |
|
|
25
25
|
|
|
26
|
+
For public repositories, use a staging/review token that cannot access
|
|
27
|
+
production Control Plane resources. Generated review-app deploys skip fork PR
|
|
28
|
+
heads because Docker builds use repository secrets. If a forked change needs a
|
|
29
|
+
review app, first move the reviewed change to a trusted branch in this
|
|
30
|
+
repository.
|
|
31
|
+
|
|
26
32
|
No repository variables are required for the standard review-app path when
|
|
27
33
|
`.controlplane/controlplane.yml` has exactly one review app entry with
|
|
28
34
|
`match_if_app_name_starts_with: true`. cpflow infers the review-app prefix and
|
|
29
35
|
staging org from that config.
|
|
30
36
|
|
|
37
|
+
Review apps run pull request code. Any value mounted through
|
|
38
|
+
`cpln://secret/...` can be read by that code after the workload starts, so keep
|
|
39
|
+
review-app secret dictionaries limited to disposable databases, review-only
|
|
40
|
+
renderer credentials, and license values that are acceptable for review-app
|
|
41
|
+
exposure.
|
|
42
|
+
|
|
43
|
+
For public demos, starter staging apps, and long-lived review apps, keep the app
|
|
44
|
+
workload `type: standard` with one warm replica, set its autoscaling metric to
|
|
45
|
+
`disabled`, and enable `capacityAI: true` so Control Plane can right-size CPU and
|
|
46
|
+
memory allocation at that fixed replica count. Shared Postgres and other
|
|
47
|
+
stateful workloads are the usual exceptions and should stay manually sized;
|
|
48
|
+
Capacity AI is for supported stateless app/service workloads. If true idle
|
|
49
|
+
scale-to-zero is explicitly required, create a separate `serverless` workload
|
|
50
|
+
before the first deploy or plan a delete/recreate migration because Control
|
|
51
|
+
Plane will not change an existing `standard` workload to `serverless` in place.
|
|
52
|
+
|
|
31
53
|
Optional overrides exist for forks, clones, and unusual apps:
|
|
32
54
|
|
|
33
55
|
| Name | Notes |
|
|
@@ -142,7 +164,7 @@ Most apps do not need these:
|
|
|
142
164
|
| Name | Notes |
|
|
143
165
|
| --- | --- |
|
|
144
166
|
| `DOCKER_BUILD_EXTRA_ARGS` | Newline-delimited extra Docker build tokens. |
|
|
145
|
-
| `DOCKER_BUILD_SSH_KEY` |
|
|
167
|
+
| `DOCKER_BUILD_SSH_KEY` | Read-only, revocable deploy key for Docker builds that fetch private dependencies. Do not use a personal SSH key. |
|
|
146
168
|
| `DOCKER_BUILD_SSH_KNOWN_HOSTS` | SSH known_hosts entries when SSH build hosts are not GitHub.com. |
|
|
147
169
|
| `REVIEW_APP_DEPLOYING_ICON_URL` | Cosmetic custom image URL for the animated deploying icon. Set to `none` to use the text fallback icon. |
|
|
148
170
|
| `STAGING_APP_BRANCH` | Custom staging branch. The branch must also appear in `cpflow-deploy-staging.yml`'s push filter. |
|
|
@@ -109,6 +109,53 @@ jobs:
|
|
|
109
109
|
variable:STAGING_APP_NAME
|
|
110
110
|
variable:PRODUCTION_APP_NAME
|
|
111
111
|
|
|
112
|
+
- name: Normalize Control Plane org names
|
|
113
|
+
id: cpln-orgs
|
|
114
|
+
env:
|
|
115
|
+
CPLN_ORG_STAGING: ${{ vars.CPLN_ORG_STAGING }}
|
|
116
|
+
CPLN_ORG_PRODUCTION: ${{ vars.CPLN_ORG_PRODUCTION }}
|
|
117
|
+
shell: bash
|
|
118
|
+
run: |
|
|
119
|
+
set -euo pipefail
|
|
120
|
+
|
|
121
|
+
sanitize_control_plane_name() {
|
|
122
|
+
local label="$1"
|
|
123
|
+
local value="$2"
|
|
124
|
+
|
|
125
|
+
value="${value#"${value%%[![:space:]]*}"}"
|
|
126
|
+
value="${value%"${value##*[![:space:]]}"}"
|
|
127
|
+
|
|
128
|
+
if [[ "${value}" == *$'\r'* || "${value}" == *$'\n'* ]]; then
|
|
129
|
+
echo "::error::${label} contains embedded line endings; remove them from the repository variable instead of relying on normalization." >&2
|
|
130
|
+
exit 1
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
printf '%s' "${value}"
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
validate_control_plane_org() {
|
|
137
|
+
local label="$1"
|
|
138
|
+
local value="$2"
|
|
139
|
+
|
|
140
|
+
if ! [[ "${value}" =~ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ ]]; then
|
|
141
|
+
local display_value
|
|
142
|
+
display_value="$(printf '%q' "${value}")"
|
|
143
|
+
echo "::error::${label} (${display_value}) must be a valid Control Plane org name; use lowercase alphanumeric characters and hyphens only, with no leading or trailing hyphen." >&2
|
|
144
|
+
exit 1
|
|
145
|
+
fi
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
staging_org="$(sanitize_control_plane_name "CPLN_ORG_STAGING" "${CPLN_ORG_STAGING}")"
|
|
149
|
+
production_org="$(sanitize_control_plane_name "CPLN_ORG_PRODUCTION" "${CPLN_ORG_PRODUCTION}")"
|
|
150
|
+
|
|
151
|
+
validate_control_plane_org "CPLN_ORG_STAGING" "${staging_org}"
|
|
152
|
+
validate_control_plane_org "CPLN_ORG_PRODUCTION" "${production_org}"
|
|
153
|
+
|
|
154
|
+
{
|
|
155
|
+
echo "staging=${staging_org}"
|
|
156
|
+
echo "production=${production_org}"
|
|
157
|
+
} >> "$GITHUB_OUTPUT"
|
|
158
|
+
|
|
112
159
|
- name: Capture release context
|
|
113
160
|
id: release-context
|
|
114
161
|
env:
|
|
@@ -127,7 +174,7 @@ jobs:
|
|
|
127
174
|
uses: ./.cpflow/.github/actions/cpflow-setup-environment
|
|
128
175
|
with:
|
|
129
176
|
token: ${{ secrets.CPLN_TOKEN_PRODUCTION }}
|
|
130
|
-
org: ${{
|
|
177
|
+
org: ${{ steps.cpln-orgs.outputs.production }}
|
|
131
178
|
working_directory: .cpflow
|
|
132
179
|
cpln_cli_version: ${{ vars.CPLN_CLI_VERSION }}
|
|
133
180
|
cpflow_version: ${{ vars.CPFLOW_VERSION }}
|
|
@@ -200,42 +247,100 @@ jobs:
|
|
|
200
247
|
CPLN_TOKEN_PRODUCTION: ${{ secrets.CPLN_TOKEN_PRODUCTION }}
|
|
201
248
|
STAGING_APP_NAME: ${{ vars.STAGING_APP_NAME }}
|
|
202
249
|
PRODUCTION_APP_NAME: ${{ vars.PRODUCTION_APP_NAME }}
|
|
203
|
-
CPLN_ORG_STAGING: ${{
|
|
204
|
-
CPLN_ORG_PRODUCTION: ${{
|
|
250
|
+
CPLN_ORG_STAGING: ${{ steps.cpln-orgs.outputs.staging }}
|
|
251
|
+
CPLN_ORG_PRODUCTION: ${{ steps.cpln-orgs.outputs.production }}
|
|
252
|
+
WORKLOAD_NAMES: ${{ steps.workloads.outputs.names }}
|
|
205
253
|
shell: bash
|
|
206
254
|
run: |
|
|
207
255
|
set -euo pipefail
|
|
208
256
|
|
|
209
|
-
|
|
210
|
-
|
|
257
|
+
list_gvc_env_names() {
|
|
258
|
+
local token="$1"
|
|
259
|
+
local org="$2"
|
|
260
|
+
local app="$3"
|
|
261
|
+
|
|
262
|
+
CPLN_TOKEN="${token}" cpln gvc get "${app}" --org "${org}" -o json |
|
|
263
|
+
jq -r '.spec.env // [] | .[] | .name // empty' |
|
|
264
|
+
sort -u
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
list_workload_env_names() {
|
|
268
|
+
local token="$1"
|
|
269
|
+
local org="$2"
|
|
270
|
+
local app="$3"
|
|
271
|
+
local workload="$4"
|
|
272
|
+
|
|
273
|
+
CPLN_TOKEN="${token}" cpln workload get "${workload}" --gvc "${app}" --org "${org}" -o json |
|
|
274
|
+
jq -r '.spec.containers // [] | .[] | (.env // [])[]? | .name // empty' |
|
|
275
|
+
sort -u
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
check_required_vars() {
|
|
279
|
+
local staging_scope="$1"
|
|
280
|
+
local production_scope="$2"
|
|
281
|
+
local missing_message="$3"
|
|
282
|
+
local staging_vars="$4"
|
|
283
|
+
local production_vars="$5"
|
|
284
|
+
local missing_vars
|
|
285
|
+
local production_only_vars
|
|
286
|
+
|
|
287
|
+
if [[ -z "${staging_vars}" ]]; then
|
|
288
|
+
echo "Staging ${staging_scope} exposes no environment variables; skipping parity check."
|
|
289
|
+
return
|
|
290
|
+
fi
|
|
211
291
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
292
|
+
# Treat staging as the promotion source of truth: fail when a variable
|
|
293
|
+
# present in staging is missing in production. Production-only variables
|
|
294
|
+
# are allowed, but surface them so teams can spot drift.
|
|
295
|
+
missing_vars="$(comm -23 <(printf '%s\n' "${staging_vars}") <(printf '%s\n' "${production_vars}"))"
|
|
296
|
+
production_only_vars="$(comm -13 <(printf '%s\n' "${staging_vars}") <(printf '%s\n' "${production_vars}"))"
|
|
216
297
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
production_only_vars="$(comm -13 <(printf '%s\n' "${staging_vars}") <(printf '%s\n' "${production_vars}"))"
|
|
298
|
+
if [[ -n "${production_only_vars}" ]]; then
|
|
299
|
+
echo "::warning::Production ${production_scope} has environment variables that are not present in staging:"
|
|
300
|
+
echo "${production_only_vars}"
|
|
301
|
+
fi
|
|
222
302
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
303
|
+
if [[ -n "${missing_vars}" ]]; then
|
|
304
|
+
echo "::error::${missing_message}"
|
|
305
|
+
echo "${missing_vars}"
|
|
306
|
+
env_check_failed=1
|
|
307
|
+
fi
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
# check_required_vars intentionally mutates env_check_failed in this
|
|
311
|
+
# shell; keep calls outside subshells so failures aggregate before the
|
|
312
|
+
# final exit.
|
|
313
|
+
env_check_failed=0
|
|
314
|
+
|
|
315
|
+
staging_vars="$(list_gvc_env_names "${CPLN_TOKEN_STAGING}" "${CPLN_ORG_STAGING}" "${STAGING_APP_NAME}")"
|
|
316
|
+
production_vars="$(list_gvc_env_names "${CPLN_TOKEN_PRODUCTION}" "${CPLN_ORG_PRODUCTION}" "${PRODUCTION_APP_NAME}")"
|
|
317
|
+
check_required_vars \
|
|
318
|
+
"GVC '${STAGING_APP_NAME}'" \
|
|
319
|
+
"GVC '${PRODUCTION_APP_NAME}'" \
|
|
320
|
+
"Production GVC '${PRODUCTION_APP_NAME}' is missing environment variables that exist in staging" \
|
|
321
|
+
"${staging_vars}" \
|
|
322
|
+
"${production_vars}"
|
|
227
323
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
324
|
+
while IFS= read -r workload_name; do
|
|
325
|
+
[[ -n "${workload_name}" ]] || continue
|
|
326
|
+
|
|
327
|
+
staging_workload_vars="$(list_workload_env_names "${CPLN_TOKEN_STAGING}" "${CPLN_ORG_STAGING}" "${STAGING_APP_NAME}" "${workload_name}")"
|
|
328
|
+
production_workload_vars="$(list_workload_env_names "${CPLN_TOKEN_PRODUCTION}" "${CPLN_ORG_PRODUCTION}" "${PRODUCTION_APP_NAME}" "${workload_name}")"
|
|
329
|
+
check_required_vars \
|
|
330
|
+
"workload '${workload_name}'" \
|
|
331
|
+
"workload '${workload_name}'" \
|
|
332
|
+
"Production workload '${workload_name}' is missing environment variables that exist in staging" \
|
|
333
|
+
"${staging_workload_vars}" \
|
|
334
|
+
"${production_workload_vars}"
|
|
335
|
+
done < <(tr ',' '\n' <<< "${WORKLOAD_NAMES}")
|
|
336
|
+
|
|
337
|
+
exit "${env_check_failed}"
|
|
233
338
|
|
|
234
339
|
- name: Capture current production image
|
|
235
340
|
id: capture-current
|
|
236
341
|
env:
|
|
237
342
|
PRODUCTION_APP_NAME: ${{ vars.PRODUCTION_APP_NAME }}
|
|
238
|
-
CPLN_ORG_PRODUCTION: ${{
|
|
343
|
+
CPLN_ORG_PRODUCTION: ${{ steps.cpln-orgs.outputs.production }}
|
|
239
344
|
WORKLOAD_NAMES: ${{ steps.workloads.outputs.names }}
|
|
240
345
|
PRIMARY_WORKLOAD: ${{ steps.workloads.outputs.primary }}
|
|
241
346
|
shell: bash
|
|
@@ -293,7 +398,7 @@ jobs:
|
|
|
293
398
|
env:
|
|
294
399
|
CPLN_TOKEN_STAGING: ${{ secrets.CPLN_TOKEN_STAGING }}
|
|
295
400
|
STAGING_APP_NAME: ${{ vars.STAGING_APP_NAME }}
|
|
296
|
-
CPLN_ORG_STAGING: ${{
|
|
401
|
+
CPLN_ORG_STAGING: ${{ steps.cpln-orgs.outputs.staging }}
|
|
297
402
|
WORKLOAD_NAMES: ${{ steps.workloads.outputs.names }}
|
|
298
403
|
PRIMARY_WORKLOAD: ${{ steps.workloads.outputs.primary }}
|
|
299
404
|
shell: bash
|
|
@@ -335,14 +440,17 @@ jobs:
|
|
|
335
440
|
|
|
336
441
|
echo "image=${staging_image}" >> "$GITHUB_OUTPUT"
|
|
337
442
|
|
|
443
|
+
- name: Set up Docker Buildx
|
|
444
|
+
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5
|
|
445
|
+
|
|
338
446
|
- name: Copy image from staging
|
|
447
|
+
id: copy-image
|
|
339
448
|
env:
|
|
340
|
-
# Pass the upstream token via env rather than `-t` so it doesn't appear in /proc/<pid>/cmdline.
|
|
341
449
|
CPLN_TOKEN_STAGING: ${{ secrets.CPLN_TOKEN_STAGING }}
|
|
342
|
-
|
|
450
|
+
CPLN_TOKEN_PRODUCTION: ${{ secrets.CPLN_TOKEN_PRODUCTION }}
|
|
343
451
|
PRODUCTION_APP_NAME: ${{ vars.PRODUCTION_APP_NAME }}
|
|
344
|
-
CPLN_ORG_STAGING: ${{
|
|
345
|
-
CPLN_ORG_PRODUCTION: ${{
|
|
452
|
+
CPLN_ORG_STAGING: ${{ steps.cpln-orgs.outputs.staging }}
|
|
453
|
+
CPLN_ORG_PRODUCTION: ${{ steps.cpln-orgs.outputs.production }}
|
|
346
454
|
STAGING_IMAGE: ${{ steps.staging-image.outputs.image }}
|
|
347
455
|
shell: bash
|
|
348
456
|
run: |
|
|
@@ -362,14 +470,82 @@ jobs:
|
|
|
362
470
|
copy_image_attempts=$((copy_image_retries + 1))
|
|
363
471
|
copy_image_retry_interval=$((10#${COPY_IMAGE_RETRY_INTERVAL}))
|
|
364
472
|
|
|
365
|
-
|
|
473
|
+
staging_image="${STAGING_IMAGE}"
|
|
474
|
+
if [[ -z "${staging_image}" ]]; then
|
|
475
|
+
echo "::error::STAGING_IMAGE is not set or is empty."
|
|
476
|
+
exit 1
|
|
477
|
+
fi
|
|
478
|
+
|
|
479
|
+
if ! CPLN_TOKEN="${CPLN_TOKEN_STAGING}" cpln image get "${staging_image}" --org "${CPLN_ORG_STAGING}" -o json >/dev/null; then
|
|
366
480
|
echo "::error::Staging image '${STAGING_IMAGE}' was not found in org '${CPLN_ORG_STAGING}'; aborting promotion."
|
|
367
481
|
exit 1
|
|
368
482
|
fi
|
|
369
483
|
|
|
484
|
+
staging_tag=""
|
|
485
|
+
if [[ "${staging_image}" == *@* ]]; then
|
|
486
|
+
staging_tag="${staging_image##*@}"
|
|
487
|
+
elif [[ "${staging_image}" == *:* ]]; then
|
|
488
|
+
staging_tag="${staging_image##*:}"
|
|
489
|
+
fi
|
|
490
|
+
staging_commit=""
|
|
491
|
+
if [[ "${staging_tag}" == *_* ]]; then
|
|
492
|
+
staging_commit="${staging_tag##*_}"
|
|
493
|
+
else
|
|
494
|
+
echo "::warning::Staging image '${staging_image}' did not include a '_<commit>' suffix; production image tag will omit the commit suffix."
|
|
495
|
+
fi
|
|
496
|
+
|
|
497
|
+
# The workflow-level concurrency group serializes this sequence so two
|
|
498
|
+
# production promotions cannot derive and publish the same next tag.
|
|
499
|
+
# See the top-level concurrency group: cpflow-promote-staging-to-production.
|
|
500
|
+
latest_number="$(
|
|
501
|
+
cpln image query --org "${CPLN_ORG_PRODUCTION}" --prop "name~${PRODUCTION_APP_NAME}:" --max 0 -o json |
|
|
502
|
+
jq -r --arg prefix "${PRODUCTION_APP_NAME}:" \
|
|
503
|
+
'[.items[].name | select(startswith($prefix)) | (try capture("^[^:]+:(?<number>[0-9]+)") catch empty) | .number | tonumber] | max // 0'
|
|
504
|
+
)"
|
|
505
|
+
if ! [[ "${latest_number}" =~ ^[0-9]+$ ]]; then
|
|
506
|
+
echo "::error::Could not determine the next production image number for app '${PRODUCTION_APP_NAME}' in org '${CPLN_ORG_PRODUCTION}'."
|
|
507
|
+
exit 1
|
|
508
|
+
fi
|
|
509
|
+
|
|
510
|
+
production_image="${PRODUCTION_APP_NAME}:$((latest_number + 1))"
|
|
511
|
+
if [[ -n "${staging_commit}" ]]; then
|
|
512
|
+
production_image="${production_image}_${staging_commit}"
|
|
513
|
+
fi
|
|
514
|
+
|
|
515
|
+
staging_registry="${CPLN_ORG_STAGING}.registry.cpln.io"
|
|
516
|
+
production_registry="${CPLN_ORG_PRODUCTION}.registry.cpln.io"
|
|
517
|
+
source_image_ref="${staging_registry}/${STAGING_IMAGE}"
|
|
518
|
+
production_image_ref="${production_registry}/${production_image}"
|
|
519
|
+
|
|
520
|
+
docker_config_dir="$(mktemp -d)"
|
|
521
|
+
cleanup_copy_credentials() {
|
|
522
|
+
rm -rf "${docker_config_dir}"
|
|
523
|
+
}
|
|
524
|
+
trap cleanup_copy_credentials EXIT
|
|
525
|
+
|
|
526
|
+
export DOCKER_CONFIG="${docker_config_dir}"
|
|
527
|
+
|
|
528
|
+
if ! printf '%s' "${CPLN_TOKEN_STAGING}" |
|
|
529
|
+
docker login "${staging_registry}" -u '<token>' --password-stdin >/dev/null; then
|
|
530
|
+
echo "::error::Failed to authenticate to staging registry '${staging_registry}'."
|
|
531
|
+
exit 1
|
|
532
|
+
fi
|
|
533
|
+
|
|
534
|
+
if ! printf '%s' "${CPLN_TOKEN_PRODUCTION}" |
|
|
535
|
+
docker login "${production_registry}" -u '<token>' --password-stdin >/dev/null; then
|
|
536
|
+
echo "::error::Failed to authenticate to production registry '${production_registry}'."
|
|
537
|
+
exit 1
|
|
538
|
+
fi
|
|
539
|
+
|
|
540
|
+
if docker buildx imagetools inspect "${production_image_ref}" >/dev/null 2>&1; then
|
|
541
|
+
echo "::error::Production image '${production_image}' already exists in org '${CPLN_ORG_PRODUCTION}'; aborting to avoid overwriting it."
|
|
542
|
+
exit 1
|
|
543
|
+
fi
|
|
544
|
+
|
|
370
545
|
copy_status=1
|
|
371
546
|
for attempt in $(seq 1 "${copy_image_attempts}"); do
|
|
372
|
-
if
|
|
547
|
+
if docker buildx imagetools inspect "${source_image_ref}" >/dev/null &&
|
|
548
|
+
docker buildx imagetools create --prefer-index=false --tag "${production_image_ref}" "${source_image_ref}"; then
|
|
373
549
|
copy_status=0
|
|
374
550
|
break
|
|
375
551
|
else
|
|
@@ -389,10 +565,12 @@ jobs:
|
|
|
389
565
|
exit "${copy_status}"
|
|
390
566
|
fi
|
|
391
567
|
|
|
568
|
+
echo "image=${production_image}" >> "$GITHUB_OUTPUT"
|
|
569
|
+
|
|
392
570
|
- name: Deploy image to production
|
|
393
571
|
env:
|
|
394
572
|
PRODUCTION_APP_NAME: ${{ vars.PRODUCTION_APP_NAME }}
|
|
395
|
-
CPLN_ORG_PRODUCTION: ${{
|
|
573
|
+
CPLN_ORG_PRODUCTION: ${{ steps.cpln-orgs.outputs.production }}
|
|
396
574
|
RELEASE_PHASE_FLAG: ${{ steps.release-phase.outputs.flag }}
|
|
397
575
|
shell: bash
|
|
398
576
|
run: |
|
|
@@ -402,6 +580,9 @@ jobs:
|
|
|
402
580
|
if [[ -n "${RELEASE_PHASE_FLAG}" ]]; then
|
|
403
581
|
deploy_args+=("${RELEASE_PHASE_FLAG}")
|
|
404
582
|
fi
|
|
583
|
+
# `cpflow deploy-image` deploys the latest image for the app. The
|
|
584
|
+
# workflow-level concurrency group keeps production promotion copy and
|
|
585
|
+
# deploy steps coupled across workflow runs.
|
|
405
586
|
deploy_args+=(--org "${CPLN_ORG_PRODUCTION}" --verbose)
|
|
406
587
|
|
|
407
588
|
cpflow deploy-image "${deploy_args[@]}"
|
|
@@ -412,7 +593,7 @@ jobs:
|
|
|
412
593
|
with:
|
|
413
594
|
workload_name: ${{ steps.workloads.outputs.primary }}
|
|
414
595
|
app_name: ${{ vars.PRODUCTION_APP_NAME }}
|
|
415
|
-
org: ${{
|
|
596
|
+
org: ${{ steps.cpln-orgs.outputs.production }}
|
|
416
597
|
max_retries: ${{ env.HEALTH_CHECK_RETRIES }}
|
|
417
598
|
interval_seconds: ${{ env.HEALTH_CHECK_INTERVAL }}
|
|
418
599
|
accepted_statuses: ${{ env.HEALTH_CHECK_ACCEPTED_STATUSES }}
|
|
@@ -422,7 +603,7 @@ jobs:
|
|
|
422
603
|
env:
|
|
423
604
|
ROLLBACK_STATE: ${{ steps.capture-current.outputs.rollback_state }}
|
|
424
605
|
PRODUCTION_APP_NAME: ${{ vars.PRODUCTION_APP_NAME }}
|
|
425
|
-
CPLN_ORG_PRODUCTION: ${{
|
|
606
|
+
CPLN_ORG_PRODUCTION: ${{ steps.cpln-orgs.outputs.production }}
|
|
426
607
|
shell: bash
|
|
427
608
|
run: |
|
|
428
609
|
# Best-effort rollback: try every workload, aggregate failures, exit non-zero at the end
|
|
@@ -484,7 +665,7 @@ jobs:
|
|
|
484
665
|
env:
|
|
485
666
|
ROLLBACK_STATE: ${{ steps.capture-current.outputs.rollback_state }}
|
|
486
667
|
PRODUCTION_APP_NAME: ${{ vars.PRODUCTION_APP_NAME }}
|
|
487
|
-
CPLN_ORG_PRODUCTION: ${{
|
|
668
|
+
CPLN_ORG_PRODUCTION: ${{ steps.cpln-orgs.outputs.production }}
|
|
488
669
|
shell: bash
|
|
489
670
|
run: |
|
|
490
671
|
set -euo pipefail
|
|
@@ -510,8 +691,10 @@ jobs:
|
|
|
510
691
|
set -euo pipefail
|
|
511
692
|
ready=false
|
|
512
693
|
for attempt in $(seq 1 "${ROLLBACK_READINESS_RETRIES}"); do
|
|
513
|
-
|
|
514
|
-
|
|
694
|
+
workload_status="$(cpln workload get "${workload_name}" --gvc "${PRODUCTION_APP_NAME}" --org "${CPLN_ORG_PRODUCTION}" -o json)"
|
|
695
|
+
deployment_ready="$(echo "${workload_status}" | jq -r '.status.ready // false')"
|
|
696
|
+
latest_ready="$(echo "${workload_status}" | jq -r '.status.readyLatest // false')"
|
|
697
|
+
if [[ "${deployment_ready}" == "true" && "${latest_ready}" == "true" ]]; then
|
|
515
698
|
ready=true
|
|
516
699
|
break
|
|
517
700
|
fi
|
|
@@ -553,7 +736,7 @@ jobs:
|
|
|
553
736
|
HEALTHY: ${{ steps.health-check.outputs.healthy }}
|
|
554
737
|
PREVIOUS_IMAGE: ${{ steps.capture-current.outputs.current_image }}
|
|
555
738
|
PREVIOUS_VERSION: ${{ steps.capture-current.outputs.current_version }}
|
|
556
|
-
|
|
739
|
+
COPIED_IMAGE: ${{ steps.copy-image.outputs.image }}
|
|
557
740
|
shell: bash
|
|
558
741
|
run: |
|
|
559
742
|
{
|
|
@@ -561,13 +744,15 @@ jobs:
|
|
|
561
744
|
echo
|
|
562
745
|
if [[ "${HEALTHY}" == "true" ]]; then
|
|
563
746
|
echo "✅ Status: deployment successful"
|
|
747
|
+
deployed_image="${COPIED_IMAGE}"
|
|
564
748
|
else
|
|
565
749
|
echo "❌ Status: deployment failed"
|
|
750
|
+
deployed_image="${PREVIOUS_IMAGE}"
|
|
566
751
|
fi
|
|
567
752
|
echo
|
|
568
753
|
echo "Previous image: \`${PREVIOUS_IMAGE}\`"
|
|
569
754
|
echo "Previous version: ${PREVIOUS_VERSION}"
|
|
570
|
-
echo "Deployed image: \`${
|
|
755
|
+
echo "Deployed image: \`${deployed_image}\`"
|
|
571
756
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
572
757
|
|
|
573
758
|
create-github-release:
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: cpflow
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.
|
|
4
|
+
version: 5.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Justin Gordon
|
|
@@ -75,7 +75,16 @@ executables:
|
|
|
75
75
|
extensions: []
|
|
76
76
|
extra_rdoc_files: []
|
|
77
77
|
files:
|
|
78
|
+
- ".agents/agent-workflow.yml"
|
|
79
|
+
- ".agents/bin/README.md"
|
|
80
|
+
- ".agents/bin/docs"
|
|
81
|
+
- ".agents/bin/lint"
|
|
82
|
+
- ".agents/bin/setup"
|
|
83
|
+
- ".agents/bin/test"
|
|
84
|
+
- ".agents/bin/validate"
|
|
85
|
+
- ".agents/trusted-github-actors.yml"
|
|
78
86
|
- ".agents/workflows/address-review.md"
|
|
87
|
+
- ".agents/workflows/ai-rollout-e2e-test.md"
|
|
79
88
|
- ".claude/commands/address-review.md"
|
|
80
89
|
- ".claude/commands/update-changelog.md"
|
|
81
90
|
- ".github/actions/cpflow-build-docker-image/action.yml"
|
|
@@ -106,7 +115,9 @@ files:
|
|
|
106
115
|
- ".overcommit.yml"
|
|
107
116
|
- ".rubocop.yml"
|
|
108
117
|
- ".simplecov_spawn.rb"
|
|
118
|
+
- AGENTS.md
|
|
109
119
|
- CHANGELOG.md
|
|
120
|
+
- CLAUDE.md
|
|
110
121
|
- COMM-LICENSE.txt
|
|
111
122
|
- CONTRIBUTING.md
|
|
112
123
|
- Gemfile
|
|
@@ -120,17 +131,37 @@ files:
|
|
|
120
131
|
- docs/ai-github-flow-prompt.md
|
|
121
132
|
- docs/assets/cpflow-deploying.svg
|
|
122
133
|
- docs/assets/grafana-alert.png
|
|
134
|
+
- docs/assets/logo/favicon.ico
|
|
135
|
+
- docs/assets/logo/icon-1024.png
|
|
136
|
+
- docs/assets/logo/icon-128.png
|
|
137
|
+
- docs/assets/logo/icon-16.png
|
|
138
|
+
- docs/assets/logo/icon-192.png
|
|
139
|
+
- docs/assets/logo/icon-24.png
|
|
140
|
+
- docs/assets/logo/icon-32.png
|
|
141
|
+
- docs/assets/logo/icon-48.png
|
|
142
|
+
- docs/assets/logo/icon-512.png
|
|
143
|
+
- docs/assets/logo/icon-64.png
|
|
144
|
+
- docs/assets/logo/icon-tile.svg
|
|
145
|
+
- docs/assets/logo/mark-transparent.svg
|
|
123
146
|
- docs/assets/memcached.png
|
|
124
147
|
- docs/assets/sidekiq-pre-stop-hook.png
|
|
125
148
|
- docs/ci-automation.md
|
|
126
149
|
- docs/commands.md
|
|
127
150
|
- docs/dns.md
|
|
151
|
+
- docs/grafana-opentelemetry.md
|
|
128
152
|
- docs/migrating-heroku-to-control-plane.md
|
|
129
153
|
- docs/postgres.md
|
|
130
154
|
- docs/rds-private-networking.md
|
|
131
155
|
- docs/redis.md
|
|
132
156
|
- docs/releasing.md
|
|
133
157
|
- docs/secrets-and-env-values.md
|
|
158
|
+
- docs/sidebars.ts
|
|
159
|
+
- docs/telemetry/application-instrumentation.md
|
|
160
|
+
- docs/telemetry/collector.md
|
|
161
|
+
- docs/telemetry/index.md
|
|
162
|
+
- docs/telemetry/pipelines.md
|
|
163
|
+
- docs/telemetry/review-apps.md
|
|
164
|
+
- docs/telemetry/troubleshooting.md
|
|
134
165
|
- docs/terraform/details.md
|
|
135
166
|
- docs/terraform/example/.controlplane/controlplane.yml
|
|
136
167
|
- docs/terraform/example/.controlplane/templates/app.yml
|
|
@@ -272,7 +303,7 @@ licenses:
|
|
|
272
303
|
metadata:
|
|
273
304
|
rubygems_mfa_required: 'true'
|
|
274
305
|
post_install_message: |
|
|
275
|
-
cpflow 5.
|
|
306
|
+
cpflow 5.2.0 installed.
|
|
276
307
|
|
|
277
308
|
If this repository already uses generated cpflow GitHub Actions, update the
|
|
278
309
|
checked-in wrappers so GitHub loads the matching control-plane-flow release tag:
|