cpflow 5.3.0 → 6.0.0.rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.agents/agent-workflow.yml +13 -0
  3. data/.agents/bin/README.md +12 -1
  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/.coderabbit.yaml +13 -0
  10. data/.github/actions/cpflow-delete-control-plane-app/action.yml +2 -1
  11. data/.github/actions/cpflow-setup-environment/action.yml +8 -6
  12. data/.github/dependabot.yml +10 -0
  13. data/.github/workflows/check_cpln_links.yml +6 -1
  14. data/.github/workflows/claude-code-review.yml +5 -2
  15. data/.github/workflows/claude.yml +6 -4
  16. data/.github/workflows/coderabbit-lifecycle-review.yml +29 -0
  17. data/.github/workflows/command_docs.yml +7 -2
  18. data/.github/workflows/cpflow-cleanup-stale-review-apps.yml +12 -5
  19. data/.github/workflows/cpflow-delete-review-app.yml +18 -11
  20. data/.github/workflows/cpflow-deploy-review-app.yml +25 -16
  21. data/.github/workflows/cpflow-deploy-staging.yml +16 -10
  22. data/.github/workflows/cpflow-help-command.yml +3 -3
  23. data/.github/workflows/cpflow-promote-staging-to-production.yml +7 -7
  24. data/.github/workflows/cpflow-review-app-help.yml +1 -1
  25. data/.github/workflows/rspec-shared.yml +34 -26
  26. data/.github/workflows/rspec-specific.yml +10 -2
  27. data/.github/workflows/rspec.yml +71 -4
  28. data/.github/workflows/rubocop.yml +9 -2
  29. data/.github/workflows/trigger-docs-site.yml +2 -0
  30. data/.rubocop.yml +6 -1
  31. data/CHANGELOG.md +24 -1
  32. data/CONTRIBUTING.md +39 -2
  33. data/Gemfile.lock +1 -1
  34. data/README.md +6 -1
  35. data/cpflow.gemspec +3 -15
  36. data/docs/ai-github-flow-prompt.md +2 -2
  37. data/docs/ci-automation.md +112 -60
  38. data/docs/commands.md +18 -8
  39. data/docs/rds-private-networking.md +12 -12
  40. data/docs/releasing.md +15 -4
  41. data/examples/controlplane.yml +5 -0
  42. data/lib/command/ai_github_flow_prompt.rb +1 -1
  43. data/lib/command/cleanup_stale_apps.rb +28 -4
  44. data/lib/command/copy_image_from_upstream.rb +3 -0
  45. data/lib/command/deploy_image.rb +38 -1
  46. data/lib/command/generate_github_actions.rb +36 -12
  47. data/lib/command/run.rb +224 -34
  48. data/lib/command/update_github_actions.rb +7 -6
  49. data/lib/core/controlplane.rb +145 -78
  50. data/lib/core/shell.rb +35 -10
  51. data/lib/core/timed_command.rb +111 -0
  52. data/lib/cpflow/version.rb +1 -1
  53. data/lib/cpflow.rb +1 -1
  54. data/lib/github_flow_templates/.github/workflows/cpflow-promote-staging-to-production.yml +7 -7
  55. data/lib/github_flow_templates/bin/test-cpflow-github-flow +63 -3
  56. data/lib/patches/hash.rb +2 -2
  57. data/rakelib/create_release.rake +14 -14
  58. data/script/check_shell_scripts +66 -0
  59. metadata +11 -18
@@ -10,7 +10,7 @@ first.
10
10
  ```text
11
11
  Set up Control Plane GitHub Flow for this repo. First make sure the `cpflow` CLI is available: use the repo's existing `bundle exec cpflow` if present, otherwise install the published `cpflow` Ruby gem with `gem install cpflow`; if neither is possible, stop and report that blocker. Use the same `cpflow` invocation for the rest of the rollout. 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.
12
12
 
13
- 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 normal generated review-app setup simple: review apps require only `CPLN_TOKEN_STAGING` when the generated review app config can be inferred. For public demos, starter staging apps, and long-lived review apps, keep the app workload `type: standard` with one warm replica, set its autoscaling metric to `disabled`, and enable `capacityAI: true` so Control Plane can right-size CPU and memory allocation at that fixed replica count. Shared Postgres and other stateful workloads are the usual exceptions and should stay manually sized; Capacity AI is for supported stateless app/service workloads. If true idle scale-to-zero is explicitly required, create a separate `serverless` workload before first deploy or plan a delete/recreate migration because Control Plane will not change an existing `standard` workload to `serverless` in place. 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.
13
+ 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. Keep every generated Control Plane Flow source pin synchronized at the same `vX.Y.Z`: each reusable-workflow `uses: ...@vX.Y.Z` ref, the production promotion `.cpflow` checkout ref, and its `control_plane_flow_ref` setup input. By default, `cpflow generate-github-actions` derives these values from the installed `cpflow` gem version; temporary explicit ref overrides are for unreleased testing and must keep all three values synchronized. Keep the generated `.github/actions/cpflow-*` files from that same gem unchanged; upstream reusable workflows invoke those checked-in local actions and separately check out the matching runtime source at `.cpflow`. 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 workflows and local actions move together. Keep the normal generated review-app setup simple: review apps require only `CPLN_TOKEN_STAGING` when the generated review app config can be inferred. For public demos, starter staging apps, and long-lived review apps, keep the app workload `type: standard` with one warm replica, set its autoscaling metric to `disabled`, and enable `capacityAI: true` so Control Plane can right-size CPU and memory allocation at that fixed replica count. Shared Postgres and other stateful workloads are the usual exceptions and should stay manually sized; Capacity AI is for supported stateless app/service workloads. If true idle scale-to-zero is explicitly required, create a separate `serverless` workload before first deploy or plan a delete/recreate migration because Control Plane will not change an existing `standard` workload to `serverless` in place. 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.
14
14
 
15
15
  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`.
16
16
 
@@ -46,7 +46,7 @@ Stop and report the blocker instead of generating `cpflow-*` workflow files when
46
46
  The rollout is done when all of the following are true:
47
47
 
48
48
  - `.controlplane/` exists and matches the actual app shape
49
- - `.github/cpflow-help.md` and `.github/workflows/cpflow-*` wrappers are in place
49
+ - `.github/cpflow-help.md`, `.github/workflows/cpflow-*`, and `.github/actions/cpflow-*` are in place
50
50
  - review apps are opt-in, staging auto-deploys from one branch, and production promotion is manual
51
51
  - required GitHub secrets and variables are documented for the repo
52
52
  - the production image build path is validated for the real app
@@ -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
 
@@ -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`
@@ -473,7 +475,12 @@ The generated flow uses these defaults:
473
475
  makes the newest intent fail closed instead of falling back to an older operation. This permission gate applies to every
474
476
  `+review-app-deploy` comment, whether or not a review app already exists. Later pushes to a base-repository branch PR
475
477
  redeploy automatically without another approval because the auto-push path (`pull_request` event) does not use the
476
- comment permission gate;
478
+ comment permission gate. The reusable deploy workflow loads generated local actions through `actions/checkout`'s
479
+ default with no `ref:` override, which resolves to the commit GitHub recorded for the triggering event: the
480
+ pull-request merge revision for automatic same-repository deploys, the selected ref for manual dispatch, or the
481
+ default-branch revision for comment triggers. This keeps the wrapper and generated actions synchronized during an
482
+ upgrade or first-installation PR. Preserve the same-repository caller guard because pull-request workflow and action
483
+ code run with staging/review credentials;
477
484
  - fork pull requests cannot deploy via the generated `pull_request` path because the caller workflow's job-level `if:`
478
485
  condition explicitly skips fork-originated runs. For `issue_comment` events, the caller `if:` restricts invocation to
479
486
  the exact command shape; the reusable workflow resolves repository permission before its deploy job runs, then its
@@ -510,13 +517,16 @@ generated staging/review org and make that token disposable and unable to access
510
517
  The PR-close teardown workflow runs trusted base-branch workflow code with repository secret access so it can delete fork
511
518
  PR review apps. The generated `cpflow-delete-control-plane-app` composite action script refuses to call `cpflow delete`
512
519
  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.
520
+ delete workflow loads generated actions through `actions/checkout`'s default with no `ref:` override, which resolves to
521
+ the commit GitHub recorded for the triggering event (`GITHUB_SHA`, the base-branch tip under `pull_request_target`). Its
522
+ separate app checkout has no `ref:` override either, so `pull_request_target` teardown uses base-branch code for both
523
+ paths rather than PR or fork code. If you customize this
524
+ 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
525
+ script itself and would also make `hooks.pre_deletion` come from the PR's `controlplane.yml`. This is still not a token
526
+ policy, so use a scoped staging service account limited to review/staging operations. A configured
527
+ `hooks.pre_deletion` command still runs through the latest PR-built image on all delete paths: PR-close teardown,
528
+ `+review-app-delete` comments, manual dispatch, and scheduled cleanup. Review-app credentials must remain disposable
529
+ even during deletion.
520
530
 
521
531
  If you customize the generated `pull_request_target` workflow, never pass `github.event.pull_request.head.sha` or another
522
532
  fork-controlled ref to `actions/checkout`, `git fetch`, `git merge`, `git cherry-pick`, or any other step that fetches,
@@ -698,11 +708,13 @@ wrapper-level `if:` guard shown in that file, for example
698
708
  ## Upstream Workflows And Actions
699
709
 
700
710
  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.
711
+ logic and comment formatting live in upstream reusable workflows. The canonical
712
+ composite actions live in this repository and the gem copies them into each
713
+ downstream repository at `.github/actions/cpflow-*`; the reusable workflows use
714
+ those regular local action paths. They separately check out the matching
715
+ upstream ref at `.cpflow` for the `cpflow` runtime source. Production promotion
716
+ is expanded into the caller repository so it can own `environment: production`,
717
+ but follows the same local-action and `.cpflow` source split.
706
718
 
707
719
  - `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
720
  - `cpflow-build-docker-image`: builds and pushes the app image with the desired commit SHA
@@ -727,37 +739,51 @@ reusable-workflow wrappers should not pass `control_plane_flow_ref`; if you see
727
739
  that input outside the production promotion setup step, regenerate with a newer
728
740
  `cpflow`.
729
741
 
730
- There are two locks, and they protect different things:
742
+ There are two coordinated inputs, and they protect different things:
731
743
 
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.
744
+ - The GitHub ref locks the reusable workflow and the `.cpflow` runtime source
745
+ checkout that GitHub runs.
746
+ - The installed Ruby gem supplies the generated local composite actions and
747
+ wrappers. `CPFLOW_VERSION`, when configured, separately locks the published
748
+ `cpflow` runtime installed by the setup action.
737
749
 
738
750
  That means a downstream app cannot rely on the gem alone for GitHub Actions
739
751
  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:
752
+ developers must commit the generated wrappers and local actions from the gem
753
+ that matches the upstream release tag:
742
754
 
743
755
  1. Publish a `cpflow` gem.
744
756
  2. Install or bundle that released gem in the downstream project.
745
757
  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`.
758
+ 4. Commit the generated wrappers and `.github/actions/cpflow-*` files; the
759
+ wrappers should point to the matching upstream release tag such as `v5.0.0`.
748
760
 
749
761
  That release tag should point to the same source that produced the RubyGems
750
762
  release. Downstream production automation should use release tags, not `main` or
751
763
  feature-branch refs.
752
764
 
765
+ Generated cross-repository reusable-workflow calls deliberately use an exact
766
+ release tag such as `v5.0.0`. This is the intentional downstream exception to
767
+ the repository's full-SHA external-action policy: it keeps the installed gem,
768
+ generated wrappers, generated local actions, and upstream workflow on one
769
+ reviewed release update contract. It does not extend to external actions inside
770
+ those workflows, which remain pinned to full commit SHAs with readable release
771
+ comments. Unreleased downstream validation temporarily replaces the exact tag
772
+ with the upstream PR's full commit SHA as described below; moving version
773
+ aliases and branches remain forbidden.
774
+
753
775
  ## Updating Generated GitHub Actions After Gem Updates
754
776
 
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`.
777
+ Whenever a downstream repo updates the `cpflow` gem, update all checked-in
778
+ GitHub Actions files in the same PR. The gem version does not make GitHub load
779
+ new reusable workflow YAML or local action files by itself; GitHub uses the
780
+ workflow ref and `.github/actions/cpflow-*` files committed in the repository.
781
+
782
+ For a new repository that does not have generated wrappers yet, run
783
+ `cpflow generate-github-actions` first. Use the update command below for later
784
+ gem upgrades.
759
785
 
760
- Use the installed gem to refresh the generated wrappers:
786
+ Use the installed gem to refresh the generated workflows and actions:
761
787
 
762
788
  ```sh
763
789
  cpflow update-github-actions
@@ -771,11 +797,11 @@ bundle exec cpflow update-github-actions
771
797
  bin/test-cpflow-github-flow bundle exec cpflow
772
798
  ```
773
799
 
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.
800
+ `cpflow update-github-actions` regenerates the workflow wrappers, local
801
+ composite actions, and helper files from the installed gem, pins the wrapper
802
+ `uses:` refs to `v<gem-version>`, and preserves a single custom staging branch
803
+ from the existing generated staging workflow. Pass `--staging-branch BRANCH`
804
+ when changing or restoring a custom staging branch explicitly.
779
805
 
780
806
  When keeping `cpflow` in an app Gemfile, leave a comment next to the gem entry
781
807
  so future dependency bumps include the wrapper update:
@@ -809,7 +835,8 @@ action commit, so a moving branch named like `v5.0.0` cannot be used with
809
835
  runners that cannot reach GitHub should leave `CPFLOW_VERSION` unset and build
810
836
  `cpflow` from the checked-out ref instead. When testing an unreleased upstream
811
837
  commit SHA, leave `CPFLOW_VERSION` unset so the workflow builds `cpflow` from the
812
- same source that supplies the reusable workflow and composite actions.
838
+ same source that supplies the reusable workflow. Refresh the generated local
839
+ actions from that checkout before testing as described below.
813
840
 
814
841
  ## Testing Unreleased Upstream Changes Downstream
815
842
 
@@ -817,24 +844,26 @@ You can test a `control-plane-flow` PR in a downstream app before merging or
817
844
  releasing it. Use an immutable commit SHA from the upstream PR branch:
818
845
 
819
846
  1. Push the upstream PR branch and copy its full 40-character head SHA.
820
- 2. In a downstream test branch, run:
847
+ 2. In a downstream test branch, regenerate from the upstream checkout and then
848
+ pin the wrappers to its exact SHA:
821
849
 
822
850
  ```sh
851
+ ruby /path/to/control-plane-flow/bin/cpflow update-github-actions
823
852
  bin/pin-cpflow-github-ref <upstream-pr-sha>
824
853
  ```
825
854
 
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.
855
+ The update command copies the PR checkout's canonical composite actions into
856
+ `.github/actions/cpflow-*`. The pin helper then updates every generated
857
+ reusable-workflow `uses:` ref plus the production workflow's pinned
858
+ `control-plane-flow` checkout and setup validation ref. It accepts release
859
+ tags and full commit SHAs by default, rejects branch names such as `main` or
860
+ `feature/foo`, and requires `--allow-moving-ref` for short-lived local
861
+ experiments that should not be committed.
862
+
863
+ 3. Keep `CPFLOW_VERSION` unset so the workflow builds `cpflow` from the pinned
864
+ upstream SHA. If `CPFLOW_VERSION` is set while the wrapper is pinned to a
865
+ SHA, the setup action fails before deployment because the published gem and
866
+ upstream source cannot be proven to match.
838
867
  4. Run:
839
868
 
840
869
  ```sh
@@ -848,25 +877,48 @@ releasing it. Use an immutable commit SHA from the upstream PR branch:
848
877
  bin/test-cpflow-github-flow ruby /path/to/control-plane-flow/bin/cpflow
849
878
  ```
850
879
 
851
- 5. Open a downstream PR and trigger the real review app with a comment whose body
852
- is exactly:
880
+ 5. Push the downstream test branch, then choose the remote validation path that
881
+ matches the repository's installation state.
882
+
883
+ For an existing installation, where `cpflow-deploy-review-app.yml` is already
884
+ present on the default branch, dispatch the workflow definition and generated
885
+ local actions from the unmerged downstream test branch against a suitable open
886
+ base-repository PR:
853
887
 
854
- ```text
855
- +review-app-deploy
888
+ ```sh
889
+ gh workflow run cpflow-deploy-review-app.yml --ref <downstream-test-branch> -f pr_number=<pr-number>
890
+ ```
891
+
892
+ The `--ref` selects the downstream wrapper and local-action commit under test;
893
+ `pr_number` selects the application source to deploy. Do not use a
894
+ `+review-app-deploy` comment for this pre-merge validation: `issue_comment`
895
+ always loads the workflow definition from the default branch, so it cannot
896
+ validate unmerged wrappers or local actions.
897
+
898
+ For a first installation, GitHub will not dispatch a workflow that does not
899
+ yet exist on the default branch, even when `--ref` names the installation
900
+ branch. Treat the rollout as staged: run the local contract before merging,
901
+ review the generated diff, merge it, and dispatch the merged workflow
902
+ immediately afterward against a suitable open base-repository PR:
903
+
904
+ ```sh
905
+ gh workflow run cpflow-deploy-review-app.yml --ref <default-branch> -f pr_number=<pr-number>
856
906
  ```
857
907
 
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.
908
+ 6. Verify the dispatched deploy logs show the expected upstream commit SHA, the
909
+ setup step prints the expected `cpflow` source/version, and the review app URL
910
+ returns HTTP 200. After merge, `+review-app-deploy` remains the normal
911
+ maintainer-facing trigger for subsequent review apps.
861
912
  7. After the upstream PR merges and a gem is released, regenerate the downstream
862
913
  wrappers from that released gem and commit the release tag. Use
863
914
  `bin/pin-cpflow-github-ref vX.Y.Z` only for a ref-only update when the
864
915
  generated templates are already current.
865
916
 
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.
917
+ For an existing installation, this tests the real reusable workflows, the
918
+ unmerged regenerated local composite actions, and the source-built `cpflow` gem
919
+ against one immutable upstream commit. A first installation gets the same remote
920
+ proof immediately after its locally validated files reach the default branch.
921
+ Both paths avoid testing against a moving upstream branch.
870
922
 
871
923
  ## Local Generated-Flow Checks
872
924
 
data/docs/commands.md CHANGED
@@ -85,7 +85,7 @@ cpflow cleanup-images -a $APP_NAME
85
85
 
86
86
  - Acts on stale apps based on the creation date of the latest image, or the GVC if no images exist
87
87
  - 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)
88
- - With `--mode=stop`: suspends all workloads via `cpflow ps:stop` — no GVC, volumeset, or image is removed; resume with `cpflow ps:start`
88
+ - With `--mode=stop`: suspends configured workloads that exist in the live GVC through Control Plane — no GVC, volumeset, or image is removed; resume with `cpflow ps:start`
89
89
  - `--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
90
90
  - `--mode=stop` returns once each workload is marked suspended; it does not wait for the workload to reach a not-ready state
91
91
  - Specify the amount of days after an app should be considered stale through `stale_app_image_deployed_days` in the `.controlplane/controlplane.yml` file
@@ -232,6 +232,9 @@ Creates GitHub Actions templates for a Heroku Flow style Control Plane pipeline:
232
232
  - manual promotion from staging to production
233
233
  - nightly cleanup and PR help workflows
234
234
 
235
+ It also copies cpflow's composite actions into `.github/actions/cpflow-*`
236
+ so every local `uses:` target is checked in and can be audited directly.
237
+
235
238
  Pass `--staging-branch BRANCH` when staging should auto-deploy from a branch
236
239
  other than `main` or `master`; the generator will bake that branch into the
237
240
  GitHub Actions push trigger and use it as the default STAGING_APP_BRANCH.
@@ -240,13 +243,13 @@ Pass `--force` to overwrite existing generated files. Prefer
240
243
  repo.
241
244
 
242
245
  ```sh
243
- # Creates thin .github/workflows wrappers for the Control Plane flow
246
+ # Creates workflow wrappers, local composite actions, and validation helpers
244
247
  cpflow generate-github-actions
245
248
 
246
249
  # Creates the flow with staging deploys triggered from develop
247
250
  cpflow generate-github-actions --staging-branch develop
248
251
 
249
- # Overwrites existing generated wrappers from the installed cpflow gem
252
+ # Overwrites existing generated GitHub Actions files from the installed cpflow gem
250
253
  cpflow generate-github-actions --force
251
254
  ```
252
255
 
@@ -483,6 +486,12 @@ timeout 300 cpflow ps:wait -a $APP_NAME
483
486
  (can be configured though `runner_job_timeout` in `controlplane.yml`)
484
487
  - Waiting for a runner replica is limited to the smaller of `runner_job_timeout` and 1000 seconds.
485
488
  A terminal cron status fails immediately, and reaching the observation deadline reports the last safe status
489
+ - With non-interactive log methods 2 and 3, after a command prints its completion marker, Control Plane
490
+ has up to 20 minutes to reconcile the cron job to a terminal status. This can be configured through
491
+ `runner_job_status_reconciliation_timeout` in `controlplane.yml`; timing out exits nonzero and reports
492
+ the job, replica, and last observed status
493
+ - Log method 1 does not emit a completion marker, so its job-status polling is not covered by the
494
+ post-command reconciliation timeout
486
495
  - Non-interactive jobs return the Control Plane cron job status even when the job finishes before
487
496
  Control Plane exposes a runner replica to attach logs to
488
497
  - Injects `CPFLOW_GVC_ID` and `CPFLOW_GVC_CREATED` into the job, exposing the app's immutable GVC
@@ -579,17 +588,18 @@ cpflow terraform import
579
588
 
580
589
  ### `update-github-actions`
581
590
 
582
- Regenerates the generated cpflow GitHub Actions wrappers and helper files
583
- from the currently installed cpflow gem. Use this after updating the
584
- cpflow gem so checked-in workflow wrappers move to the matching upstream
585
- release tag, for example `v5.2.0`.
591
+ Regenerates the cpflow workflow wrappers, local composite actions, and
592
+ helper files from the currently installed cpflow gem. Use this after
593
+ updating the cpflow gem so checked-in workflow wrappers move to the
594
+ matching upstream release tag, for example `v5.3.0`, and the
595
+ generated action implementations move with them.
586
596
 
587
597
  If the existing generated staging workflow uses a custom single staging
588
598
  branch, the command preserves it. Pass `--staging-branch BRANCH` to set or
589
599
  replace the generated staging branch explicitly.
590
600
 
591
601
  ```sh
592
- # After updating the cpflow gem, refresh generated GitHub Actions wrappers
602
+ # After updating the cpflow gem, refresh every generated GitHub Actions file
593
603
  cpflow update-github-actions
594
604
 
595
605
  # When running cpflow through Bundler
@@ -6,13 +6,14 @@ path — not the public internet.
6
6
 
7
7
  This guide covers the recommended setup: **CPLN Cloud Wormhole via an Agent**.
8
8
 
9
- > **Sourcing note verify field casing before you apply.** Field names, schema, and limits in this guide
10
- > are sourced from the public Control Plane documentation at <https://shakadocs.controlplane.com> as of
11
- > May 2026 and have **not** been end-to-end verified against a live org. This matters because a casing
12
- > mismatch **fails silently**: `cpln apply` accepts the file, ignores the unrecognized field, and the
13
- > workload then can't reach the database with no error pointing back to the YAML. Before applying in
14
- > production, diff your edited files against a fresh `cpln identity get <name> -o yaml-slim` (and
15
- > `cpln agent get <name> -o yaml`) export to confirm the exact field names, and consult
9
+ > **Field casing verified.** Sanitized `cpln identity get <name> -o yaml-slim` exports from live orgs
10
+ > confirmed the `networkResources` nesting and the common `name`, `agentLink`, `FQDN`, and `ports` field casing.
11
+ > The current Control Plane [Identity reference](https://shakadocs.controlplane.com/reference/identity) and
12
+ > [Identity API](https://shakadocs.controlplane.com/api-reference/identity/get-an-identity-by-gvc-and-name)
13
+ > confirm the alternative `IPs` form and optional `resolverIP` spelling; neither appeared in the inspected
14
+ > live resources. Field names are case-sensitive: `cpln apply` can accept an unrecognized field while omitting
15
+ > the intended route, without pointing back to the identity YAML. Before applying in production, check the new
16
+ > block against these casing sources, diff your edited file against a fresh identity export, and consult
16
17
  > `cpln <command> --help` for the latest CLI flags.
17
18
 
18
19
  ## Why private networking
@@ -234,11 +235,10 @@ cpln identity get my-app-production-identity \
234
235
  Edit `identity-db.yaml` and **add** the `networkResources` block — keep every other apply-safe field
235
236
  exactly as exported:
236
237
 
237
- > ⚠️ **Verify field casing against your org before applying.** The field names below (`FQDN`, `IPs`,
238
- > `agentLink`, `resolverIP`) are sourced from public CPLN docs. If the live API uses different casing,
239
- > `cpln apply` will accept the file but silently ignore the resource workloads will hit
240
- > `could not translate host name` with no obvious link to the identity YAML. Diff your edited file
241
- > against the original `yaml-slim` export from `cpln identity get` to confirm.
238
+ > **Apply-safe editing:** start from a fresh `yaml-slim` export and diff your edited file against the
239
+ > original before applying. This helps detect unintended changes to exported fields, but it does not validate
240
+ > the newly added `networkResources` block. Check every key in that block, including `agentLink`, `FQDN`/`IPs`,
241
+ > `resolverIP`, and `ports`, against the verified casing sources above.
242
242
 
243
243
  ```yaml
244
244
  # identity-db.yaml (after edit — abbreviated, your file will have more fields)
data/docs/releasing.md CHANGED
@@ -23,6 +23,11 @@ Always update `CHANGELOG.md` before running the release task.
23
23
  - Fixes, improvements, deprecations, removals, or security updates: patch
24
24
  4. Merge the changelog PR before releasing.
25
25
 
26
+ A minimum Ruby version change is a breaking change. Merge the gemspec floor,
27
+ CI coverage for each supported Ruby minor, and an `Unreleased` breaking-change
28
+ entry together. Keep `Cpflow::VERSION` unchanged in that implementation PR.
29
+ The release task applies the required major version when the change ships.
30
+
26
31
  If updating the changelog manually, move the relevant `Unreleased` entries into
27
32
  a versioned header:
28
33
 
@@ -49,9 +54,13 @@ With no arguments, `rake release`:
49
54
 
50
55
  1. Reads the first versioned `CHANGELOG.md` header, such as `## [4.2.0]`.
51
56
  2. Uses that version when it is newer than the current gem version.
52
- 3. Uses the current version if the changelog version matches the gem version
53
- but has not been tagged yet.
54
- 4. Falls back to a patch bump if no new changelog version is found.
57
+ 3. Keeps using that version when it matches the current gem version, including
58
+ on a retry after a release attempt created the version commit or tag.
59
+ 4. Lets the existing-tag policy stop an incomplete retry instead of silently
60
+ selecting a different version.
61
+ 5. Falls back to a patch bump only from a stable current version when the
62
+ changelog does not name the current or a newer version. Prereleases require
63
+ an explicit target if the changelog cannot supply one.
55
64
 
56
65
  Other supported forms:
57
66
 
@@ -152,4 +161,6 @@ bundle exec rake "sync_github_release[4.2.0]"
152
161
  ```
153
162
 
154
163
  If the tag was pushed but the gem was not published, delete or correct the tag
155
- and version commit intentionally before trying again.
164
+ and version commit intentionally before trying again. A no-argument retry keeps
165
+ the changelog version authoritative and refuses to reinterpret a prerelease as
166
+ its stable version.
@@ -104,6 +104,11 @@ aliases:
104
104
  # If not specified, defaults to 21600 (6 hours).
105
105
  runner_job_timeout: 1000
106
106
 
107
+ # Sets how long `cpflow run` waits for Control Plane to reconcile a cron job status
108
+ # after the non-interactive command prints its completion marker.
109
+ # If not specified, defaults to 1200 (20 minutes).
110
+ runner_job_status_reconciliation_timeout: 1200
111
+
107
112
  # Apps with a deployed image created before this amount of days will be listed for deletion
108
113
  # when running the command `cpflow cleanup-stale-apps`.
109
114
  stale_app_image_deployed_days: 5
@@ -32,7 +32,7 @@ module Command
32
32
  <<~PROMPT
33
33
  Set up Control Plane GitHub Flow for this repo. First make sure the `cpflow` CLI is available: use the repo's existing `bundle exec cpflow` if present, otherwise install the published `cpflow` Ruby gem with `gem install cpflow`; if neither is possible, stop and report that blocker. Use the same `cpflow` invocation for the rest of the rollout. 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.
34
34
 
35
- If `.controlplane/` is missing, run `cpflow generate`. Treat the generated app names as the repo-name default (`#{inferred_app_prefix}`) 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 normal generated review-app setup simple: review apps require only `CPLN_TOKEN_STAGING` when the generated review app config can be inferred. For public demos, starter staging apps, and long-lived review apps, keep the app workload `type: standard` with one warm replica, set its autoscaling metric to `disabled`, and enable `capacityAI: true` so Control Plane can right-size CPU and memory allocation at that fixed replica count. Shared Postgres and other stateful workloads are the usual exceptions and should stay manually sized; Capacity AI is for supported stateless app/service workloads. If true idle scale-to-zero is explicitly required, create a separate `serverless` workload before first deploy or plan a delete/recreate migration because Control Plane will not change an existing `standard` workload to `serverless` in place. 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.
35
+ If `.controlplane/` is missing, run `cpflow generate`. Treat the generated app names as the repo-name default (`#{inferred_app_prefix}`) 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. Keep every generated Control Plane Flow source pin synchronized at the same `vX.Y.Z`: each reusable-workflow `uses: ...@vX.Y.Z` ref, the production promotion `.cpflow` checkout ref, and its `control_plane_flow_ref` setup input. By default, `cpflow generate-github-actions` derives these values from the installed `cpflow` gem version; temporary explicit ref overrides are for unreleased testing and must keep all three values synchronized. Keep the generated `.github/actions/cpflow-*` files from that same gem unchanged; upstream reusable workflows invoke those checked-in local actions and separately check out the matching runtime source at `.cpflow`. 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 workflows and local actions move together. Keep the normal generated review-app setup simple: review apps require only `CPLN_TOKEN_STAGING` when the generated review app config can be inferred. For public demos, starter staging apps, and long-lived review apps, keep the app workload `type: standard` with one warm replica, set its autoscaling metric to `disabled`, and enable `capacityAI: true` so Control Plane can right-size CPU and memory allocation at that fixed replica count. Shared Postgres and other stateful workloads are the usual exceptions and should stay manually sized; Capacity AI is for supported stateless app/service workloads. If true idle scale-to-zero is explicitly required, create a separate `serverless` workload before first deploy or plan a delete/recreate migration because Control Plane will not change an existing `standard` workload to `serverless` in place. 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.
36
36
 
37
37
  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`.
38
38
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Command
4
- class CleanupStaleApps < Base
4
+ class CleanupStaleApps < Base # rubocop:disable Metrics/ClassLength
5
5
  CLEANUP_MODE_OPTION = {
6
6
  name: :mode,
7
7
  params: {
@@ -24,7 +24,7 @@ module Command
24
24
  LONG_DESCRIPTION = <<~DESC
25
25
  - Acts on stale apps based on the creation date of the latest image, or the GVC if no images exist
26
26
  - 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)
27
- - With `--mode=stop`: suspends all workloads via `cpflow ps:stop` — no GVC, volumeset, or image is removed; resume with `cpflow ps:start`
27
+ - With `--mode=stop`: suspends configured workloads that exist in the live GVC through Control Plane — no GVC, volumeset, or image is removed; resume with `cpflow ps:start`
28
28
  - `--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
29
29
  - `--mode=stop` returns once each workload is marked suspended; it does not wait for the workload to reach a not-ready state
30
30
  - Specify the amount of days after an app should be considered stale through `stale_app_image_deployed_days` in the `.controlplane/controlplane.yml` file
@@ -101,14 +101,38 @@ module Command
101
101
  def process_app(app)
102
102
  if mode == "stop"
103
103
  progress.puts("Stopping app '#{app}'")
104
- run_cpflow_command("ps:stop", "-a", app)
104
+ stop_configured_live_workloads(app)
105
105
  else
106
106
  run_cpflow_command("delete", "-a", app, "--yes")
107
107
  end
108
108
  end
109
109
 
110
+ def stop_configured_live_workloads(app)
111
+ app_config = config.find_app_config(app)
112
+ raise "Can't find config for stale app '#{app}'." unless app_config
113
+
114
+ configured_workloads = required_app_option(app_config, app, :app_workloads) +
115
+ required_app_option(app_config, app, :additional_workloads)
116
+ live_workloads = (cp.fetch_workloads(app)&.fetch("items", []) || []).map { |workload| workload.fetch("name") }
117
+
118
+ (configured_workloads & live_workloads).each do |workload|
119
+ step("Stopping workload '#{workload}'") do
120
+ cp.set_workload_suspend(workload, true, app, missing_ok: true)
121
+ end
122
+ end
123
+ end
124
+
125
+ def required_app_option(app_config, app, option)
126
+ raise "Can't find option '#{option}' for app '#{app}' in 'controlplane.yml'." unless app_config.key?(option)
127
+
128
+ value = app_config.fetch(option)
129
+ raise "Option '#{option}' for app '#{app}' in 'controlplane.yml' must be an array." unless value.is_a?(Array)
130
+
131
+ value
132
+ end
133
+
110
134
  def action_description
111
- mode == "stop" ? "suspend all workloads in" : "delete"
135
+ mode == "stop" ? "suspend configured workloads in" : "delete"
112
136
  end
113
137
 
114
138
  def mode
@@ -80,6 +80,9 @@ module Command
80
80
  upstream_image = cp.latest_image(@upstream, @upstream_org) if !upstream_image || upstream_image == "latest"
81
81
  @commit = cp.extract_image_commit(upstream_image)
82
82
  @upstream_image_url = "#{@upstream_org}.registry.cpln.io/#{upstream_image}"
83
+ rescue ControlplaneApiDirect::ForbiddenError => e
84
+ Shell.write_to_tmp_stderr(e.message)
85
+ false
83
86
  end
84
87
  end
85
88