cpflow 5.3.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.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/.agents/agent-workflow.yml +16 -25
  3. data/.agents/bin/README.md +19 -9
  4. data/.agents/bin/docs +1 -1
  5. data/.agents/bin/lint +1 -1
  6. data/.agents/bin/setup +1 -1
  7. data/.agents/bin/test +1 -1
  8. data/.agents/bin/validate +2 -1
  9. data/.agents/legacy-workflow-policy.yml +11 -0
  10. data/.agents/shaka.md +20 -0
  11. data/.agents/trusted-actions.yml +12 -0
  12. data/.coderabbit.yaml +14 -0
  13. data/.github/actions/cpflow-build-docker-image/action.yml +153 -22
  14. data/.github/actions/cpflow-delete-control-plane-app/action.yml +2 -1
  15. data/.github/actions/cpflow-delete-control-plane-app/delete-app.sh +3 -0
  16. data/.github/actions/cpflow-preview-promotion/action.yml +82 -0
  17. data/.github/actions/cpflow-preview-promotion/preview-promotion.sh +90 -0
  18. data/.github/actions/cpflow-setup-environment/action.yml +8 -6
  19. data/.github/dependabot.yml +10 -0
  20. data/.github/workflows/check_cpln_links.yml +6 -1
  21. data/.github/workflows/claude-code-review.yml +5 -2
  22. data/.github/workflows/claude.yml +6 -4
  23. data/.github/workflows/coderabbit-lifecycle-review.yml +29 -0
  24. data/.github/workflows/command_docs.yml +7 -2
  25. data/.github/workflows/cpflow-cleanup-stale-review-apps.yml +12 -5
  26. data/.github/workflows/cpflow-delete-review-app.yml +18 -11
  27. data/.github/workflows/cpflow-deploy-review-app.yml +38 -17
  28. data/.github/workflows/cpflow-deploy-staging.yml +28 -11
  29. data/.github/workflows/cpflow-help-command.yml +3 -3
  30. data/.github/workflows/cpflow-promote-staging-to-production.yml +135 -16
  31. data/.github/workflows/cpflow-review-app-help.yml +1 -1
  32. data/.github/workflows/rspec-shared.yml +34 -26
  33. data/.github/workflows/rspec-specific.yml +10 -2
  34. data/.github/workflows/rspec.yml +95 -4
  35. data/.github/workflows/rubocop.yml +9 -2
  36. data/.github/workflows/trigger-docs-site.yml +2 -0
  37. data/.rubocop.yml +6 -1
  38. data/AGENTS.md +18 -3
  39. data/CHANGELOG.md +39 -1
  40. data/CONTRIBUTING.md +39 -2
  41. data/Gemfile.lock +1 -1
  42. data/README.md +10 -2
  43. data/cpflow.gemspec +3 -15
  44. data/docs/ai-github-flow-prompt.md +3 -3
  45. data/docs/ci-automation.md +284 -68
  46. data/docs/commands.md +38 -15
  47. data/docs/postgres-review-apps.md +90 -0
  48. data/docs/postgres.md +233 -375
  49. data/docs/rds-private-networking.md +12 -12
  50. data/docs/releasing.md +15 -4
  51. data/docs/secrets-and-env-values.md +42 -0
  52. data/docs/sidebars.ts +1 -0
  53. data/examples/controlplane.yml +5 -0
  54. data/lib/command/ai_github_flow_prompt.rb +2 -2
  55. data/lib/command/apply_template.rb +36 -4
  56. data/lib/command/base.rb +56 -3
  57. data/lib/command/cleanup_stale_apps.rb +28 -4
  58. data/lib/command/copy_image_from_upstream.rb +3 -0
  59. data/lib/command/delete.rb +174 -4
  60. data/lib/command/deploy_image.rb +44 -1
  61. data/lib/command/generate.rb +247 -3
  62. data/lib/command/generate_github_actions.rb +43 -13
  63. data/lib/command/run.rb +224 -34
  64. data/lib/command/setup_app.rb +92 -10
  65. data/lib/command/update_github_actions.rb +82 -43
  66. data/lib/core/config.rb +73 -0
  67. data/lib/core/controlplane.rb +167 -80
  68. data/lib/core/controlplane_api.rb +28 -2
  69. data/lib/core/controlplane_api_direct.rb +17 -0
  70. data/lib/core/disposable_postgres_helpers.rb +167 -0
  71. data/lib/core/doctor_service.rb +7 -0
  72. data/lib/core/github_flow_readiness/checks.rb +2 -1
  73. data/lib/core/github_flow_readiness_service.rb +4 -0
  74. data/lib/core/repo_introspection.rb +112 -2
  75. data/lib/core/shell.rb +35 -10
  76. data/lib/core/timed_command.rb +111 -0
  77. data/lib/cpflow/version.rb +1 -1
  78. data/lib/cpflow.rb +1 -1
  79. data/lib/generator_templates/.dockerignore +16 -0
  80. data/lib/generator_templates/Dockerfile +9 -5
  81. data/lib/generator_templates/entrypoint.sh +2 -0
  82. data/lib/generator_templates/templates/postgres.yml +9 -0
  83. data/lib/generator_templates_sqlite/controlplane.yml +2 -1
  84. data/lib/generator_templates_sqlite/release_script.sh +1 -1
  85. data/lib/generator_templates_sqlite/templates/rails.yml +2 -1
  86. data/lib/github_flow_templates/.github/cpflow-help.md +39 -12
  87. data/lib/github_flow_templates/.github/workflows/cpflow-deploy-review-app.yml +1 -0
  88. data/lib/github_flow_templates/.github/workflows/cpflow-deploy-staging.yml +1 -0
  89. data/lib/github_flow_templates/.github/workflows/cpflow-promote-staging-to-production.yml +135 -16
  90. data/lib/github_flow_templates/bin/pin-cpflow-github-ref +37 -13
  91. data/lib/github_flow_templates/bin/test-cpflow-github-flow +125 -46
  92. data/lib/patches/hash.rb +2 -2
  93. data/rakelib/create_release.rake +14 -14
  94. data/script/check_shell_scripts +66 -0
  95. metadata +18 -17
@@ -23,7 +23,7 @@ End-to-end rollout in one view:
23
23
 
24
24
  1. `cpflow github-flow-readiness` — exits non-zero if the repo is not ready to deploy.
25
25
  2. `cpflow generate` — creates `.controlplane/` if missing.
26
- 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.
26
+ 3. `cpflow generate-github-actions` — adds `cpflow-*` workflow wrappers and `.github/actions/cpflow-*` composite actions. 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.
27
27
  4. Configure the [GitHub Actions secrets and variables](#github-actions-secrets-and-variables) the workflows expect.
28
28
  5. Push the branch, then comment `+review-app-deploy` on a PR to spin up a review environment.
29
29
 
@@ -69,6 +69,7 @@ The second command writes namespaced files so they can coexist with an app's exi
69
69
  - `.github/workflows/cpflow-deploy-staging.yml`
70
70
  - `.github/workflows/cpflow-promote-staging-to-production.yml`
71
71
  - `.github/workflows/cpflow-cleanup-stale-review-apps.yml`
72
+ - `.github/actions/cpflow-*` (the local composite actions used by those workflows)
72
73
  - `bin/pin-cpflow-github-ref`
73
74
  - `bin/test-cpflow-github-flow`
74
75
 
@@ -76,7 +77,7 @@ The second command writes namespaced files so they can coexist with an app's exi
76
77
  Docker base Ruby version from `.ruby-version`, `.tool-versions`, or the app's
77
78
  `Gemfile`, preserves repo-defined frontend precompile hooks such as Shakapacker
78
79
  `precompile_hook` commands or React on Rails auto bundle generation, and
79
- switches to persistent SQLite `db` and `storage` templates when
80
+ switches to persistent SQLite data and Active Storage templates when
80
81
  `config/database.yml` shows SQLite in production.
81
82
 
82
83
  `cpflow github-flow-readiness` checks public RubyGems and npm registry metadata
@@ -214,7 +215,8 @@ For production promotion, also configure:
214
215
  - a GitHub Environment named `production`
215
216
  - required reviewers on that environment, limited to the people or team allowed to promote production
216
217
  - "Prevent self-review" on that environment, so the person who starts the promotion cannot approve it
217
- - optionally disable administrator bypass and restrict deployment branches/tags to your protected release branch
218
+ - disable administrator bypass if your organization requires two-person control
219
+ - restrict deployment branches/tags to your protected release branch
218
220
  - `CPLN_TOKEN_PRODUCTION` as an environment secret on `production`, not as a repository or organization secret
219
221
  - `CPLN_ORG_PRODUCTION` as a production environment variable, for example `company-production`
220
222
  - `PRODUCTION_APP_NAME` as a production environment variable, for example `my-app-production`
@@ -228,14 +230,75 @@ because they could change the target org name after normalization.
228
230
 
229
231
  Production promotion copies the exact image currently deployed on the selected
230
232
  staging workload. If that staging image is digest-pinned, the digest is used for
231
- the source copy while the production tag is derived from the tag portion. Tags
232
- with a `_<commit>` suffix keep that suffix in production; plain numeric tags are
233
- also valid and promote to the next plain production tag. The copy step uses
233
+ the source copy while the production tag is derived from the tag portion. Staging tags must
234
+ include a full lowercase 40-character Git commit SHA suffix (`_<commit>`),
235
+ which is preserved in the new production tag. A staging image without that
236
+ provenance stops promotion before approval. The copy step uses
234
237
  `docker buildx imagetools create --prefer-index=false --tag` with isolated
235
238
  Docker credentials, which preserves multi-architecture manifests, preserves
236
239
  single-platform manifest format when supported, and avoids pulling image layers
237
240
  onto the GitHub Actions runner.
238
241
 
242
+ When you start production promotion, the **preview-promotion** job finishes
243
+ before GitHub requests approval for the protected production Environment. Open
244
+ the run summary’s **Commits ready for production** section before approving. It
245
+ compares staging’s deployed primary-workload image with the last recorded
246
+ successful `production-*` GitHub release. It links the full comparison, lists up
247
+ to 100 commit subjects, and flags diverged history or staging behind production.
248
+ Matching SHAs report no commits to promote. Release discovery checks the latest
249
+ 100 releases; missing production history or GitHub API failures produce an
250
+ explicit availability notice. The preview uses the existing staging token and
251
+ GitHub `contents: read` permission; it has no production token.
252
+
253
+ The preview selects the primary workload from the staging app’s
254
+ `app_workloads` configuration. `PRODUCTION_APP_NAME` and
255
+ `CPLN_ORG_PRODUCTION` can remain production Environment variables. When staging
256
+ and production have different primary-workload defaults, set `PRIMARY_WORKLOAD`
257
+ as a repository variable shared by both jobs; the protected job rejects a
258
+ workload-selection mismatch. A Control Plane read failure or invalid staging
259
+ SHA fails the preview and prevents approval from starting.
260
+
261
+ After approval, the protected job checks that staging still uses the captured
262
+ image and, when a release baseline was available, that live production matches
263
+ that release’s commit. A mismatch stops before copying or deploying an image;
264
+ rerun after staging changes, or reconcile the production release record after an
265
+ out-of-band production deployment. If production history was unavailable, the
266
+ summary does not claim to know the production-to-staging diff.
267
+
268
+ Existing repositories adopt this with
269
+ `cpflow update-github-actions --workflows cpflow-promote-staging-to-production.yml`.
270
+ Review and reapply downstream workflow customizations before committing the
271
+ replacement and the refreshed local composite actions. The bare update command
272
+ preserves existing top-level workflows.
273
+
274
+ ### Recover a missing or stale production release record
275
+
276
+ A successful production deployment and its GitHub release are separate jobs.
277
+ If deployment succeeded but **create-github-release** failed, open that original
278
+ Actions run and choose **Re-run failed jobs**. This retries release publication
279
+ at the original promoted SHA; do not rerun the whole promotion merely to repair
280
+ the record. Confirm the resulting `production-*` release targets the live
281
+ production image’s SHA, then start a fresh promotion preview.
282
+
283
+ After an out-of-band production deploy or rollback, an app maintainer should
284
+ first verify the healthy live primary-workload image and its full commit suffix
285
+ with `cpln workload get <primary-workload> --gvc <production-app> --org
286
+ <production-org> -o json`. Use that SHA to publish a fresh release record; do not
287
+ move an existing release tag. For example, after replacing `<verified-live-sha>`:
288
+
289
+ ```sh
290
+ gh release create "production-$(date -u +%Y-%m-%d-%H%M%S)-0" \
291
+ --target <verified-live-sha> \
292
+ --title "Production state reconciliation" \
293
+ --notes "Records the verified live production commit after a manual deployment or rollback."
294
+ ```
295
+
296
+ Run this in the app repository with the maintainer’s normal GitHub credentials.
297
+ Verify the new tag’s commit and the live image agree, then rerun promotion to
298
+ produce a fresh preview. If the live image has no commit suffix, establish its
299
+ provenance and use a traceable image before recording a baseline; guessing a
300
+ branch head would make the comparison misleading.
301
+
239
302
  Before copying the image, production promotion compares the environment variable
240
303
  names exposed by staging and production at both the GVC level and each configured
241
304
  app workload's container level. Variables present in staging are treated as
@@ -423,9 +486,11 @@ Recommended org layout:
423
486
  review/staging CI operations; use a dedicated review-app org only if you also customize the generated
424
487
  workflow/configuration to target that org separately
425
488
 
426
- Optional repository secret for private dependency builds:
489
+ Optional repository secrets for Docker builds:
427
490
 
428
491
  - `DOCKER_BUILD_SSH_KEY`: private SSH key used when the Dockerfile needs `RUN --mount=type=ssh` to fetch private GitHub dependencies during image build
492
+ - `DOCKER_BUILD_SECRETS`: staging build credentials as newline-delimited `id=value` entries supplied through BuildKit secret mounts. Values must be single-line; spaces and additional `=` characters are preserved. IDs may contain letters, digits, underscores, and hyphens, and must start with a letter or digit. Empty values and duplicate IDs are rejected. Leave this secret unset when the build does not need credentials.
493
+ - `REVIEW_APP_DOCKER_BUILD_SECRETS`: separate credentials for review-app builds, using the same format. The generated review-app caller uses only this secret; it does not fall back to `DOCKER_BUILD_SECRETS`. Leave it unset to supply no BuildKit secrets to review apps.
429
494
 
430
495
  Optional repository variables for private dependency builds:
431
496
 
@@ -473,7 +538,12 @@ The generated flow uses these defaults:
473
538
  makes the newest intent fail closed instead of falling back to an older operation. This permission gate applies to every
474
539
  `+review-app-deploy` comment, whether or not a review app already exists. Later pushes to a base-repository branch PR
475
540
  redeploy automatically without another approval because the auto-push path (`pull_request` event) does not use the
476
- comment permission gate;
541
+ comment permission gate. The reusable deploy workflow loads generated local actions through `actions/checkout`'s
542
+ default with no `ref:` override, which resolves to the commit GitHub recorded for the triggering event: the
543
+ pull-request merge revision for automatic same-repository deploys, the selected ref for manual dispatch, or the
544
+ default-branch revision for comment triggers. This keeps the wrapper and generated actions synchronized during an
545
+ upgrade or first-installation PR. Preserve the same-repository caller guard because pull-request workflow and action
546
+ code run with staging/review credentials;
477
547
  - fork pull requests cannot deploy via the generated `pull_request` path because the caller workflow's job-level `if:`
478
548
  condition explicitly skips fork-originated runs. For `issue_comment` events, the caller `if:` restricts invocation to
479
549
  the exact command shape; the reusable workflow resolves repository permission before its deploy job runs, then its
@@ -510,13 +580,16 @@ generated staging/review org and make that token disposable and unable to access
510
580
  The PR-close teardown workflow runs trusted base-branch workflow code with repository secret access so it can delete fork
511
581
  PR review apps. The generated `cpflow-delete-control-plane-app` composite action script refuses to call `cpflow delete`
512
582
  on any app whose name does not match the review-app prefix. This shell-level guard is effective because the generated
513
- delete workflow checks out base/default-branch code, not PR or fork code: its repository checkout has no `ref:` override,
514
- so `pull_request_target` runs use the base branch. If you customize this workflow, never check out PR or fork code in the
515
- same job as the delete step; doing so could let a PR replace the guard script itself and would also make
516
- `hooks.pre_deletion` come from the PR's `controlplane.yml`. This is still not a token policy, so use a scoped staging
517
- service account limited to review/staging operations. A configured `hooks.pre_deletion` command still runs through the
518
- latest PR-built image on all delete paths: PR-close teardown, `+review-app-delete` comments, manual dispatch, and
519
- scheduled cleanup. Review-app credentials must remain disposable even during deletion.
583
+ delete workflow loads generated actions through `actions/checkout`'s default with no `ref:` override, which resolves to
584
+ the commit GitHub recorded for the triggering event (`GITHUB_SHA`, the base-branch tip under `pull_request_target`). Its
585
+ separate app checkout has no `ref:` override either, so `pull_request_target` teardown uses base-branch code for both
586
+ paths rather than PR or fork code. If you customize this
587
+ workflow, never check out PR or fork code in the same job as the delete step; doing so could let a PR replace the guard
588
+ script itself and would also make `hooks.pre_deletion` come from the PR's `controlplane.yml`. This is still not a token
589
+ policy, so use a scoped staging service account limited to review/staging operations. A configured
590
+ `hooks.pre_deletion` command still runs through the latest PR-built image on all delete paths: PR-close teardown,
591
+ `+review-app-delete` comments, manual dispatch, and scheduled cleanup. Review-app credentials must remain disposable
592
+ even during deletion.
520
593
 
521
594
  If you customize the generated `pull_request_target` workflow, never pass `github.event.pull_request.head.sha` or another
522
595
  fork-controlled ref to `actions/checkout`, `git fetch`, `git merge`, `git cherry-pick`, or any other step that fetches,
@@ -560,6 +633,30 @@ The generated `cpflow-build-docker-image` action supports this without hardcodin
560
633
  - set `DOCKER_BUILD_SSH_KEY` if the Docker build needs SSH access to GitHub
561
634
  - optionally set `DOCKER_BUILD_SSH_KNOWN_HOSTS` when the SSH build host is not GitHub.com or you need custom host entries
562
635
  - set `DOCKER_BUILD_EXTRA_ARGS` when you need extra `docker build` flags
636
+ - set `DOCKER_BUILD_SECRETS` for staging builds that need credentials such as a private package token or a source-map upload token
637
+ - set `REVIEW_APP_DOCKER_BUILD_SECRETS` separately if review-app builds need credentials
638
+
639
+ For example, store the following as the **value** of the `DOCKER_BUILD_SECRETS` GitHub Actions secret:
640
+
641
+ ```text
642
+ sentry_auth_token=<source-map upload token>
643
+ npm_token=<private package token>
644
+ ```
645
+
646
+ The generated staging caller forwards `DOCKER_BUILD_SECRETS`. The review-app caller forwards `REVIEW_APP_DOCKER_BUILD_SECRETS`, the only build-secret parameter the reusable review-app workflow accepts, so review builds never receive the staging secret. The build action writes each value to a private temporary file, passes only its path through `--secret=id=...,src=...`, and removes the files on success or failure. Values are not passed as build arguments or persisted through `GITHUB_ENV`.
647
+
648
+ A Dockerfile can consume a token only for the command that needs it:
649
+
650
+ ```dockerfile
651
+ # syntax=docker/dockerfile:1
652
+ ARG SENTRY_UPLOAD=false
653
+ RUN --mount=type=secret,id=sentry_auth_token,env=SENTRY_AUTH_TOKEN \
654
+ SENTRY_UPLOAD=${SENTRY_UPLOAD} npm run build
655
+ ```
656
+
657
+ If the application's build requires an explicit upload opt-in, pass `docker_build_extra_args: --build-arg=SENTRY_UPLOAD=true` in the caller workflow's `with` block, or add that token to `DOCKER_BUILD_EXTRA_ARGS`. The optional workflow input is appended after the repository variable, retaining existing build arguments. Token availability alone need not enable uploads. BuildKit secret values do not invalidate build caches; force the relevant build step to run again when rotating a credential requires repeating an upload.
658
+
659
+ Every secret supplied to a review build is accessible to the PR-controlled Dockerfile, which can read and transmit it. Automatic rebuilds of an existing review app also receive these secrets; approval to create the app does not approve later commits. Configure only credentials whose access can be entrusted to anyone able to change the deployed branch, scoped to the minimum review-build permissions. Separate secret names reduce accidental sharing but do not prevent the Dockerfile from accessing supplied credentials. If a credential cannot be entrusted to branch authors, omit it from review builds or use a customized approval gate that approves each deployed commit. Apply the review-app security guidance above.
563
660
 
564
661
  For example, a repo that installs private dependencies from GitHub during Docker build can set:
565
662
 
@@ -698,11 +795,13 @@ wrapper-level `if:` guard shown in that file, for example
698
795
  ## Upstream Workflows And Actions
699
796
 
700
797
  Most generated workflows are intentionally small wrappers. The deployment
701
- logic, comment formatting, Control Plane CLI setup, Docker image build, and
702
- cleanup helpers live in upstream reusable workflows and composite actions in
703
- this repository. Production promotion is expanded into the caller repository so
704
- it can own `environment: production`, but it still checks out the same upstream
705
- ref for shared composite actions.
798
+ logic and comment formatting live in upstream reusable workflows. The canonical
799
+ composite actions live in this repository and the gem copies them into each
800
+ downstream repository at `.github/actions/cpflow-*`; the reusable workflows use
801
+ those regular local action paths. They separately check out the matching
802
+ upstream ref at `.cpflow` for the `cpflow` runtime source. Production promotion
803
+ is expanded into the caller repository so it can own `environment: production`,
804
+ but follows the same local-action and `.cpflow` source split.
706
805
 
707
806
  - `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.
708
807
  - `cpflow-build-docker-image`: builds and pushes the app image with the desired commit SHA
@@ -727,37 +826,51 @@ reusable-workflow wrappers should not pass `control_plane_flow_ref`; if you see
727
826
  that input outside the production promotion setup step, regenerate with a newer
728
827
  `cpflow`.
729
828
 
730
- There are two locks, and they protect different things:
829
+ There are two coordinated inputs, and they protect different things:
731
830
 
732
- - The GitHub ref locks the reusable workflow and composite action code that
733
- GitHub runs.
734
- - The RubyGems version locks the `cpflow` CLI/runtime code only when you install
735
- or run that gem. It does not make GitHub load reusable workflow YAML from the
736
- gem.
831
+ - The GitHub ref locks the reusable workflow and the `.cpflow` runtime source
832
+ checkout that GitHub runs.
833
+ - The installed Ruby gem supplies the generated local composite actions and
834
+ wrappers. `CPFLOW_VERSION`, when configured, separately locks the published
835
+ `cpflow` runtime installed by the setup action.
737
836
 
738
837
  That means a downstream app cannot rely on the gem alone for GitHub Actions
739
838
  behavior. The safe stable path is still gem-driven for generation, but
740
- developers must commit generated wrappers that reference the matching upstream
741
- release tag:
839
+ developers must commit the generated wrappers and local actions from the gem
840
+ that matches the upstream release tag:
742
841
 
743
842
  1. Publish a `cpflow` gem.
744
843
  2. Install or bundle that released gem in the downstream project.
745
844
  3. Run `cpflow generate-github-actions`.
746
- 4. Commit the generated wrappers that point to the matching upstream release tag
747
- such as `v5.0.0`.
845
+ 4. Commit the generated wrappers and `.github/actions/cpflow-*` files; the
846
+ wrappers should point to the matching upstream release tag such as `v5.0.0`.
748
847
 
749
848
  That release tag should point to the same source that produced the RubyGems
750
849
  release. Downstream production automation should use release tags, not `main` or
751
850
  feature-branch refs.
752
851
 
852
+ Generated cross-repository reusable-workflow calls deliberately use an exact
853
+ release tag such as `v5.0.0`. This is the intentional downstream exception to
854
+ the repository's full-SHA external-action policy: it keeps the installed gem,
855
+ generated wrappers, generated local actions, and upstream workflow on one
856
+ reviewed release update contract. It does not extend to external actions inside
857
+ those workflows, which remain pinned to full commit SHAs with readable release
858
+ comments. Unreleased downstream validation temporarily replaces the exact tag
859
+ with the upstream PR's full commit SHA as described below; moving version
860
+ aliases and branches remain forbidden.
861
+
753
862
  ## Updating Generated GitHub Actions After Gem Updates
754
863
 
755
- Whenever a downstream repo updates the `cpflow` gem, update the checked-in
756
- GitHub Actions wrappers in the same PR. The gem version does not make GitHub load
757
- new reusable workflow YAML by itself; GitHub loads the `uses:` ref committed in
758
- `.github/workflows/cpflow-*.yml`.
864
+ Whenever a downstream repo updates the `cpflow` gem, update all checked-in
865
+ GitHub Actions files in the same PR. The gem version does not make GitHub load
866
+ new reusable workflow YAML or local action files by itself; GitHub uses the
867
+ workflow ref and `.github/actions/cpflow-*` files committed in the repository.
759
868
 
760
- Use the installed gem to refresh the generated wrappers:
869
+ For a new repository that does not have generated wrappers yet, run
870
+ `cpflow generate-github-actions` first. Use the update command below for later
871
+ gem upgrades.
872
+
873
+ Use the installed gem to refresh the generated workflows and actions:
761
874
 
762
875
  ```sh
763
876
  cpflow update-github-actions
@@ -771,11 +884,88 @@ bundle exec cpflow update-github-actions
771
884
  bin/test-cpflow-github-flow bundle exec cpflow
772
885
  ```
773
886
 
774
- `cpflow update-github-actions` regenerates the generated wrapper and helper
775
- files from the installed gem, pins the wrapper `uses:` refs to `v<gem-version>`,
776
- and preserves a single custom staging branch from the existing generated staging
777
- workflow. Pass `--staging-branch BRANCH` when changing or restoring a custom
778
- staging branch explicitly.
887
+ `cpflow update-github-actions` refreshes local composite actions and helpers.
888
+ It preserves every top-level workflow by default, including refs, triggers,
889
+ permissions, and downstream deployment ownership. It does not create staging
890
+ or production deployment workflows when another CI system owns those deploys.
891
+ Select the wrappers you intend to replace explicitly; see
892
+ `cpflow update-github-actions --help` for the current interface:
893
+
894
+ ```sh
895
+ bundle exec cpflow update-github-actions --workflows \
896
+ cpflow-deploy-review-app.yml cpflow-delete-review-app.yml \
897
+ cpflow-cleanup-stale-review-apps.yml cpflow-help-command.yml \
898
+ cpflow-review-app-help.yml
899
+ ```
900
+
901
+ Each selected wrapper is replaced in full with the installed gem's template
902
+ and release ref. Review the diff and reapply downstream changes before
903
+ committing. Unselected wrappers keep their refs: preservation is not evidence
904
+ that old wrappers are compatible with new local actions. Migrate wrapper
905
+ contracts and source pins together, then run the validator in the same PR.
906
+ For a selected staging wrapper, a single existing push branch or the default
907
+ main/master pair can be inferred. Missing, malformed, or ambiguous branch
908
+ configuration requires an explicit `--staging-branch BRANCH`; that option also
909
+ requires selecting `cpflow-deploy-staging.yml`.
910
+
911
+ ### Preserving downstream validation
912
+
913
+ The generated `bin/test-cpflow-github-flow` runs baseline checks followed by
914
+ an optional executable `bin/test-cpflow-github-flow-custom`. The extension runs
915
+ from the repository root with the cpflow command argv forwarded unchanged
916
+ (for example, `bundle`, `exec`, `cpflow`), and its nonzero exit fails validation.
917
+ The generator never creates or overwrites the custom extension. Baseline
918
+ production checks apply when the generated production workflow exists;
919
+ downstream-owned production workflows belong in the custom checks.
920
+
921
+ An existing validator that differs from the installed template blocks the
922
+ entire update before any writes. This includes unmodified validators from
923
+ older releases: the updater cannot safely distinguish them from custom code.
924
+ For a legacy installation such as HiChee's 5.3-to-6.0 RC migration:
925
+
926
+ 1. Save the old validator in version control and compare it with its original
927
+ release. Extract the downstream checks into `bin/test-cpflow-github-flow-custom`
928
+ and make it executable. If it is entirely downstream-specific, rename it
929
+ directly. Remove any call back to the generated validator to avoid recursion.
930
+ 2. Remove or rename the old `bin/test-cpflow-github-flow` after preserving those
931
+ checks, then run the update command with the selected wrappers. No override
932
+ flag bypasses this check. Do not use `generate-github-actions --force` as a
933
+ migration shortcut: it intentionally replaces the full generated flow.
934
+ 3. Run the new validator. Confirm that the custom checks execute and that a
935
+ failing custom check still fails the command. Repeating the updater with the
936
+ same selected workflows should produce no further diff.
937
+
938
+ ### Migrating trusted action allowlists
939
+
940
+ Repositories using a closed `trusted_actions` policy must review these exact
941
+ repositories used by the complete generated output:
942
+
943
+ ```yaml
944
+ trusted_actions:
945
+ - actions/checkout
946
+ - docker/setup-buildx-action
947
+ - ruby/setup-ruby
948
+ - shakacode/control-plane-flow
949
+ ```
950
+
951
+ Merge the needed entries into your existing policy rather than replacing it.
952
+ `ruby/setup-ruby` is used by the generated local setup action, including in
953
+ review-app-only installations. `actions/checkout` and `docker/setup-buildx-action`
954
+ are used directly by the generated production wrapper;
955
+ `shakacode/control-plane-flow` supplies the reusable wrappers. Other existing
956
+ downstream workflows may need additional entries. Upstream reusable workflows
957
+ also use `actions/github-script`; reviewing that upstream implementation remains
958
+ part of trusting `shakacode/control-plane-flow`.
959
+
960
+ Allowlisting does not waive immutable pin requirements. For a policy requiring
961
+ full SHAs, resolve and independently verify the chosen upstream release tag or
962
+ test commit, then run `bin/pin-cpflow-github-ref --version <reviewed-version-tag>
963
+ <full-sha>`. The helper writes that version/tag beside all cpflow SHA pins and
964
+ updates obsolete same-line comments. The label is supplied by the caller; the
965
+ helper does not contact GitHub or certify tag-to-SHA identity. For an unreleased
966
+ test commit, use its source version tag as context and record the exact commit
967
+ in the PR. Review the generated diff and run your repository's action security
968
+ scanner after updating its allowlist.
779
969
 
780
970
  When keeping `cpflow` in an app Gemfile, leave a comment next to the gem entry
781
971
  so future dependency bumps include the wrapper update:
@@ -809,7 +999,8 @@ action commit, so a moving branch named like `v5.0.0` cannot be used with
809
999
  runners that cannot reach GitHub should leave `CPFLOW_VERSION` unset and build
810
1000
  `cpflow` from the checked-out ref instead. When testing an unreleased upstream
811
1001
  commit SHA, leave `CPFLOW_VERSION` unset so the workflow builds `cpflow` from the
812
- same source that supplies the reusable workflow and composite actions.
1002
+ same source that supplies the reusable workflow. Refresh the generated local
1003
+ actions from that checkout before testing as described below.
813
1004
 
814
1005
  ## Testing Unreleased Upstream Changes Downstream
815
1006
 
@@ -817,24 +1008,26 @@ You can test a `control-plane-flow` PR in a downstream app before merging or
817
1008
  releasing it. Use an immutable commit SHA from the upstream PR branch:
818
1009
 
819
1010
  1. Push the upstream PR branch and copy its full 40-character head SHA.
820
- 2. In a downstream test branch, run:
1011
+ 2. In a downstream test branch, regenerate from the upstream checkout and then
1012
+ pin the wrappers to its exact SHA:
821
1013
 
822
1014
  ```sh
823
- bin/pin-cpflow-github-ref <upstream-pr-sha>
1015
+ ruby /path/to/control-plane-flow/bin/cpflow update-github-actions
1016
+ bin/pin-cpflow-github-ref --version <reviewed-version-tag> <upstream-pr-sha>
824
1017
  ```
825
1018
 
826
- The helper updates every generated reusable-workflow `uses:` ref plus the
827
- production workflow's pinned `control-plane-flow` checkout and setup
828
- validation ref. It accepts release tags and full commit SHAs by default,
829
- rejects branch names such as `main` or `feature/foo`, and requires
830
- `--allow-moving-ref` for short-lived local experiments that should not be
831
- committed.
832
-
833
- 3. Keep `CPFLOW_VERSION` unset so the workflow builds `cpflow` from the same
834
- upstream SHA that supplies the reusable workflow and composite actions. If
835
- `CPFLOW_VERSION` is set while the wrapper is pinned to a SHA, the setup
836
- action fails before deployment because the gem and action code cannot be
837
- proven to match.
1019
+ The update command copies the PR checkout's canonical composite actions into
1020
+ `.github/actions/cpflow-*`. The pin helper then updates every generated
1021
+ reusable-workflow `uses:` ref plus the production workflow's pinned
1022
+ `control-plane-flow` checkout and setup validation ref. It accepts release
1023
+ tags and full commit SHAs (with a reviewed `--version` label), rejects branch names such as `main` or
1024
+ `feature/foo`, and requires `--allow-moving-ref` for short-lived local
1025
+ experiments that should not be committed.
1026
+
1027
+ 3. Keep `CPFLOW_VERSION` unset so the workflow builds `cpflow` from the pinned
1028
+ upstream SHA. If `CPFLOW_VERSION` is set while the wrapper is pinned to a
1029
+ SHA, the setup action fails before deployment because the published gem and
1030
+ upstream source cannot be proven to match.
838
1031
  4. Run:
839
1032
 
840
1033
  ```sh
@@ -848,25 +1041,48 @@ releasing it. Use an immutable commit SHA from the upstream PR branch:
848
1041
  bin/test-cpflow-github-flow ruby /path/to/control-plane-flow/bin/cpflow
849
1042
  ```
850
1043
 
851
- 5. Open a downstream PR and trigger the real review app with a comment whose body
852
- is exactly:
1044
+ 5. Push the downstream test branch, then choose the remote validation path that
1045
+ matches the repository's installation state.
1046
+
1047
+ For an existing installation, where `cpflow-deploy-review-app.yml` is already
1048
+ present on the default branch, dispatch the workflow definition and generated
1049
+ local actions from the unmerged downstream test branch against a suitable open
1050
+ base-repository PR:
1051
+
1052
+ ```sh
1053
+ gh workflow run cpflow-deploy-review-app.yml --ref <downstream-test-branch> -f pr_number=<pr-number>
1054
+ ```
1055
+
1056
+ The `--ref` selects the downstream wrapper and local-action commit under test;
1057
+ `pr_number` selects the application source to deploy. Do not use a
1058
+ `+review-app-deploy` comment for this pre-merge validation: `issue_comment`
1059
+ always loads the workflow definition from the default branch, so it cannot
1060
+ validate unmerged wrappers or local actions.
853
1061
 
854
- ```text
855
- +review-app-deploy
1062
+ For a first installation, GitHub will not dispatch a workflow that does not
1063
+ yet exist on the default branch, even when `--ref` names the installation
1064
+ branch. Treat the rollout as staged: run the local contract before merging,
1065
+ review the generated diff, merge it, and dispatch the merged workflow
1066
+ immediately afterward against a suitable open base-repository PR:
1067
+
1068
+ ```sh
1069
+ gh workflow run cpflow-deploy-review-app.yml --ref <default-branch> -f pr_number=<pr-number>
856
1070
  ```
857
1071
 
858
- 6. Verify the deploy logs show the expected upstream commit SHA, the setup step
859
- prints the expected `cpflow` source/version, and the review app URL returns
860
- HTTP 200.
1072
+ 6. Verify the dispatched deploy logs show the expected upstream commit SHA, the
1073
+ setup step prints the expected `cpflow` source/version, and the review app URL
1074
+ returns HTTP 200. After merge, `+review-app-deploy` remains the normal
1075
+ maintainer-facing trigger for subsequent review apps.
861
1076
  7. After the upstream PR merges and a gem is released, regenerate the downstream
862
1077
  wrappers from that released gem and commit the release tag. Use
863
1078
  `bin/pin-cpflow-github-ref vX.Y.Z` only for a ref-only update when the
864
1079
  generated templates are already current.
865
1080
 
866
- This tests the real reusable workflows, the production workflow's checked-out
867
- shared composite actions, and the source-built `cpflow` gem from one immutable
868
- upstream commit. It avoids merging upstream blind and avoids running production
869
- automation against a moving branch.
1081
+ For an existing installation, this tests the real reusable workflows, the
1082
+ unmerged regenerated local composite actions, and the source-built `cpflow` gem
1083
+ against one immutable upstream commit. A first installation gets the same remote
1084
+ proof immediately after its locally validated files reach the default branch.
1085
+ Both paths avoid testing against a moving upstream branch.
870
1086
 
871
1087
  ## Local Generated-Flow Checks
872
1088
 
@@ -909,7 +1125,7 @@ In practice, porting the flow into a demo app usually follows five phases.
909
1125
 
910
1126
  4. Update `.controlplane/controlplane.yml` with staging, review, and production entries.
911
1127
  5. Confirm that the generated Dockerfile picked a Ruby base image compatible with the app's declared Ruby requirement.
912
- 6. For SQLite-backed apps, confirm that the generated scaffold switched to persistent `db` and `storage` volumes, mounted them into the main workload, and added a release script that runs `rails db:prepare`.
1128
+ 6. For SQLite-backed apps, confirm that the generated scaffold redirects every production database file into persistent `/app/data` (or keeps it under persistent `/app/storage`) without hiding image migrations under `/app/db`, mounts Active Storage at `/app/storage`, and adds a release script that runs `rails db:prepare`.
913
1129
 
914
1130
  **Adapt for the app's runtime:**
915
1131
 
@@ -953,7 +1169,7 @@ Expand that prompt with app-specific requirements before editing files:
953
1169
  - make sure the generated Dockerfile uses a Ruby base image compatible with the app's declared Ruby requirement
954
1170
  - preserve repo-defined frontend precompile hooks, such as Shakapacker `precompile_hook` commands or React on Rails `config.auto_load_bundle = true`
955
1171
  - keep Node available in the final image if Rails or SSR depends on ExecJS, Yarn, or `pnpm` after the main `npm install` layer
956
- - if `config/database.yml` shows SQLite in production, confirm that `cpflow generate` emitted persistent `db` and `storage` volumes plus a `rails db:prepare` release script; otherwise keep the default Postgres workload
1172
+ - if `config/database.yml` shows SQLite in production, confirm that `cpflow generate` redirected every production database path into persistent `/app/data` or `/app/storage`, emitted both volumes without mounting over `/app/db`, and added a `rails db:prepare` release script; otherwise keep the default Postgres workload
957
1173
  - inspect the production Dockerfile and package sources for private GitHub dependencies, and wire `DOCKER_BUILD_SSH_KEY` plus `DOCKER_BUILD_SSH_KNOWN_HOSTS` when the build uses `RUN --mount=type=ssh` against non-GitHub hosts
958
1174
  - add extra `app_workloads` and template files for any runtime sidecars, workers, or renderer processes
959
1175
  - make sure any sidecar process exposed to sibling workloads binds to `0.0.0.0` instead of container-local `localhost`