cpflow 5.0.4 → 5.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 51e0566b72525c5e975b384c12930f95ad5f13faaf1691ccbcca27b0c50a13b4
4
- data.tar.gz: 91560ebafb43692488b8996ef5e05799a5621bc6066329a71636b3677d1c63e5
3
+ metadata.gz: 44479e287fa1f7366a4df86ee7f68176c00f2b6cd2fe59c786e5de6f1143d2b3
4
+ data.tar.gz: acbf5149907b43cc628d2af8c19572d0ffb5de6d1bae630cb98c14c5ca46d4cc
5
5
  SHA512:
6
- metadata.gz: 89ec31dcc8d5b6b53ee62246ec8b514513101466c2f3751297583a540fad6ac99b4579ff6d13dbd332080313e3c8fb25df527b55d69a3e1037ce35a0625f98a4
7
- data.tar.gz: 2f3c85b15e050142703328aa6ef5b24bfd5c6ca32d386c6c2918dee0d1ee4b1a34092980781195f65ca59bacc095d57df24a85a26a5be5e719fd79d6cb205748
6
+ metadata.gz: 1bf64792213761b8e5af2f44bf18c90c32c2ceeef36c23082afbc34def70d7059bb36747dc696e7bb4236538f3670dc177c9d2b06de68459d1e80634604e088a
7
+ data.tar.gz: 3c789100969a47e6d7b29efa75fb1b99b799074b89e8cf276dbdf505bab36adec20e010700366e36fc12063426ca80cacf58db3f17253f3dbc241a6533485104
@@ -39,6 +39,8 @@ env:
39
39
  # expose a dedicated health endpoint (e.g. "200" for a plain /health, or "200 401 403"
40
40
  # for apps that auth-gate / without redirecting).
41
41
  HEALTH_CHECK_ACCEPTED_STATUSES: ${{ vars.HEALTH_CHECK_ACCEPTED_STATUSES || '200 301 302' }}
42
+ COPY_IMAGE_RETRIES: ${{ vars.COPY_IMAGE_RETRIES || '3' }}
43
+ COPY_IMAGE_RETRY_INTERVAL: ${{ vars.COPY_IMAGE_RETRY_INTERVAL || '20' }}
42
44
  ROLLBACK_READINESS_RETRIES: 24
43
45
  ROLLBACK_READINESS_INTERVAL: 15
44
46
 
@@ -317,14 +319,56 @@ jobs:
317
319
  - name: Copy image from staging
318
320
  env:
319
321
  # Pass the upstream token via env rather than `-t` so it doesn't appear in /proc/<pid>/cmdline.
322
+ CPLN_TOKEN_STAGING: ${{ secrets.CPLN_TOKEN_STAGING }}
320
323
  CPLN_UPSTREAM_TOKEN: ${{ secrets.CPLN_TOKEN_STAGING }}
321
324
  PRODUCTION_APP_NAME: ${{ vars.PRODUCTION_APP_NAME }}
325
+ CPLN_ORG_STAGING: ${{ vars.CPLN_ORG_STAGING }}
322
326
  CPLN_ORG_PRODUCTION: ${{ vars.CPLN_ORG_PRODUCTION }}
323
327
  STAGING_IMAGE: ${{ steps.staging-image.outputs.image }}
324
328
  shell: bash
325
329
  run: |
326
330
  set -euo pipefail
327
- cpflow copy-image-from-upstream -a "${PRODUCTION_APP_NAME}" --org "${CPLN_ORG_PRODUCTION}" --image "${STAGING_IMAGE}"
331
+
332
+ if ! [[ "${COPY_IMAGE_RETRIES}" =~ ^[0-9]+$ ]]; then
333
+ echo "::error::COPY_IMAGE_RETRIES must be a non-negative integer."
334
+ exit 1
335
+ fi
336
+
337
+ if ! [[ "${COPY_IMAGE_RETRY_INTERVAL}" =~ ^[0-9]+$ ]]; then
338
+ echo "::error::COPY_IMAGE_RETRY_INTERVAL must be a non-negative integer."
339
+ exit 1
340
+ fi
341
+
342
+ copy_image_retries=$((10#${COPY_IMAGE_RETRIES}))
343
+ copy_image_attempts=$((copy_image_retries + 1))
344
+ copy_image_retry_interval=$((10#${COPY_IMAGE_RETRY_INTERVAL}))
345
+
346
+ if ! CPLN_TOKEN="${CPLN_TOKEN_STAGING}" cpln image get "${STAGING_IMAGE}" --org "${CPLN_ORG_STAGING}" -o json >/dev/null; then
347
+ echo "::error::Staging image '${STAGING_IMAGE}' was not found in org '${CPLN_ORG_STAGING}'; aborting promotion."
348
+ exit 1
349
+ fi
350
+
351
+ copy_status=1
352
+ for attempt in $(seq 1 "${copy_image_attempts}"); do
353
+ if cpflow copy-image-from-upstream -a "${PRODUCTION_APP_NAME}" --org "${CPLN_ORG_PRODUCTION}" --image "${STAGING_IMAGE}"; then
354
+ copy_status=0
355
+ break
356
+ else
357
+ copy_status=$?
358
+ fi
359
+
360
+ if [[ "${attempt}" -lt "${copy_image_attempts}" ]]; then
361
+ echo "::warning::Image copy attempt ${attempt}/${copy_image_attempts} failed with exit ${copy_status}; retrying in ${copy_image_retry_interval}s."
362
+ sleep "${copy_image_retry_interval}"
363
+ else
364
+ echo "::warning::Image copy attempt ${attempt}/${copy_image_attempts} failed with exit ${copy_status}; no attempts remain."
365
+ fi
366
+ done
367
+
368
+ if [[ "${copy_status}" -ne 0 ]]; then
369
+ echo "::error::Could not copy staging image '${STAGING_IMAGE}' from '${CPLN_ORG_STAGING}' to '${CPLN_ORG_PRODUCTION}' after ${copy_image_attempts} attempt(s)."
370
+ exit "${copy_status}"
371
+ fi
328
372
 
329
373
  - name: Deploy image to production
330
374
  env:
@@ -391,19 +435,14 @@ jobs:
391
435
  continue
392
436
  fi
393
437
 
394
- if ! rollback_container_entries="$(
395
- jq -r \
396
- --argjson current_names "${current_names}" \
397
- '.[] as $container | ($current_names | index($container.name)) as $index | "\($index)\t\($container.image)"' \
398
- <<< "${previous_containers}"
399
- )"; then
438
+ if ! rollback_container_entries="$(jq -r '.[] | "\(.name)\t\(.image)"' <<< "${previous_containers}")"; then
400
439
  echo "::warning::Could not build rollback image list for workload '${workload_name}'; skipping rollback for this workload." >&2
401
440
  rollback_failures=$((rollback_failures + 1))
402
441
  continue
403
442
  fi
404
443
 
405
- while IFS=$'\t' read -r index image; do
406
- rollback_args+=(--set "spec.containers[${index}].image=${image}")
444
+ while IFS=$'\t' read -r container_name image; do
445
+ rollback_args+=(--set "spec.containers.${container_name}.image=${image}")
407
446
  done <<< "${rollback_container_entries}"
408
447
 
409
448
  if ! cpln workload update "${workload_name}" \
data/CHANGELOG.md CHANGED
@@ -12,6 +12,18 @@ In addition to the standard keepachangelog.com categories, this project uses a l
12
12
 
13
13
  ## [Unreleased]
14
14
 
15
+ ## [5.1.0] - 2026-06-02
16
+
17
+ ### Added
18
+
19
+ - **Added `shared_secret_grants` configuration so apps can reference org-level Control Plane secrets by name instead of hardcoding them in templates.** [PR 354](https://github.com/shakacode/control-plane-flow/pull/354) by [Justin Gordon](https://github.com/justin808). Each grant validates a unique placeholder, a safe Control Plane resource name, and a secret policy that targets exactly that secret; templates gain `{{SHARED_SECRET_<NAME>}}` substitution, and the shared-policy lifecycle is wired through `setup-app`, `deploy-image`, `delete`, and `cleanup-stale-apps`. Enables the shared staging-database pattern for cheaper review apps.
20
+
21
+ ### Fixed
22
+
23
+ - **Fixed `cpflow generate-github-actions` so the generated `.github/cpflow-help.md` version-locking example derives a `CPFLOW_VERSION=<major>.<minor>.x` placeholder from the installed gem version instead of a hardcoded release that goes stale against the `@v<version>` wrapper refs in the same file.** [PR 343](https://github.com/shakacode/control-plane-flow/pull/343) by [Justin Gordon](https://github.com/justin808). Fixes [issue 341](https://github.com/shakacode/control-plane-flow/issues/341).
24
+ - **Fixed generated production promotion so `cpflow-promote-staging-to-production.yml` runs as a caller-owned job with `environment: production`, letting GitHub inject the `CPLN_TOKEN_PRODUCTION` environment secret after the protected gate instead of failing because a cross-repo reusable workflow cannot receive caller environment secrets.** [PR 353](https://github.com/shakacode/control-plane-flow/pull/353) by [Justin Gordon](https://github.com/justin808). The job checks out the pinned `control-plane-flow` ref into `.cpflow`, and generated help plus `docs/ci-automation.md` now explain why a same-named repository or organization secret can mask a missing environment secret.
25
+ - **Hardened generated production promotion image copy to preflight the staging image, retry the copy via configurable `COPY_IMAGE_RETRIES` and `COPY_IMAGE_RETRY_INTERVAL` repo vars, and roll back failed deploys using `spec.containers.<name>.image` paths instead of unsupported array-index paths.** [PR 355](https://github.com/shakacode/control-plane-flow/pull/355) by [Justin Gordon](https://github.com/justin808).
26
+
15
27
  ## [5.0.4] - 2026-05-27
16
28
 
17
29
  ### Fixed
@@ -398,7 +410,8 @@ Deprecated `cpl` gem. New gem is `cpflow`.
398
410
 
399
411
  First release.
400
412
 
401
- [Unreleased]: https://github.com/shakacode/control-plane-flow/compare/v5.0.4...HEAD
413
+ [Unreleased]: https://github.com/shakacode/control-plane-flow/compare/v5.1.0...HEAD
414
+ [5.1.0]: https://github.com/shakacode/control-plane-flow/compare/v5.0.4...v5.1.0
402
415
  [5.0.4]: https://github.com/shakacode/control-plane-flow/compare/v5.0.3...v5.0.4
403
416
  [5.0.3]: https://github.com/shakacode/control-plane-flow/compare/v5.0.2...v5.0.3
404
417
  [5.0.2]: https://github.com/shakacode/control-plane-flow/compare/v5.0.1...v5.0.2
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- cpflow (5.0.4)
4
+ cpflow (5.1.0)
5
5
  dotenv (~> 3.1)
6
6
  jwt (~> 3.1)
7
7
  psych (~> 5.2)
data/README.md CHANGED
@@ -241,6 +241,18 @@ aliases:
241
241
  # it would be 'my-app-review-secrets-policy'
242
242
  secrets_policy_name: my-secrets-policy
243
243
 
244
+ # Optional: grant each app identity access to shared org-level secrets
245
+ # without hardcoding shared secret names in workload templates.
246
+ #
247
+ # This is useful for review apps that share one staging database secret
248
+ # instead of provisioning a database per PR. Create the shared secret and
249
+ # policy once, then reference the secret in templates with
250
+ # {{SHARED_SECRET_DATABASE}}.
251
+ # shared_secret_grants:
252
+ # - name: database
253
+ # secret_name: my-shared-database-secrets
254
+ # policy_name: my-shared-database-secrets-policy
255
+
244
256
  # Configure the workload name used as a template for one-off scripts, like a Heroku one-off dyno.
245
257
  one_off_workload: rails
246
258
 
@@ -500,6 +512,11 @@ aws-rds-single-pg-instance
500
512
  mydb-review-333
501
513
  ```
502
514
 
515
+ For production, you'll typically want RDS or Aurora in private subnets, reached from your Control Plane workloads
516
+ over a private network path rather than the public internet. See
517
+ [Connecting Control Plane workloads to a private AWS RDS/Aurora database](./docs/rds-private-networking.md) for the
518
+ full Cloud Wormhole + Agent setup.
519
+
503
520
  If you want to run PostgreSQL on Control Plane instead of keeping a Heroku add-on or moving to RDS, review the
504
521
  [Control Plane PostgreSQL Template Catalog page](https://shakadocs.controlplane.com/template-catalog/templates/postgres). It includes
505
522
  persistent storage and optional scheduled backups. Additionally, we provide a default `postgres` template in this
@@ -565,17 +582,21 @@ cpflow --help
565
582
 
566
583
  ## Mapping of Heroku Commands to `cpflow` and `cpln`
567
584
 
568
- | Heroku Command | `cpflow` or `cpln` |
569
- | -------------------------------------------------------------------------------------------------------------- | ------------------------------- |
570
- | [heroku ps](https://devcenter.heroku.com/articles/heroku-cli-commands#heroku-ps-type-type) | `cpflow ps` |
571
- | [heroku config](https://devcenter.heroku.com/articles/heroku-cli-commands#heroku-config) | ? |
572
- | [heroku maintenance](https://devcenter.heroku.com/articles/heroku-cli-commands#heroku-maintenance) | `cpflow maintenance` |
573
- | [heroku logs](https://devcenter.heroku.com/articles/heroku-cli-commands#heroku-logs) | `cpflow logs` |
574
- | [heroku pg](https://devcenter.heroku.com/articles/heroku-cli-commands#heroku-pg-database) | ? |
575
- | [heroku pipelines:promote](https://devcenter.heroku.com/articles/heroku-cli-commands#heroku-pipelines-promote) | `cpflow promote-app-from-upstream` |
576
- | [heroku psql](https://devcenter.heroku.com/articles/heroku-cli-commands#heroku-psql-database) | ? |
577
- | [heroku redis](https://devcenter.heroku.com/articles/heroku-cli-commands#heroku-redis-database) | ? |
578
- | [heroku releases](https://devcenter.heroku.com/articles/heroku-cli-commands#heroku-releases) | ? |
585
+ | Heroku Command | `cpflow` or `cpln` |
586
+ | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
587
+ | [heroku ps](https://devcenter.heroku.com/articles/heroku-cli-commands#heroku-ps-type-type) | `cpflow ps` |
588
+ | [heroku config](https://devcenter.heroku.com/articles/heroku-cli-commands#heroku-config) | `cpflow env -a APP_NAME` displays Control Plane app environment variables; `cpflow config -a APP_NAME` displays local `.controlplane/controlplane.yml` settings |
589
+ | [heroku maintenance](https://devcenter.heroku.com/articles/heroku-cli-commands#heroku-maintenance) | `cpflow maintenance`, `cpflow maintenance:on`, `cpflow maintenance:off`, and `cpflow maintenance:set-page` |
590
+ | [heroku logs](https://devcenter.heroku.com/articles/heroku-cli-commands#heroku-logs) | `cpflow logs -a APP_NAME`; add `-w WORKLOAD_NAME` to filter by workload, or `-w WORKLOAD_NAME -r REPLICA_NAME` to narrow to a specific replica |
591
+ | [heroku pg](https://devcenter.heroku.com/articles/heroku-cli-commands#heroku-pg-database) | No direct `cpflow` add-on wrapper. Use an external Postgres provider, the Control Plane Template Catalog, or project templates such as `.controlplane/templates/postgres.yml`. |
592
+ | [heroku pipelines:promote](https://devcenter.heroku.com/articles/heroku-cli-commands#heroku-pipelines-promote) | `cpflow promote-app-from-upstream` |
593
+ | [heroku psql](https://devcenter.heroku.com/articles/heroku-cli-commands#heroku-psql-database) | No direct `cpflow` equivalent. Connect with your provider's `psql` flow, or run `cpflow run -a APP_NAME -- psql "$DATABASE_URL"` when `psql` is available in the application image. |
594
+ | [heroku redis](https://devcenter.heroku.com/articles/heroku-cli-commands#heroku-redis-database) | No direct `cpflow` add-on wrapper. Use an external Redis provider, the Control Plane Template Catalog, or project templates such as `.controlplane/templates/redis.yml`. |
595
+ | [heroku releases](https://devcenter.heroku.com/articles/heroku-cli-commands#heroku-releases) | `cpflow latest-image -a APP_NAME` for the latest image tag; `cpflow deploy-image -a APP_NAME` deploys that image. No `cpflow` equivalent for browsing full release history. |
596
+
597
+ Unlike Heroku add-ons, Control Plane database and cache services are usually managed as provider resources or workload
598
+ templates. `cpflow` focuses on the application deployment flow and leaves provider-specific database administration to
599
+ the provider tooling, Control Plane templates, or direct `cpln` operations.
579
600
 
580
601
  ## Examples
581
602
 
@@ -20,7 +20,7 @@ prompt tells the agent to stop on.
20
20
  ```text
21
21
  Set up Control Plane GitHub Flow for this repo. Start with `cpflow github-flow-readiness` and stop on any reported blockers. The repo must be deployable from a clean clone: published package versions, complete runtime scaffold, and a production Dockerfile that can build the app. If any package version is unpublished, inaccessible from CI, or requires credentials that are not already modeled in the repo or GitHub settings, stop and report the blocker instead of generating workflow files. If the repo is a legacy sample pinned to an obsolete Ruby or Bundler toolchain, if it does not even have a production Dockerfile yet, or if it is a monorepo without an already-decided single app boundary for this flow, stop and report that as a prerequisite instead of forcing the rollout.
22
22
 
23
- If `.controlplane/` is missing, run `cpflow generate`. Treat the generated app names as the repo-name default and rename them only if the project needs a different prefix. Then run `cpflow generate-github-actions` (or `cpflow generate-github-actions --staging-branch BRANCH` when staging should deploy from a branch other than `main`/`master`), keep review apps opt-in via `+review-app-deploy`, make sure any `STAGING_APP_BRANCH` repository variable is also present in the generated staging workflow's `on.push.branches` filter, and list the GitHub secrets and variables that must be configured. Do not hand-edit duplicated upstream refs into the generated wrappers: the only downstream Control Plane Flow pin should be the reusable workflow `uses: ...@vX.Y.Z` value generated from the installed `cpflow` gem version, and upstream workflows load their matching shared actions automatically. When bumping the `cpflow` gem in a downstream repo, run `cpflow update-github-actions` (or `bundle exec cpflow update-github-actions`) and validate with `bin/test-cpflow-github-flow` in the same PR so the checked-in wrappers move to the matching release tag. Keep the standard path simple: review apps require only `CPLN_TOKEN_STAGING` when the generated review app config can be inferred. Document the one-time Control Plane bootstrap command for persistent staging and production apps with `cpflow setup-app --skip-post-creation-hook`; for existing apps or later template updates, document `cpflow apply-template` and the need for the app identity to have `reveal` on the app secret policy. Do not imply the staging deploy or promotion workflows create those persistent GVCs. For production promotion, document a protected `production` GitHub Environment with required reviewers, prevent self-review, and `CPLN_TOKEN_PRODUCTION` stored as an environment secret, not as a repository or organization secret.
23
+ If `.controlplane/` is missing, run `cpflow generate`. Treat the generated app names as the repo-name default and rename them only if the project needs a different prefix. Then run `cpflow generate-github-actions` (or `cpflow generate-github-actions --staging-branch BRANCH` when staging should deploy from a branch other than `main`/`master`), keep review apps opt-in via `+review-app-deploy`, make sure any `STAGING_APP_BRANCH` repository variable is also present in the generated staging workflow's `on.push.branches` filter, and list the GitHub secrets and variables that must be configured. Do not hand-edit duplicated upstream refs into the generated wrappers: the only downstream Control Plane Flow pin should be the reusable workflow `uses: ...@vX.Y.Z` value generated from the installed `cpflow` gem version, and upstream workflows load their matching shared actions automatically. When bumping the `cpflow` gem in a downstream repo, run `cpflow update-github-actions` (or `bundle exec cpflow update-github-actions`) and validate with `bin/test-cpflow-github-flow` in the same PR so the checked-in wrappers move to the matching release tag. Keep the standard path simple: review apps require only `CPLN_TOKEN_STAGING` when the generated review app config can be inferred. For shared review-app resources such as one staging database, use `shared_secret_grants` and `{{SHARED_SECRET_DATABASE}}` placeholders instead of hardcoding the base app secret name; this keeps review-app policy binding and cleanup automatic while avoiding per-PR database cost. Document the one-time Control Plane bootstrap command for persistent staging and production apps with `cpflow setup-app --skip-post-creation-hook`; for existing apps or later template updates, document `cpflow apply-template` and the need for the app identity to have `reveal` on the app secret policy. Do not imply the staging deploy or promotion workflows create those persistent GVCs. For production promotion, document a protected `production` GitHub Environment with required reviewers, prevent self-review, and `CPLN_TOKEN_PRODUCTION` stored as an environment secret, not as a repository or organization secret.
24
24
 
25
25
  Keep Node available in the final image if asset compilation or SSR depends on ExecJS, Yarn, `pnpm`, or npm after the main install layer. Make sure the generated Dockerfile uses a Ruby base image compatible with the app's declared Ruby requirement. Preserve repo-defined frontend build hooks: if `config/shakapacker.yml` defines a `precompile_hook`, or React on Rails enables `config.auto_load_bundle = true`, confirm the generated Dockerfile runs that codegen step before `rails assets:precompile`. If `config/database.yml` shows SQLite in production, confirm that the generated scaffold uses persistent `db` and `storage` volumes plus a release script that runs `rails db:prepare`; otherwise keep the default Postgres workload. If the public workload is not named `rails`, set `PRIMARY_WORKLOAD` or adjust the generated workflows. Inspect the Dockerfile and package sources for private GitHub dependencies or `RUN --mount=type=ssh`; if present, wire `DOCKER_BUILD_SSH_KEY`, optionally set `DOCKER_BUILD_SSH_KNOWN_HOSTS` for non-GitHub SSH hosts, and keep `DOCKER_BUILD_EXTRA_ARGS` to newline-delimited single tokens such as `--build-arg=FOO=bar`.
26
26
 
@@ -16,7 +16,7 @@ End-to-end rollout in one view:
16
16
 
17
17
  1. `cpflow github-flow-readiness` — exits non-zero if the repo is not ready to deploy.
18
18
  2. `cpflow generate` — creates `.controlplane/` if missing.
19
- 3. `cpflow generate-github-actions` — adds thin `cpflow-*` workflow wrappers that call upstream reusable workflows.
19
+ 3. `cpflow generate-github-actions` — adds `cpflow-*` workflow wrappers. Review-app, staging, cleanup, and helper workflows call upstream reusable workflows; production promotion is a normal caller-repo job so it can own the protected production Environment.
20
20
  4. Configure the GitHub [repository secrets and variables](#required-github-repository-settings) the workflows expect.
21
21
  5. Push the branch, then comment `+review-app-deploy` on a PR to spin up a review environment.
22
22
 
@@ -197,22 +197,37 @@ For production promotion, also configure:
197
197
  - `PRODUCTION_APP_NAME` as a production environment variable, for example `my-app-production`
198
198
 
199
199
  Do not put `CPLN_TOKEN_PRODUCTION` in repository or organization secrets for
200
- sensitive production systems. The generated promotion reusable workflow declares
201
- `environment: production`; the generated caller passes that environment name
202
- through `production_environment`. GitHub waits for the `production`
203
- environment's protection rules before injecting `CPLN_TOKEN_PRODUCTION` into
204
- the upstream production job.
205
-
206
- GitHub's reusable-workflow syntax still requires the upstream workflow to
207
- declare `CPLN_TOKEN_PRODUCTION` as an optional `workflow_call` secret so static
208
- validation accepts `secrets.CPLN_TOKEN_PRODUCTION`, but the generated caller
209
- must not pass it. GitHub uses the secret from the reusable workflow job's
210
- `production` environment when that environment is configured.
211
-
212
- Generated caller workflows pass only the named secrets each reusable workflow
213
- needs. They do not use `secrets: inherit`; the production token is supplied by
214
- the protected `production` Environment after approval, not forwarded from a
215
- repository secret.
200
+ sensitive production systems. Production promotion intentionally runs as a
201
+ normal caller-repo workflow job with `environment: production`, then checks out
202
+ the pinned `control-plane-flow` release for shared actions. GitHub exposes the
203
+ production token to that job only after the `production` environment gate.
204
+ GitHub does not expose which secret scope supplied a nonempty value at runtime,
205
+ so a broader repository or organization secret with the same name can mask a
206
+ missing environment secret. Keep the production token absent from broader secret
207
+ scopes.
208
+
209
+ Do not move production promotion behind a cross-repo reusable workflow. GitHub
210
+ does not expose the caller repository's environment secrets to that called
211
+ workflow, so `secrets.CPLN_TOKEN_PRODUCTION` remains empty even when the
212
+ `production` Environment contains the secret. Generated reusable-workflow
213
+ callers still pass only the named secrets each upstream workflow needs and do
214
+ not use `secrets: inherit`; production promotion is the caller-owned exception.
215
+
216
+ If promotion fails in the `Validate production token` step with
217
+ `CPLN_TOKEN_PRODUCTION is not set. Add it as a secret on the 'production' GitHub Environment.`,
218
+ check the environment scope first. Also verify that the `promote-to-production`
219
+ job declares `environment: production` and that no same-named repository or
220
+ organization secret exists. Create or verify the environment secret with:
221
+ You need permission to manage repository environments and secrets to run these
222
+ commands.
223
+
224
+ ```sh
225
+ gh secret set CPLN_TOKEN_PRODUCTION --repo OWNER/REPO --env production
226
+ # Paste the token value when prompted.
227
+ gh secret list --repo OWNER/REPO --env production
228
+ gh secret list --repo OWNER/REPO
229
+ gh secret list --org OWNER | grep '^CPLN_TOKEN_PRODUCTION[[:space:]]' || true
230
+ ```
216
231
 
217
232
  ## First-Time Control Plane Bootstrap
218
233
 
@@ -264,6 +279,28 @@ templates, and the staging token must have access to create and update
264
279
  review-app GVCs, workloads, images, identities, policies, and secrets in the
265
280
  staging org.
266
281
 
282
+ If review apps share an existing staging database or another existing secret,
283
+ declare it with `shared_secret_grants` on the review app config entry. The
284
+ deploy workflow runs `setup-app` for new review apps and `deploy-image` for
285
+ image updates; those commands bind or repair the review app identity's `reveal`
286
+ permission on each configured shared policy. The delete and cleanup workflows
287
+ call `cpflow delete`, which removes those bindings as review apps go away. This
288
+ lets one shared database or license secret serve many short-lived review apps
289
+ without granting every review identity access to unrelated app secrets.
290
+
291
+ ```yaml
292
+ apps:
293
+ my-app-review:
294
+ match_if_app_name_starts_with: true
295
+ shared_secret_grants:
296
+ - name: database
297
+ secret_name: my-app-review-database-secrets
298
+ policy_name: my-app-review-database-secrets-policy
299
+ ```
300
+
301
+ Then reference `cpln://secret/{{SHARED_SECRET_DATABASE}}.DATABASE_URL` from the
302
+ workload template.
303
+
267
304
  ### Production Promotion Safety
268
305
 
269
306
  `CPLN_TOKEN_PRODUCTION` can change live production workloads, images, releases,
@@ -281,10 +318,12 @@ The standard path is:
281
318
  intentionally non-sensitive.
282
319
 
283
320
  GitHub only exposes environment secrets to jobs that reference the environment
284
- after configured protection rules pass. GitHub also does not allow a caller job
285
- that directly invokes a reusable workflow to set `environment`; for that reason,
286
- the reusable promotion workflow itself declares `environment: production`. See
287
- GitHub's docs for [managing environments](https://docs.github.com/en/actions/how-tos/deploy/configure-and-manage-deployments/manage-environments),
321
+ after configured protection rules pass. GitHub does not allow a caller job that
322
+ directly invokes a reusable workflow to set `environment`, and cross-repo
323
+ reusable workflows do not receive the caller repository's environment secrets.
324
+ For that reason, generated production promotion stays as a normal caller-repo
325
+ job with `environment: production`. See GitHub's docs for
326
+ [managing environments](https://docs.github.com/en/actions/how-tos/deploy/configure-and-manage-deployments/manage-environments),
288
327
  [deployment protection rules](https://docs.github.com/en/actions/reference/workflows-and-actions/deployments-and-environments),
289
328
  and [reusable workflow limitations](https://docs.github.com/en/actions/reference/reusable-workflows-reference#supported-keywords-for-jobs-that-call-a-reusable-workflow).
290
329
 
@@ -405,32 +444,37 @@ or clones that copy the workflow before configuring Control Plane can remove
405
444
  wrapper-level `if:` guard shown in that file, for example
406
445
  `vars.REVIEW_APP_PREFIX != '' || vars.CPLN_ORG_STAGING != ''`.
407
446
 
408
- ## Upstream Reusable Workflows
447
+ ## Upstream Workflows And Actions
409
448
 
410
- The generated workflows are intentionally small wrappers. The deployment logic,
411
- comment formatting, Control Plane CLI setup, Docker image build, and cleanup helpers
412
- live in upstream reusable workflows and composite actions in this repository.
449
+ Most generated workflows are intentionally small wrappers. The deployment
450
+ logic, comment formatting, Control Plane CLI setup, Docker image build, and
451
+ cleanup helpers live in upstream reusable workflows and composite actions in
452
+ this repository. Production promotion is expanded into the caller repository so
453
+ it can own `environment: production`, but it still checks out the same upstream
454
+ ref for shared composite actions.
413
455
 
414
- - `cpflow-setup-environment`: installs Ruby, the Control Plane CLI, and `cpflow`, then logs into the target org. By default it builds `cpflow` from the checked-out upstream reusable-workflow ref; set the `CPFLOW_VERSION` repository variable only when you want to force a published RubyGems release.
456
+ - `cpflow-setup-environment`: installs Ruby, the Control Plane CLI, and `cpflow`, then logs into the target org. By default it builds `cpflow` from the checked-out upstream `control-plane-flow` ref; set the `CPFLOW_VERSION` repository variable only when you want to force a published RubyGems release.
415
457
  - `cpflow-build-docker-image`: builds and pushes the app image with the desired commit SHA
416
458
  - `cpflow-delete-control-plane-app`: safely deletes temporary apps and refuses to touch names outside the configured review-app prefix
417
459
 
418
460
  ## Version Pins: GitHub Ref vs RubyGems
419
461
 
420
- The generated `cpflow-*` workflow files are thin wrappers around reusable
421
- workflows in `shakacode/control-plane-flow`. GitHub loads reusable workflows
422
- from a repository ref, not from the Ruby gem, so each wrapper has an upstream
423
- GitHub ref:
462
+ Generated `cpflow-*` workflow files pin `shakacode/control-plane-flow` from
463
+ GitHub, not from the Ruby gem. Reusable workflow wrappers pin that source with
464
+ an upstream `uses:` ref:
424
465
 
425
466
  ```yaml
426
467
  uses: shakacode/control-plane-flow/.github/workflows/cpflow-deploy-review-app.yml@<ref>
427
468
  ```
428
469
 
429
- That single `uses:` ref is the downstream lock. GitHub exposes the reusable
430
- workflow's own repository, ref, and SHA to the called job, so the upstream
431
- workflow checks out the matching `control-plane-flow` source automatically from
432
- that context. Downstream wrappers should not pass `control_plane_flow_ref`; if
433
- you see that input in generated wrappers, regenerate with a newer `cpflow`.
470
+ Production promotion pins the same source in its `Checkout control-plane-flow
471
+ actions` step because it is a caller-owned job, not a reusable workflow caller.
472
+ Those refs are the downstream lock. GitHub exposes a reusable workflow's own
473
+ repository, ref, and SHA to called jobs, so reusable upstream workflows check out
474
+ matching `control-plane-flow` source automatically from that context. Downstream
475
+ reusable-workflow wrappers should not pass `control_plane_flow_ref`; if you see
476
+ that input outside the production promotion setup step, regenerate with a newer
477
+ `cpflow`.
434
478
 
435
479
  There are two locks, and they protect different things:
436
480
 
@@ -528,10 +572,12 @@ releasing it. Use an immutable commit SHA from the upstream PR branch:
528
572
  bin/pin-cpflow-github-ref <upstream-pr-sha>
529
573
  ```
530
574
 
531
- The helper updates every generated `cpflow-*` workflow wrapper. It accepts
532
- release tags and full commit SHAs by default, rejects branch names such as
533
- `main` or `feature/foo`, and requires `--allow-moving-ref` for short-lived
534
- local experiments that should not be committed.
575
+ The helper updates every generated reusable-workflow `uses:` ref plus the
576
+ production workflow's pinned `control-plane-flow` checkout and setup
577
+ validation ref. It accepts release tags and full commit SHAs by default,
578
+ rejects branch names such as `main` or `feature/foo`, and requires
579
+ `--allow-moving-ref` for short-lived local experiments that should not be
580
+ committed.
535
581
 
536
582
  3. Keep `CPFLOW_VERSION` unset so the workflow builds `cpflow` from the same
537
583
  upstream SHA that supplies the reusable workflow and composite actions. If
@@ -566,9 +612,10 @@ releasing it. Use an immutable commit SHA from the upstream PR branch:
566
612
  `bin/pin-cpflow-github-ref vX.Y.Z` only for a ref-only update when the
567
613
  generated templates are already current.
568
614
 
569
- This tests the real reusable workflow, shared composite actions, and source-built
570
- `cpflow` gem from one immutable upstream commit. It avoids merging upstream blind
571
- and avoids running production automation against a moving branch.
615
+ This tests the real reusable workflows, the production workflow's checked-out
616
+ shared composite actions, and the source-built `cpflow` gem from one immutable
617
+ upstream commit. It avoids merging upstream blind and avoids running production
618
+ automation against a moving branch.
572
619
 
573
620
  ## Local Generated-Flow Checks
574
621
 
@@ -580,9 +627,11 @@ bin/test-cpflow-github-flow
580
627
 
581
628
  The helper runs `cpflow github-flow-readiness`, parses generated workflow YAML,
582
629
  checks composite action metadata for literal GitHub expressions in descriptions,
583
- checks that all generated wrappers use one upstream ref consistently, rejects
584
- broad `secrets: inherit` usage in generated cpflow wrappers, rejects obsolete
585
- `control_plane_flow_ref` wrapper inputs, and runs
630
+ checks that all generated wrappers and the production `control-plane-flow`
631
+ checkout use one upstream ref consistently, rejects broad `secrets: inherit`
632
+ usage in generated cpflow wrappers, rejects obsolete `control_plane_flow_ref`
633
+ wrapper inputs, verifies production promotion remains a caller-owned
634
+ `environment: production` job, and runs
586
635
  `actionlint` against `.github/workflows/cpflow-*.yml`. Its `actionlint` command
587
636
  keeps the existing shellcheck ignore and also ignores stale local `actionlint`
588
637
  false positives for GitHub's newer reusable-workflow `job.workflow_*` fields.
data/docs/commands.md CHANGED
@@ -41,6 +41,9 @@ cpflow ai-github-flow-prompt
41
41
  {{APP_IMAGE_LINK}} - full link for latest app image, ready to be used for the value of `containers[].image` in the templates
42
42
  {{APP_IDENTITY}} - default identity
43
43
  {{APP_IDENTITY_LINK}} - full link for identity, ready to be used for the value of `identityLink` in the templates
44
+ {{APP_SECRETS}} - app secret dictionary name
45
+ {{APP_SECRETS_POLICY}} - app secret policy name
46
+ {{SHARED_SECRET_<NAME>}} - shared secret dictionary name from `shared_secret_grants`
44
47
  ```
45
48
 
46
49
  ```sh
@@ -79,7 +82,7 @@ cpflow cleanup-images -a $APP_NAME
79
82
  ### `cleanup-stale-apps`
80
83
 
81
84
  - Acts on stale apps based on the creation date of the latest image, or the GVC if no images exist
82
- - With `--mode=delete` (default): deletes the whole app (GVC with all workloads, all volumesets and all images), and unbinds the app from the secrets policy as long as both the identity and the policy exist (and are bound)
85
+ - With `--mode=delete` (default): deletes the whole app (GVC with all workloads, all volumesets and all images), and unbinds the app from the secrets policy and any configured `shared_secret_grants` policies as long as both the identity and each policy exist (and are bound)
83
86
  - With `--mode=stop`: suspends all workloads via `cpflow ps:stop` — no GVC, volumeset, or image is removed; resume with `cpflow ps:start`
84
87
  - `--mode=stop` only suspends workloads listed in `app_workloads` + `additional_workloads`; workloads present in the live GVC but missing from the config are skipped silently
85
88
  - `--mode=stop` returns once each workload is marked suspended; it does not wait for the workload to reach a not-ready state
@@ -128,7 +131,8 @@ cpflow copy-image-from-upstream -a $APP_NAME --upstream-token $UPSTREAM_TOKEN --
128
131
  ### `delete`
129
132
 
130
133
  - Deletes the whole app (GVC with all workloads, all volumesets and all images) or a specific workload
131
- - Also unbinds the app from the secrets policy, as long as both the identity and the policy exist (and are bound)
134
+ - Also unbinds the app from the secrets policy and any configured `shared_secret_grants` policies, as long as both the identity and each policy exist (and are bound)
135
+ - For the app-specific secrets policy, removes every permission held by the app identity; for `shared_secret_grants`, removes only `reveal`
132
136
  - Will ask for explicit user confirmation
133
137
  - Runs a pre-deletion hook before the app is deleted if `hooks.pre_deletion` is specified in the `.controlplane/controlplane.yml` file
134
138
  - If the hook exits with a non-zero code, the command will stop executing and also exit with a non-zero code
@@ -149,6 +153,7 @@ cpflow delete -a $APP_NAME -w $WORKLOAD_NAME
149
153
  - The release script is run in the context of `cpflow run` with the latest image
150
154
  - If the release script exits with a non-zero code, the command will stop executing and also exit with a non-zero code
151
155
  - If `use_digest_image_ref` is `true` in the `.controlplane/controlplane.yml` file or `--use-digest-image-ref` option is provided, deployed image's reference will include its digest
156
+ - Repairs missing `shared_secret_grants` policy bindings before running a release phase or updating workloads
152
157
 
153
158
  ```sh
154
159
  cpflow deploy-image -a $APP_NAME
@@ -513,6 +518,7 @@ cpflow run -a $APP_NAME --entrypoint /app/alternative-entrypoint.sh -- rails db:
513
518
  - Configures app to have org-level secrets with default name `"{APP_PREFIX}-secrets"`
514
519
  using org-level policy with default name `"{APP_PREFIX}-secrets-policy"` (names can be customized, see docs)
515
520
  - Creates identity for secrets if it does not exist
521
+ - Binds the app identity to any configured `shared_secret_grants` policies as part of the secrets setup flow; skipped when `--skip-secrets-setup` or `--skip-secret-access-binding` is provided, or `skip_secrets_setup` is set
516
522
  - Use `--skip-secrets-setup` to prevent the automatic setup of secrets,
517
523
  or set it through `skip_secrets_setup` in the `.controlplane/controlplane.yml` file
518
524
  - Runs a post-creation hook after the app is created if `hooks.post_creation` is specified in the `.controlplane/controlplane.yml` file
@@ -544,7 +550,7 @@ cpflow terraform import
544
550
  Regenerates the generated cpflow GitHub Actions wrappers and helper files
545
551
  from the currently installed cpflow gem. Use this after updating the
546
552
  cpflow gem so checked-in workflow wrappers move to the matching upstream
547
- release tag, for example `v5.0.3`.
553
+ release tag, for example `v5.0.4`.
548
554
 
549
555
  If the existing generated staging workflow uses a custom single staging
550
556
  branch, the command preserves it. Pass `--staging-branch BRANCH` to set or
data/docs/postgres.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Migrating Postgres database from Heroku infrastructure
2
2
 
3
+ > **Networking note.** This guide is written against a *publicly reachable* RDS instance for simplicity of the
4
+ > Bucardo migration steps. For production, you almost certainly want RDS/Aurora in private subnets reached from
5
+ > Control Plane workloads via the Cloud Wormhole agent — see
6
+ > [Connecting Control Plane workloads to a private AWS RDS/Aurora database](./rds-private-networking.md). The
7
+ > Bucardo migration here still works against a private RDS once the network path is set up.
8
+
3
9
  If you are replacing Heroku Postgres or another pre-provisioned database service, also review the
4
10
  [Control Plane PostgreSQL Template Catalog page](https://shakadocs.controlplane.com/template-catalog/templates/postgres). The
5
11
  catalog template covers a single-instance PostgreSQL workload with persistent storage, optional PgBouncer, and optional