GitHub Actions Flow for Review Apps, Staging, and Production
This document describes the reusable GitHub Actions scaffolding generated by cpflow generate-github-actions.
The goal is to bring the Heroku Flow model into any cpflow project:
- Comment
+review-app-deployon a pull request to create or update a review app. - Push more commits to the PR to auto-redeploy that review app.
- Push to the staging branch to auto-deploy staging.
- Promote the already-built staging artifact to production from the Actions tab.
- Let a nightly workflow clean up stale review apps.
For public repositories, review apps are intentionally limited to branches in the base repository. Fork pull requests can receive help comments, but generated deploy workflows skip fork heads because review-app deployment builds Docker images with repository secrets. The skip is enforced by complementary guards on different trigger axes; see Review app security for repositories with external contributors before customizing these workflows. If a forked change needs a review app, a maintainer should first review the code and move the change into a trusted branch in the base repository.
Quick Start
End-to-end rollout in one view:
cpflow github-flow-readiness— exits non-zero if the repo is not ready to deploy.cpflow generate— creates.controlplane/if missing.cpflow generate-github-actions— addscpflow-*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.- Configure the GitHub Actions secrets and variables the workflows expect.
- Push the branch, then comment
+review-app-deployon a PR to spin up a review environment.
AI rollout: copy the AI rollout prompt when you want an agent to run this setup. The prompt works whether cpflow is already installed or the agent needs to install it first. If cpflow is already available in the target repo, cpflow ai-github-flow-prompt prints the same prompt with the default app prefix filled in.
See Bootstrap a Project for command details, Repo Readiness Checklist for what "ready" means, and AI Playbook to run the rollout through an agent.
Bootstrap a Project
Run these commands from the project root:
# Check the repo for common rollout blockers before generating files
cpflow github-flow-readiness
# Print the current AI rollout prompt for this repo, if you want to hand it to an agent
cpflow ai-github-flow-prompt
# Create .controlplane/ if it does not exist yet
cpflow generate
# Add reusable GitHub Actions for the Control Plane flow
cpflow generate-github-actions
# Or, run this instead when staging should trigger from a branch other than main/master:
cpflow generate-github-actions --staging-branch develop
These local bootstrap commands do not require cpln to be installed yet. Install and
log into the Control Plane CLI before any command that talks to the real platform.
cpflow github-flow-readiness is the fastest gate: it exits non-zero when the repo is
missing a production Dockerfile, missing Rails runtime files, pinned to a legacy
Ruby or Bundler toolchain, or depends on exact-pinned gem or npm versions that do
not appear to exist in the public registries.
The second command writes namespaced files so they can coexist with an app's existing CI:
.github/cpflow-help.md.github/workflows/cpflow-review-app-help.yml.github/workflows/cpflow-help-command.yml.github/workflows/cpflow-deploy-review-app.yml.github/workflows/cpflow-delete-review-app.yml.github/workflows/cpflow-deploy-staging.yml.github/workflows/cpflow-promote-staging-to-production.yml.github/workflows/cpflow-cleanup-stale-review-apps.yml.github/actions/cpflow-*(the local composite actions used by those workflows)bin/pin-cpflow-github-refbin/test-cpflow-github-flow
cpflow generate also infers the app prefix from the repo directory, infers the
Docker base Ruby version from .ruby-version, .tool-versions, or the app's
Gemfile, preserves repo-defined frontend precompile hooks such as Shakapacker
precompile_hook commands or React on Rails auto bundle generation, and
switches to persistent SQLite db and storage templates when
config/database.yml shows SQLite in production.
cpflow github-flow-readiness checks public RubyGems and npm registry metadata
for exact-pinned direct dependencies. In air-gapped or egress-restricted
environments those checks may report unknown instead of failing hard; confirm
the deployment runner has the package access your app needs before rollout.
Repo Readiness Checklist
Before generating this flow, confirm that the target repository is already a deployable application rather than a partial sample:
- the repo can be cloned and installed from scratch with published gem and npm package versions
- the repo does not depend on unpublished or inaccessible package versions unless the deployment flow also provisions the credentials needed to fetch them
- the repo is not just a historical generator snapshot pinned to an obsolete Ruby or Bundler toolchain with no validated production build path
- the app has its real runtime scaffold checked in, for example a complete Rails
app with the boot files needed to run
bin/railsandbin/dev - the repo root maps to one deployable app; multi-app monorepos need a separate rollout decision before using this one-app-per-repo flow
- the production Dockerfile can build the app's assets and any SSR or renderer bundles that production needs
- any repo-defined frontend codegen or precompile hooks are preserved before
rails assets:precompile - the runtime workloads, release command, and required secrets are known well
enough to model in
.controlplane/
If any of those fail, stop and fix the application first. Do not merge
cpflow-* workflows into a repository that is not yet runnable from a clean
clone, because the result will be a misleading "deployment flow" for an app that
still cannot build or boot.
Required .controlplane/controlplane.yml Structure
The generated workflows assume that .controlplane/controlplane.yml defines:
- one staging app
- one review-app prefix with
match_if_app_name_starts_with: true - one production app with
upstreampointing to staging
Typical shape:
aliases:
common: &common
cpln_org: my-org-staging
default_location: aws-us-east-2
setup_app_templates:
- app
- postgres
- redis
- rails
app_workloads:
- rails
additional_workloads:
- postgres
- redis
apps:
my-app-staging:
<<: *common
my-app-review:
<<: *common
match_if_app_name_starts_with: true
hooks:
post_creation: bundle exec rails db:prepare
# pre_deletion intentionally omitted for shared databases: `cpflow delete` runs it before removing the workloads,
# so live connections can block the drop. Prefer admin-side cleanup. See docs/tips.md ("Share One Control Plane Postgres").
my-app-production:
<<: *common
allow_org_override_by_env: false
allow_app_override_by_env: false
cpln_org: my-org-production
upstream: my-app-staging
release_script: release_script.sh
Important points:
match_if_app_name_starts_with: trueis what allows a single config entry to backmy-app-review-123,my-app-review-456, and cleanup commands likecpflow cleanup-stale-apps -a my-app-review.- Review-app deploy, delete, and cleanup workflows infer the review app prefix from the single app entry with
match_if_app_name_starts_with: true. - Review-app workflows infer the staging Control Plane org from that review app entry's
cpln_org. upstream: my-app-stagingis what lets the production promotion workflow copy the exact staging artifact.- If your main web workload is not named
rails, set the optionalPRIMARY_WORKLOADrepository variable described below. - For public demos, starter staging apps, and long-lived review apps, prefer
capacityAI: truewith one warm replica and the workload's autoscaling metric disabled, so Control Plane can right-size CPU and memory allocation at that fixed replica count. See Enable Capacity AI for Demo and Starter Staging Apps.
GitHub Actions Secrets and Variables
GitHub Actions Secrets
For a normal generated review-app setup, configure one GitHub Actions secret:
CPLN_TOKEN_STAGING: service-account token scoped to the staging Control Plane org on controlplane.com. Its policies should allow only review/staging CI operations; it must not read production secrets or manage production workloads.
GitHub Actions Variables
No GitHub Actions variables are required for review apps when .controlplane/controlplane.yml
has exactly one review app entry with match_if_app_name_starts_with: true and
that entry has a cpln_org. The inferred values come from that config file:
the review-app prefix is the app key with match_if_app_name_starts_with: true,
and the staging org is that app's cpln_org value. Set these variables only
when you need to test a fork or clone against a different Control Plane org,
choose a different review-app prefix, expose a different public workload, or
disambiguate generated review-app config:
CPLN_ORG_STAGING: Control Plane org on controlplane.com for staging and review apps. Overrides the org inferred fromcpln_org, for examplecompany-stagingREVIEW_APP_PREFIX: override the inferred review-app prefix; required only when multiple review app prefixes exist incontrolplane.ymlPRIMARY_WORKLOAD: override the public workload used to discover the public endpoint and do review/production health checks; defaults torailsREVIEW_APP_HEALTH_CHECK_RETRIES: override review-app health polling attempts; defaults to24REVIEW_APP_HEALTH_CHECK_INTERVAL: override seconds between review-app health attempts; defaults to15REVIEW_APP_HEALTH_CHECK_ACCEPTED_STATUSES: override space-separated healthy HTTP statuses for review apps; defaults to200 301 302REVIEW_APP_HEALTH_CHECK_CURL_MAX_TIME: override per-request review-app curl timeout in seconds; defaults to10
If controlplane.yml defines more than one app with
match_if_app_name_starts_with: true, inference intentionally fails. Set
CPLN_ORG_STAGING and REVIEW_APP_PREFIX to tell the workflow which review-app
family to manage.
For staging deploys, also configure:
CPLN_ORG_STAGING: staging org name, for examplecompany-stagingSTAGING_APP_NAME: staging GVC name, for examplemy-app-stagingSTAGING_APP_BRANCH: optional branch that auto-deploys staging. If you use a custom branch, either pass it tocpflow generate-github-actions --staging-branch BRANCHduring generation or editcpflow-deploy-staging.ymlso itson.push.brancheslist includes the same branch.
For production promotion, also configure:
- a GitHub Environment named
production - required reviewers on that environment, limited to the people or team allowed to promote production
- "Prevent self-review" on that environment, so the person who starts the promotion cannot approve it
- disable administrator bypass if your organization requires two-person control
- restrict deployment branches/tags to your protected release branch
CPLN_TOKEN_PRODUCTIONas an environment secret onproduction, not as a repository or organization secretCPLN_ORG_PRODUCTIONas a production environment variable, for examplecompany-productionPRODUCTION_APP_NAMEas a production environment variable, for examplemy-app-production
Enter GitHub variables such as CPLN_ORG_STAGING,
CPLN_ORG_PRODUCTION, STAGING_APP_NAME, and PRODUCTION_APP_NAME
as plain single-line values. The generated production promotion workflow trims
accidental leading/trailing whitespace and line endings from Control Plane org
names before building registry URLs, but embedded line breaks are rejected
because they could change the target org name after normalization.
Production promotion copies the exact image currently deployed on the selected
staging workload. If that staging image is digest-pinned, the digest is used for
the source copy while the production tag is derived from the tag portion. Tags
with a _<commit> suffix keep that suffix in production; plain numeric tags are
also valid and promote to the next plain production tag. The copy step uses
docker buildx imagetools create --prefer-index=false --tag with isolated
Docker credentials, which preserves multi-architecture manifests, preserves
single-platform manifest format when supported, and avoids pulling image layers
onto the GitHub Actions runner.
Before copying the image, production promotion compares the environment variable names exposed by staging and production at both the GVC level and each configured app workload's container level. Variables present in staging are treated as required for production, while production-only variables emit warnings. A missing production workload variable such as a renderer password or runtime secret fails the promotion before the image copy starts.
Do not put CPLN_TOKEN_PRODUCTION in repository or organization secrets for
sensitive production systems. Production promotion intentionally runs as a
normal caller-repo workflow job with environment: production, then checks out
the pinned control-plane-flow release for shared actions. GitHub exposes the
production token to that job only after the production environment gate.
GitHub does not expose which secret scope supplied a nonempty value at runtime,
so a broader repository or organization secret with the same name can mask a
missing environment secret. Keep the production token absent from broader secret
scopes.
Do not move production promotion behind a cross-repo reusable workflow. GitHub
does not expose the caller repository's environment secrets to that called
workflow, so secrets.CPLN_TOKEN_PRODUCTION remains empty even when the
production Environment contains the secret. Generated reusable-workflow
callers still pass only the named secrets each upstream workflow needs and do
not use secrets: inherit; production promotion is the caller-owned exception.
If promotion fails in the Validate production token step with
CPLN_TOKEN_PRODUCTION is not set. Add it as a secret on the 'production' GitHub Environment.,
check the environment scope first. Also verify that the promote-to-production
job declares environment: production and that no same-named repository or
organization secret exists. Create or verify the environment secret with:
You need permission to manage repository environments and secrets to run these
commands.
gh secret set CPLN_TOKEN_PRODUCTION --repo OWNER/REPO --env production
# Paste the token value when prompted.
gh secret list --repo OWNER/REPO --env production
gh secret list --repo OWNER/REPO
gh secret list --org OWNER | grep '^CPLN_TOKEN_PRODUCTION[[:space:]]' || true
First-Time Control Plane Bootstrap
GitHub settings only give the workflows permission to act. They do not create the persistent staging or production GVCs for you on the first merge.
Before the first staging deploy, bootstrap the staging app once:
cpflow setup-app -a my-app-staging --org my-org-staging --skip-post-creation-hook
setup-app reads the setup_app_templates list from
.controlplane/controlplane.yml. It creates the persistent staging GVC,
workloads, app identity, app secret dictionary, app secret policy, and policy
binding that grants the app identity reveal permission on that dictionary.
Use --skip-post-creation-hook for first-time bootstrap so a database hook does
not try to run before the first image exists.
After the persistent app exists, use apply-template for later template
updates. Adjust the template list to match your repo, such as adding worker,
sidekiq, renderer, redis, or other templates present under
.controlplane/templates:
cpflow apply-template app postgres rails -a my-app-staging --org my-org-staging --yes --add-app-identity
If you use apply-template to create or repair an existing app, also confirm
that the app identity has reveal permission on the app secret policy. Without
that binding, workloads that reference cpln://secret/<app-secrets>.* stay
paused until the policy is fixed.
Before the first production promotion, run the same kind of bootstrap for the production app in the production org:
cpflow setup-app -a my-app-production --org my-org-production --skip-post-creation-hook
Use production-only runtime secrets and values for the production app. The
protected GitHub Environment controls who can run the promotion workflow, but
the production app resources still need to exist before the first promotion.
After bootstrap, populate the production app secret dictionary with the values
referenced by .controlplane/templates, then run cpflow apply-template against
production when templates change so the workload env references remain persisted.
Production promotion checks for missing GVC and workload container env names
before copying the staging image, so a staging-only runtime variable will stop
the run early instead of deploying an image that cannot boot.
Review apps are different: the generated +review-app-deploy workflow creates
temporary PR apps as needed, including the identity and secret policy binding.
For an existing review app, each deployment runs
cpflow setup-app --refresh-templates before building and deploying the new
image. Refresh mode reapplies the configured setup_app_templates without
deleting the GVC or running hooks.post_creation, answers template replacement
prompts noninteractively, preserves each matching workload container's exact
configured app-image reference even when the app is unhealthy or workloads use
different image versions, and repairs the configured app and shared-secret
policy bindings. A missing or invalid workload image uses only an unambiguous
app image from ready workloads; refresh fails closed rather than selecting the
registry's newest image. Preserving image references keeps release-phase and
ordered-deploy gates in control of image rollout; a template failure stops the
workflow before the image build or deployment. Existing secret templates are skipped
as whole resources, so refresh neither changes existing values nor adds newly templated
keys; provision required new keys separately before deployment.
You still need the shared review-app runtime secret values described by your
templates, and the staging token must have access to create and update
review-app GVCs, workloads, images, identities, policies, and secrets in the
staging org.
If review apps share an existing staging database or another existing secret,
declare it with shared_secret_grants on the review app config entry. The
deploy workflow runs setup-app for new review apps, refreshes templates for
existing review apps, and then runs deploy-image; those commands bind or
repair the review app identity's reveal permission on each configured shared
policy. The delete and cleanup workflows call cpflow delete, which removes
those bindings as review apps go away. This lets one shared database or license
secret serve many short-lived review apps without granting every review
identity access to unrelated app secrets.
apps:
my-app-review:
match_if_app_name_starts_with: true
shared_secret_grants:
- name: database
secret_name: my-app-review-database-secrets
policy_name: my-app-review-database-secrets-policy
Then reference cpln://secret/{{SHARED_SECRET_DATABASE}}.DATABASE_URL from the
workload template.
Production Promotion Safety
CPLN_TOKEN_PRODUCTION can change live production workloads, images, releases,
and rollback state. Treat it differently from review-app and staging credentials.
The standard path is:
- Create the
productionGitHub Environment before setting the production token. - Add a small required-reviewer list or team with production authority.
- Enable prevent self-review.
- Disable administrator bypass if your org policy requires two-person control.
- Restrict deployable branches or tags to the protected release branch.
- Store
CPLN_TOKEN_PRODUCTIONonly as aproductionenvironment secret. - Store
CPLN_ORG_PRODUCTIONandPRODUCTION_APP_NAMEasproductionenvironment variables, or as repository variables only when those names are intentionally non-sensitive. - Keep GitHub variable values single-line; a pasted trailing newline is trimmed for Control Plane org names, but embedded line breaks are rejected before deployment, copy, health-check, or rollback steps run.
- Bootstrap or re-apply the persistent production app templates before first promotion so app workload container env references and Control Plane secret dictionaries exist in production.
- Expect promotion to preserve the selected staging image reference. Digest references are copied by digest, commit-suffixed tags keep the commit suffix, and plain numeric tags remain valid.
- Expect production health and rollback readiness polling to require Control
Plane
status.readyandstatus.readyLatestbefore checking the standard workload endpoint. If that endpoint is unavailable, as it can be on BYOK locations, the health check waits until every reported deployment location is ready and not deploying, then tries their endpoints sequentially. Each additional location can add up tocurl_max_timeto an attempt.
GitHub only exposes environment secrets to jobs that reference the environment
after configured protection rules pass. GitHub does not allow a caller job that
directly invokes a reusable workflow to set environment, and cross-repo
reusable workflows do not receive the caller repository's environment secrets.
For that reason, generated production promotion stays as a normal caller-repo
job with environment: production. See GitHub's docs for
managing environments,
deployment protection rules,
and reusable workflow limitations.
Application runtime secrets such as SECRET_KEY_BASE, API keys, or private
license keys belong in Control Plane secret dictionaries referenced by
controlplane.yml. They are not GitHub repository variables unless your
Docker build itself needs them.
Recommended org layout:
- keep review apps and staging in a staging org that developers can access
- keep production in a separate org with tighter access controls
- for public repositories, use a staging/review token generated from a service account whose policies only allow review/staging CI operations; use a dedicated review-app org only if you also customize the generated workflow/configuration to target that org separately
Optional repository secret for private dependency builds:
DOCKER_BUILD_SSH_KEY: private SSH key used when the Dockerfile needsRUN --mount=type=sshto fetch private GitHub dependencies during image build
Optional repository variables for private dependency builds:
DOCKER_BUILD_EXTRA_ARGS: optional newline-delimited singledocker buildtokens passed through tocpflow build-image, for example--build-arg=FOO=baror--secret=id=npmrc,src=.npmrcDOCKER_BUILD_SSH_KNOWN_HOSTS: optional multi-lineknown_hostscontent used withDOCKER_BUILD_SSH_KEYwhen the build needs SSH access to hosts other than GitHub.com
Advanced optional repository variables:
REVIEW_APP_DEPLOYING_ICON_URL: custom image URL for the animated icon in review-app PR comments. Ignore this for the standard setup; it is cosmetic only.CPLN_CLI_VERSION: pin only when Control Plane CLI compatibility requires it.CPFLOW_VERSION: pin a published RubyGems version only when intentionally overriding the default build-from-ref behavior.
Review App Security for Repositories with External Contributors
Review-app deployment and teardown can execute pull request code. Even when the workflow itself is trusted, the
Dockerfile, package scripts, Rails initializers, server-rendering code, application runtime, any release_script or
hooks.post_creation defined in the PR's .controlplane/controlplane.yml, and .controlplane/templates/*.yaml identity
and policy templates applied by cpflow setup-app at first deploy with CPLN_TOKEN_STAGING can all be changed by the
pull request being deployed. Teardown can also run a hooks.pre_deletion command through the latest PR-built image, even
when the hook command comes from the base-branch config. A PR author can embed malicious code in that image; at runtime
the code executes inside the review app workload and can read any secrets mounted into the workload environment.
The generated setup action also exports CPLN_TOKEN_STAGING as CPLN_TOKEN via GITHUB_ENV, making it available in the
process environment of every subsequent runner step for the rest of the job, so keep any custom runner steps after setup
trusted. PR-controlled release_script and hook commands normally run through cpflow run in remote Control Plane
containers from the latest image, not as runner shell steps, so they do not read that runner token directly unless a
custom workflow passes a local token through. Inside the Control Plane workload, a separate runtime CPLN_TOKEN is
available only when the workload spec has an identityLink; skip_secrets_setup skips automatic identity creation and
binding, but templates can still attach an identity. Because cpflow setup-app reads controlplane.yml from the PR
checkout, a PR can also set skip_secrets_setup: false or omit it to re-enable automatic identity binding unless the
trusted workflow passes --skip-secrets-setup as a CLI flag. Do not treat skip_secrets_setup in controlplane.yml as
a token-removal control or reliable security boundary. This is why workload-mounted secrets and the staging
service-account token must remain disposable and scoped to minimum permissions.
The generated flow uses these defaults:
- same-repository pull requests can update existing review apps automatically on each push; creating the first review app
requires either a
+review-app-deploycomment from someone whose current repository permission iswrite,maintain, oradmin, or a manual workflow dispatch by a repository collaborator with write access. GitHub's collaborator-permission API resolves comment permissions when the command runs;readandtriageaccess are not sufficient, and an API lookup failure denies the command. Accepted triggers are recorded in a hidden, bot-authored intent comment before queue admission. The mutating job authenticates the newest marker against its originating Actions run and successful POST-only recording step, then rechecks the manual actor's current permission after it acquires the per-PR queue. Editing or deleting the original command comment therefore cannot reorder accepted work, and a permission revoked while the job waits makes the newest intent fail closed instead of falling back to an older operation. This permission gate applies to every+review-app-deploycomment, whether or not a review app already exists. Later pushes to a base-repository branch PR redeploy automatically without another approval because the auto-push path (pull_requestevent) does not use the comment permission gate. The reusable deploy workflow loads generated local actions throughactions/checkout's default with noref:override, which resolves to the commit GitHub recorded for the triggering event: the pull-request merge revision for automatic same-repository deploys, the selected ref for manual dispatch, or the default-branch revision for comment triggers. This keeps the wrapper and generated actions synchronized during an upgrade or first-installation PR. Preserve the same-repository caller guard because pull-request workflow and action code run with staging/review credentials; - fork pull requests cannot deploy via the generated
pull_requestpath because the caller workflow's job-levelif:condition explicitly skips fork-originated runs. Forissue_commentevents, the callerif:restricts invocation to the exact command shape; the reusable workflow resolves repository permission before its deploy job runs, then its source-validation step checks fork status before checking out or building PR code. These complementary guards cover different axes, so preserve both. Even a write-authorized comment on a fork PR cannot deploy that fork head. Removing the source-validation guard opens a path to deploy untrusted code with repository-secret access, becauseissue_commentevents execute with base-repository secret access. Removing thepull_requestguard still lets untrusted fork code into the staging environment even though GitHub withholds repository secrets from forkpull_requestruns. Keep both workflow guards in place because the workflow builds Docker images with repository secrets; - review apps are also deleted automatically when the pull request closes; that PR-close path uses
pull_request_targetso it runs in the base-repository context and has repository-secret access for teardown. That is also why you must never check out PR or fork code in this job; see the customization guidance below. The PR-close path does not require a comment permission check. The generatedcpflow-delete-review-app.ymlpinsGITHUB_TOKENpermissions to the minimum it needs (actions: write,contents: read,deployments: write,issues: write,pull-requests: write).actions: writeis used only to redispatch the newest accepted deploy/delete operation when GitHub replaces the corresponding pending run; if you customize this workflow, preserve thatpermissions:block because omitting it can fall back to broader repository defaults; - manual workflow dispatch by a repository collaborator can also delete a review app without a
+review-app-deletecomment. It is recorded in the same intent order as comment and automatic triggers, and the actor's current repository permission is rechecked after queue admission; - write-authorized comments on fork PRs still do not deploy the fork head; the workflow posts no PR comment or command
reaction in this case. The authorization job records no accepted intent, and the skip appears in its
Prepare accepted review app intentlog. Review the fork code carefully, then move the change to a branch in the base repository if it needs a generated review app. That build will run with repository-secret access; - production promotion is manual and uses production environment secrets separately from review and staging.
The generated review-app workflow targets the staging/review Control Plane org inferred from cpln_org in
controlplane.yml, or overridden by CPLN_ORG_STAGING, and uses the token from CPLN_TOKEN_STAGING. A fully separate
review-app org or token requires workflow/configuration customization; otherwise, keep review apps and staging in the
generated staging/review org and make that token disposable and unable to access production resources.
The PR-close teardown workflow runs trusted base-branch workflow code with repository secret access so it can delete fork
PR review apps. The generated cpflow-delete-control-plane-app composite action script refuses to call cpflow delete
on any app whose name does not match the review-app prefix. This shell-level guard is effective because the generated
delete workflow loads generated actions through actions/checkout's default with no ref: override, which resolves to
the commit GitHub recorded for the triggering event (GITHUB_SHA, the base-branch tip under pull_request_target). Its
separate app checkout has no ref: override either, so pull_request_target teardown uses base-branch code for both
paths rather than PR or fork code. If you customize this
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
script itself and would also make hooks.pre_deletion come from the PR's controlplane.yml. This is still not a token
policy, so use a scoped staging service account limited to review/staging operations. A configured
hooks.pre_deletion command still runs through the latest PR-built image on all delete paths: PR-close teardown,
+review-app-delete comments, manual dispatch, and scheduled cleanup. Review-app credentials must remain disposable
even during deletion.
If you customize the generated pull_request_target workflow, never pass github.event.pull_request.head.sha or another
fork-controlled ref to actions/checkout, git fetch, git merge, git cherry-pick, or any other step that fetches,
materializes, or executes the ref. Those operations run untrusted fork code with repository secret access. Referencing the
SHA as an opaque identifier for deployment status updates, comments, or API calls is safe because SHA values are hex-only
and cannot contain shell metacharacters. Do not apply that reasoning to other attacker-controlled PR event fields such as
head.ref, head.label, title, body, head.repo.full_name, or repository descriptions; pass user-controlled strings
through environment variables instead of interpolating them directly into run: steps.
These defaults protect repository secrets from direct fork PR execution, but they do not make deployed PR code harmless. For repositories with external contributors, keep review-app credentials and runtime values disposable:
- do not mount production secrets, staging customer data, package-publishing tokens, payment keys, or monitoring tokens into review apps;
- do not use
DOCKER_BUILD_SSH_KEYwith a long-lived personal key or broad deploy key; see Docker Builds with Private Dependencies for the minimum-access deploy-key requirements; - do not use broad Control Plane
superuserstokens for review-app CI; use a review/staging service account scoped only to review/staging CI operations; - keep review databases, Redis instances, object stores, and renderer credentials separate from staging and production;
- rotate any credential that may have been exposed to a malicious review-app build;
- use scheduled cleanup so stale review apps stop consuming compute and secrets access. Cleanup deletes apps through
cpflow delete, so any configuredhooks.pre_deletionstill runs through the latest PR-built image; review-app credentials must remain disposable for this path too.
Secret indirection such as cpln://secret/... protects values in Control Plane configuration. It does not protect a
value after that secret is mounted into a workload that runs pull request code. See
Secrets and ENV Values - Review app secrets for review-app secret
handling guidance.
Docker Builds with Private Dependencies
Some apps need extra Docker build configuration before the generated workflows are turnkey. Common examples are:
pnpm,npm,yarn, or Bundler dependencies pulled from private GitHub repositories- Dockerfiles that already use
RUN --mount=type=ssh - builds that need extra
--build-arg,--secret, or relateddocker buildflags
The generated cpflow-build-docker-image action supports this without hardcoding app-specific logic:
- set
DOCKER_BUILD_SSH_KEYif the Docker build needs SSH access to GitHub - optionally set
DOCKER_BUILD_SSH_KNOWN_HOSTSwhen the SSH build host is not GitHub.com or you need custom host entries - set
DOCKER_BUILD_EXTRA_ARGSwhen you need extradocker buildflags
For example, a repo that installs private dependencies from GitHub during Docker build can set:
DOCKER_BUILD_SSH_KEY=<private deploy key secret>
DOCKER_BUILD_SSH_KNOWN_HOSTS=git.example.com ssh-ed25519 AAAA...
DOCKER_BUILD_EXTRA_ARGS=--build-arg=BUNDLE_WITHOUT=development:test
The action will start an SSH agent, add the key, write known_hosts, and pass --ssh=default to cpflow build-image. When DOCKER_BUILD_SSH_KNOWN_HOSTS is unset, the generated action uses pinned GitHub.com host keys by default. If your Dockerfile relies on RUN --mount=type=ssh, validate the build locally with cpflow build-image -a <app> --ssh=default before relying on CI.
Do not configure DOCKER_BUILD_SSH_KEY unless the Dockerfile truly needs it. When configured, it is available to all
review-app and staging Docker builds; follow the review-app security guidance above by using a read-only, revocable
deploy key scoped to the minimum private dependency access, and never use a personal SSH key.
Generated Workflow Behavior
The deploy and delete workflows share a per-PR concurrency group, but GitHub keeps at most one running and one pending
member of a group and may replace the pending member without FIFO ordering. Each authorized trigger therefore records a
hidden github-actions[bot] intent marker containing its operation and originating workflow-run identity before it joins
the queue. Whichever job survives reads the complete marker ledger, authenticates the newest marker against the Actions
run API and the source run's successful POST-only recording step, revalidates a manual actor's current permission, and either performs that operation or redispatches the matching
generated workflow on the default branch. Internal redispatches reuse the existing marker only through a bot-owned
handoff bound to the returned workflow-run ID and the exact successful source dispatch step; the superseded source job
then stops before mutable work, and values entered manually in the internal handoff field are rejected. This convergence
covers automatic PR events, manual dispatches, mixed-case comment admission, authorization
completion in a different order from event creation, and edits or deletion of the original command comment. Invalid,
forged, missing, or permission-revoked newest intents fail closed and never fall back to older work.
cpflow-review-app-help.yml
- Posts a quick reference when a pull request opens, including on fork-based PRs.
- This is an onboarding comment only; it does not checkout PR code or receive Control Plane secrets. Remove this wrapper if a repo does not want automatic review-app command help on every new PR.
cpflow-help-command.yml
- Replies to
+review-app-helpon a pull request with the commands and required repo settings.
cpflow-deploy-review-app.yml
- Creates a review app when someone comments
+review-app-deployor via manual workflow dispatch by a repository collaborator. - For manual dispatch, provide the PR number; the workflow rejects fork PRs at runtime because it builds Docker images with repository secrets.
- Redeploys an existing review app automatically on later PR pushes.
- Creates a transient, non-production GitHub deployment and comments with the review URL and logs.
- Leaves PR pushes alone until the first review app is explicitly requested, which keeps demo-app costs down.
- When that no-app PR path succeeds without building or deploying, writes a prominent
Docker image not builtjob summary and returns the reusable-workflow outputimage_built=false. A downstream job can inspectneeds.deploy.outputs.image_built; a green deploy job withfalseis not Docker-image validation. - Repositories that require production-image validation before merge should add a separate required build job when
Dockerfile, runtime-version, or dependency files change. The cost-saving review-app path intentionally does not create
an app or build an image until
+review-app-deployis requested. - Supports cost-conscious review apps when paired with one warm replica, Capacity AI, and a disabled autoscaling metric for public demos, starter staging apps, and long-lived review apps; see Enable Capacity AI for Demo and Starter Staging Apps.
- Accepts
+review-app-deployonly when GitHub reports the commenter haswrite,maintain, oradminrepository permission.readandtriageare denied, permission is checked again after queue admission, and lookup failures fail closed. Manual dispatch follows the same post-queue permission rule. - Skips fork-based PR deploys because the workflow builds Docker images with repository secrets. An authorized comment on a fork PR still does not deploy the fork head, and manual dispatch must use a base-repository PR number. A fork-targeted request records no accepted intent and skips the mutating job. To give a fork PR a review app, review the code carefully first, then move the change to a branch in the base repository. The build will then run with repository-secret access.
- Rejects comment, manual, and internal deploy requests for a closed pull request before recording a durable intent, then rechecks that the pull request is still open after queue admission. A doomed deploy therefore cannot supersede the automatic close-triggered deletion that cleans up an existing review app.
cpflow-delete-review-app.yml
- Deletes the review app on
+review-app-delete. - Also supports manual workflow dispatch by a repository collaborator. The comment-time permission gate does not apply, but the dispatching actor's repository permission is rechecked after queue admission.
- Also deletes it automatically when the pull request closes through a
pull_request_targetevent, so repository secrets are available for teardown;hooks.pre_deletionstill executes through the latest PR-built image on this path, so review-app credentials must remain disposable. - After Control Plane deletion succeeds, marks every GitHub deployment for the exact
review/<app-name>environment inactive so the repository does not retain a stale active review deployment, while skipping deployments whose latest status is already inactive so repeated teardown is idempotent. Deploy and delete workflows share one per-PR concurrency queue; the accepted-intent reconciliation described above compensates for GitHub replacing pending runs, ensuring the newest accepted operation eventually wins and an older deploy cannot publish the final state after a newer deletion. If GitHub deployment cleanup fails after Control Plane deletion, the workflow reports that partial outcome accurately and still fails so the cleanup can be retried. - Accepts
+review-app-deleteonly when GitHub reports the commenter haswrite,maintain, oradminrepository permission.readandtriageare denied, permission is checked again after queue admission, and lookup failures fail closed. Manual dispatch uses the same post-queue actor check; automatic PR-close teardown does not use a manual actor permission check.
cpflow-deploy-staging.yml
- Builds and deploys the staging app on pushes to the generated staging branch filter.
- Falls back to
mainormasterwhenSTAGING_APP_BRANCHis unset and no custom branch was generated. - Custom staging branches must be present in the workflow's
on.push.branchesfilter; repository variables alone cannot trigger a branch GitHub Actions is not listening to. - Fails fast when required staging repo settings are missing instead of surfacing opaque
cpflowerrors.
cpflow-promote-staging-to-production.yml
- Manually promotes the staging artifact to production with a confirmation input.
- Runs the production job in the
productionGitHub Environment, so configured reviewers approve the job before production environment secrets are available. - Verifies that production has the GVC and app workload container env var names staging expects.
- Runs a health check against
PRIMARY_WORKLOADonly after Control Plane reports the latest workload version ready. - Attempts a rollback of every configured application workload if the new production image does not come up healthy.
- Creates a GitHub release after a successful promotion.
cpflow-deploy-review-app.yml
- Builds and deploys the pull request image after the review app exists or is created.
- Waits for
PRIMARY_WORKLOADto reportreadyLatest=trueand return an accepted HTTP status before marking the GitHub deployment successful. - Uses
REVIEW_APP_HEALTH_CHECK_*repository variables to tune review-app health checks independently from production promotion.
cpflow-cleanup-stale-review-apps.yml
- Runs nightly and on demand.
- Deletes stale review apps using
cpflow cleanup-stale-apps.
Generated review app names use <review-app-prefix>-<PR number>, for example
my-app-review-123. If an existing repository is migrating from older local
workflow glue that created names like <review-app-prefix>-pr-123, delete those
old review apps manually after merging the generated flow; the cleanup workflow
only targets the current prefix convention.
To inventory old-prefix review apps before cleanup, run:
cpln gvc query --org <staging-org> -o yaml --prop name~<review-app-prefix>-pr-
The PR-open help workflow posts the short command reference whenever the
generated wrapper exists. That is intentional for configured demo repos. Forks
or clones that copy the workflow before configuring Control Plane can remove
.github/workflows/cpflow-review-app-help.yml or uncomment and adapt the
wrapper-level if: guard shown in that file, for example
vars.REVIEW_APP_PREFIX != '' || vars.CPLN_ORG_STAGING != ''.
Upstream Workflows And Actions
Most generated workflows are intentionally small wrappers. The deployment
logic and comment formatting live in upstream reusable workflows. The canonical
composite actions live in this repository and the gem copies them into each
downstream repository at .github/actions/cpflow-*; the reusable workflows use
those regular local action paths. They separately check out the matching
upstream ref at .cpflow for the cpflow runtime source. Production promotion
is expanded into the caller repository so it can own environment: production,
but follows the same local-action and .cpflow source split.
cpflow-setup-environment: installs Ruby, the Control Plane CLI, andcpflow, then logs into the target org. By default it buildscpflowfrom the checked-out upstreamcontrol-plane-flowref; set theCPFLOW_VERSIONrepository variable only when you want to force a published RubyGems release.cpflow-build-docker-image: builds and pushes the app image with the desired commit SHAcpflow-delete-control-plane-app: safely deletes temporary apps and refuses to touch names outside the configured review-app prefix
Version Pins: GitHub Ref vs RubyGems
Generated cpflow-* workflow files pin shakacode/control-plane-flow from
GitHub, not from the Ruby gem. Reusable workflow wrappers pin that source with
an upstream uses: ref:
uses: shakacode/control-plane-flow/.github/workflows/cpflow-deploy-review-app.yml@<ref>
Production promotion pins the same source in its Checkout control-plane-flow actions step because it is a caller-owned job, not a reusable workflow caller.
Those refs are the downstream lock. GitHub exposes a reusable workflow's own
repository, ref, and SHA to called jobs, so reusable upstream workflows check out
matching control-plane-flow source automatically from that context. Downstream
reusable-workflow wrappers should not pass control_plane_flow_ref; if you see
that input outside the production promotion setup step, regenerate with a newer
cpflow.
There are two coordinated inputs, and they protect different things:
- The GitHub ref locks the reusable workflow and the
.cpflowruntime source checkout that GitHub runs. - The installed Ruby gem supplies the generated local composite actions and
wrappers.
CPFLOW_VERSION, when configured, separately locks the publishedcpflowruntime installed by the setup action.
That means a downstream app cannot rely on the gem alone for GitHub Actions behavior. The safe stable path is still gem-driven for generation, but developers must commit the generated wrappers and local actions from the gem that matches the upstream release tag:
- Publish a
cpflowgem. - Install or bundle that released gem in the downstream project.
- Run
cpflow generate-github-actions. - Commit the generated wrappers and
.github/actions/cpflow-*files; the wrappers should point to the matching upstream release tag such asv5.0.0.
That release tag should point to the same source that produced the RubyGems
release. Downstream production automation should use release tags, not main or
feature-branch refs.
Generated cross-repository reusable-workflow calls deliberately use an exact
release tag such as v5.0.0. This is the intentional downstream exception to
the repository's full-SHA external-action policy: it keeps the installed gem,
generated wrappers, generated local actions, and upstream workflow on one
reviewed release update contract. It does not extend to external actions inside
those workflows, which remain pinned to full commit SHAs with readable release
comments. Unreleased downstream validation temporarily replaces the exact tag
with the upstream PR's full commit SHA as described below; moving version
aliases and branches remain forbidden.
Updating Generated GitHub Actions After Gem Updates
Whenever a downstream repo updates the cpflow gem, update all checked-in
GitHub Actions files in the same PR. The gem version does not make GitHub load
new reusable workflow YAML or local action files by itself; GitHub uses the
workflow ref and .github/actions/cpflow-* files committed in the repository.
For a new repository that does not have generated wrappers yet, run
cpflow generate-github-actions first. Use the update command below for later
gem upgrades.
Use the installed gem to refresh the generated workflows and actions:
cpflow update-github-actions
bin/test-cpflow-github-flow
If the app runs cpflow through Bundler, use:
bundle exec cpflow update-github-actions
bin/test-cpflow-github-flow bundle exec cpflow
cpflow update-github-actions refreshes local composite actions and helpers.
It preserves every top-level workflow by default, including refs, triggers,
permissions, and downstream deployment ownership. It does not create staging
or production deployment workflows when another CI system owns those deploys.
Select the wrappers you intend to replace explicitly; see
cpflow update-github-actions --help for the current interface:
bundle exec cpflow update-github-actions --workflows \
cpflow-deploy-review-app.yml cpflow-delete-review-app.yml \
cpflow-cleanup-stale-review-apps.yml cpflow-help-command.yml \
cpflow-review-app-help.yml
Each selected wrapper is replaced in full with the installed gem's template
and release ref. Review the diff and reapply downstream changes before
committing. Unselected wrappers keep their refs: preservation is not evidence
that old wrappers are compatible with new local actions. Migrate wrapper
contracts and source pins together, then run the validator in the same PR.
For a selected staging wrapper, a single existing push branch or the default
main/master pair can be inferred. Missing, malformed, or ambiguous branch
configuration requires an explicit --staging-branch BRANCH; that option also
requires selecting cpflow-deploy-staging.yml.
Preserving downstream validation
The generated bin/test-cpflow-github-flow runs baseline checks followed by
an optional executable bin/test-cpflow-github-flow-custom. The extension runs
from the repository root with the cpflow command argv forwarded unchanged
(for example, bundle, exec, cpflow), and its nonzero exit fails validation.
The generator never creates or overwrites the custom extension. Baseline
production checks apply when the generated production workflow exists;
downstream-owned production workflows belong in the custom checks.
An existing validator that differs from the installed template blocks the entire update before any writes. This includes unmodified validators from older releases: the updater cannot safely distinguish them from custom code. For a legacy installation such as HiChee's 5.3-to-6.0 RC migration:
- Save the old validator in version control and compare it with its original
release. Extract the downstream checks into
bin/test-cpflow-github-flow-customand make it executable. If it is entirely downstream-specific, rename it directly. Remove any call back to the generated validator to avoid recursion. - Remove or rename the old
bin/test-cpflow-github-flowafter preserving those checks, then run the update command with the selected wrappers. No override flag bypasses this check. Do not usegenerate-github-actions --forceas a migration shortcut: it intentionally replaces the full generated flow. - Run the new validator. Confirm that the custom checks execute and that a failing custom check still fails the command. Repeating the updater with the same selected workflows should produce no further diff.
Migrating trusted action allowlists
Repositories using a closed trusted_actions policy must review these exact
repositories used by the complete generated output:
trusted_actions:
- actions/checkout
- docker/setup-buildx-action
- ruby/setup-ruby
- shakacode/control-plane-flow
Merge the needed entries into your existing policy rather than replacing it.
ruby/setup-ruby is used by the generated local setup action, including in
review-app-only installations. actions/checkout and docker/setup-buildx-action
are used directly by the generated production wrapper;
shakacode/control-plane-flow supplies the reusable wrappers. Other existing
downstream workflows may need additional entries. Upstream reusable workflows
also use actions/github-script; reviewing that upstream implementation remains
part of trusting shakacode/control-plane-flow.
Allowlisting does not waive immutable pin requirements. For a policy requiring
full SHAs, resolve and independently verify the chosen upstream release tag or
test commit, then run bin/pin-cpflow-github-ref --version <reviewed-version-tag> <full-sha>. The helper writes that version/tag beside all cpflow SHA pins and
updates obsolete same-line comments. The label is supplied by the caller; the
helper does not contact GitHub or certify tag-to-SHA identity. For an unreleased
test commit, use its source version tag as context and record the exact commit
in the PR. Review the generated diff and run your repository's action security
scanner after updating its allowlist.
When keeping cpflow in an app Gemfile, leave a comment next to the gem entry
so future dependency bumps include the wrapper update:
# After bumping cpflow, run:
# bundle exec cpflow update-github-actions
# bin/test-cpflow-github-flow bundle exec cpflow
gem "cpflow", "5.0.1"
CPFLOW_VERSION is a runtime override. If a downstream repository sets the
CPFLOW_VERSION variable, the setup action runs gem install cpflow -v <version>. If it is unset, the setup action builds cpflow from the checked-out
control-plane-flow source selected by the reusable workflow's own SHA. For
normal releases, leave CPFLOW_VERSION unset while pinning the wrappers to the
matching v<version> tag, or set CPFLOW_VERSION to that same released gem
version without the leading v.
When setting CPFLOW_VERSION, use RubyGems version syntax, for example
5.0.0 or 5.0.0.rc.1; do not use v5.0.0 or dash-separated prereleases
because the value is passed directly to gem install cpflow -v.
The setup action fails early when CPFLOW_VERSION and the reusable workflow tag
are out of sync. CPFLOW_VERSION=5.0.0 is accepted only when the wrapper uses a
release tag such as @v5.0.0 (or GitHub resolves it to refs/tags/v5.0.0).
Release tags may use dot- or dash-separated prerelease suffixes, such as
v5.0.0.rc.1 or v5.0.0-rc.1; the gem version should still use dots. The
action also checks the remote control-plane-flow tag and the checked-out
action commit, so a moving branch named like v5.0.0 cannot be used with
CPFLOW_VERSION=5.0.0. That tag check uses outbound HTTPS to GitHub; restricted
runners that cannot reach GitHub should leave CPFLOW_VERSION unset and build
cpflow from the checked-out ref instead. When testing an unreleased upstream
commit SHA, leave CPFLOW_VERSION unset so the workflow builds cpflow from the
same source that supplies the reusable workflow. Refresh the generated local
actions from that checkout before testing as described below.
Testing Unreleased Upstream Changes Downstream
You can test a control-plane-flow PR in a downstream app before merging or
releasing it. Use an immutable commit SHA from the upstream PR branch:
-
Push the upstream PR branch and copy its full 40-character head SHA.
-
In a downstream test branch, regenerate from the upstream checkout and then pin the wrappers to its exact SHA:
ruby /path/to/control-plane-flow/bin/cpflow update-github-actions
bin/pin-cpflow-github-ref --version <reviewed-version-tag> <upstream-pr-sha>The update command copies the PR checkout's canonical composite actions into
.github/actions/cpflow-*. The pin helper then updates every generated reusable-workflowuses:ref plus the production workflow's pinnedcontrol-plane-flowcheckout and setup validation ref. It accepts release tags and full commit SHAs (with a reviewed--versionlabel), rejects branch names such asmainorfeature/foo, and requires--allow-moving-reffor short-lived local experiments that should not be committed. -
Keep
CPFLOW_VERSIONunset so the workflow buildscpflowfrom the pinned upstream SHA. IfCPFLOW_VERSIONis set while the wrapper is pinned to a SHA, the setup action fails before deployment because the published gem and upstream source cannot be proven to match. -
Run:
bin/test-cpflow-github-flowPass a local checkout command when you are validating unreleased generator code before it is installed:
bin/test-cpflow-github-flow ruby /path/to/control-plane-flow/bin/cpflow -
Push the downstream test branch, then choose the remote validation path that matches the repository's installation state.
For an existing installation, where
cpflow-deploy-review-app.ymlis already present on the default branch, dispatch the workflow definition and generated local actions from the unmerged downstream test branch against a suitable open base-repository PR:gh workflow run cpflow-deploy-review-app.yml --ref <downstream-test-branch> -f pr_number=<pr-number>The
--refselects the downstream wrapper and local-action commit under test;pr_numberselects the application source to deploy. Do not use a+review-app-deploycomment for this pre-merge validation:issue_commentalways loads the workflow definition from the default branch, so it cannot validate unmerged wrappers or local actions.For a first installation, GitHub will not dispatch a workflow that does not yet exist on the default branch, even when
--refnames the installation branch. Treat the rollout as staged: run the local contract before merging, review the generated diff, merge it, and dispatch the merged workflow immediately afterward against a suitable open base-repository PR:gh workflow run cpflow-deploy-review-app.yml --ref <default-branch> -f pr_number=<pr-number> -
Verify the dispatched deploy logs show the expected upstream commit SHA, the setup step prints the expected
cpflowsource/version, and the review app URL returns HTTP 200. After merge,+review-app-deployremains the normal maintainer-facing trigger for subsequent review apps. -
After the upstream PR merges and a gem is released, regenerate the downstream wrappers from that released gem and commit the release tag. Use
bin/pin-cpflow-github-ref vX.Y.Zonly for a ref-only update when the generated templates are already current.
For an existing installation, this tests the real reusable workflows, the
unmerged regenerated local composite actions, and the source-built cpflow gem
against one immutable upstream commit. A first installation gets the same remote
proof immediately after its locally validated files reach the default branch.
Both paths avoid testing against a moving upstream branch.
Local Generated-Flow Checks
Run this after generation or after changing a downstream wrapper ref:
bin/test-cpflow-github-flow
The helper runs cpflow github-flow-readiness, parses generated workflow YAML,
checks composite action metadata for literal GitHub expressions in descriptions,
checks that all generated wrappers and the production control-plane-flow
checkout use one upstream ref consistently, rejects broad secrets: inherit
usage in generated cpflow wrappers, rejects obsolete control_plane_flow_ref
wrapper inputs, verifies production promotion remains a caller-owned
environment: production job, and runs
actionlint against .github/workflows/cpflow-*.yml. Its actionlint command
keeps the existing shellcheck ignore and also ignores stale local actionlint
false positives for GitHub's newer reusable-workflow job.workflow_* fields.
Applying This to React on Rails Demo Apps
This flow is a good fit for the React on Rails demo apps because they already follow the same basic assumptions:
- the deployable app is a Rails project
- the primary web workload is usually
rails - review environments should be temporary and opt-in
- staging should auto-follow a single branch
- production should promote the already-tested staging image
In practice, porting the flow into a demo app usually follows five phases.
Before generating:
- Confirm the repo passes the readiness checklist above.
- Generate
.controlplane/if the app does not have it yet. - Generate the
cpflow-*GitHub Actions files.
Verify the generated scaffold:
- Update
.controlplane/controlplane.ymlwith staging, review, and production entries. - Confirm that the generated Dockerfile picked a Ruby base image compatible with the app's declared Ruby requirement.
- For SQLite-backed apps, confirm that the generated scaffold switched to persistent
dbandstoragevolumes, mounted them into the main workload, and added a release script that runsrails db:prepare.
Adapt for the app's runtime:
- Keep Node available in the final app image whenever Rails asset compilation or SSR depends on ExecJS or frontend package managers at build or runtime.
- Preserve repo-defined frontend precompile hooks, such as Shakapacker
precompile_hookcommands or React on Railsconfig.auto_load_bundle = true, beforerails assets:precompile. - Add any additional app workloads the app needs at runtime, for example
sidekiq, a Node renderer, or any other process type that should deploy the same application image. - Adjust
PRIMARY_WORKLOADonly if the public workload is not namedrails.
Wire up GitHub secrets, variables, and private builds:
- Make sure the repo variables and secrets line up with the configured app names. For production promotion, store
CPLN_TOKEN_PRODUCTIONonly on a protectedproductionGitHub Environment with required reviewers. - If the Dockerfile pulls private dependencies over SSH, configure
DOCKER_BUILD_SSH_KEY, addDOCKER_BUILD_SSH_KNOWN_HOSTSwhen the host is not GitHub.com, and validate that the image can build withRUN --mount=type=ssh.
Validate and push:
- Validate the real production Docker build before relying on the workflows, especially if asset compilation or SSR requires Node, extra system packages, multiple processes, extra Docker build flags, or persistent writable paths.
- Expect review app deploys to run only for branches in the base repository; fork PRs still get help comments, but deploys are skipped because the workflow uses repository secrets.
- For public repositories, confirm that review-app secrets are disposable and that
CPLN_TOKEN_STAGINGcannot access production resources.
AI Playbook
If you want an AI agent to apply this flow to another project, use the
standalone AI rollout prompt. It captures the
exact wording, hard stop conditions, and definition of done for this workflow,
and it works whether cpflow is already installed or the agent needs to install
it first. When cpflow is already available in the target repo,
cpflow ai-github-flow-prompt prints the same prompt with that repo's default
app prefix already filled in.
Short version:
Set up Control Plane GitHub Flow for this repo. Start with `cpflow github-flow-readiness` and stop on any reported blockers. The repo must be deployable from a clean clone, with published package versions and a production Dockerfile that can really build the app. Stop and report blockers for unpublished packages, inaccessible private dependencies, legacy toolchains, or missing production build paths instead of generating workflows blindly. Then run `cpflow generate` if `.controlplane/` is missing, run `cpflow generate-github-actions`, adapt the generated scaffold to the real workloads, document the required GitHub secrets and variables, validate the real build path locally, push the branch, and check the GitHub Actions results. Keep production promotion safe by documenting `CPLN_TOKEN_PRODUCTION` as a protected `production` GitHub Environment secret, not a repository or organization secret.
Expand that prompt with app-specific requirements before editing files:
- verify the repo is a real deployable app, not a partial code sample or a demo pinned to unpublished package versions
- stop and report a scope decision when the repo is a monorepo or contains multiple deployable apps without an already-decided single flow target
- inspect the production Dockerfile and make sure it can build the app's assets in CI
- make sure the generated Dockerfile uses a Ruby base image compatible with the app's declared Ruby requirement
- preserve repo-defined frontend precompile hooks, such as Shakapacker
precompile_hookcommands or React on Railsconfig.auto_load_bundle = true - keep Node available in the final image if Rails or SSR depends on ExecJS, Yarn, or
pnpmafter the mainnpm installlayer - if
config/database.ymlshows SQLite in production, confirm thatcpflow generateemitted persistentdbandstoragevolumes plus arails db:preparerelease script; otherwise keep the default Postgres workload - inspect the production Dockerfile and package sources for private GitHub dependencies, and wire
DOCKER_BUILD_SSH_KEYplusDOCKER_BUILD_SSH_KNOWN_HOSTSwhen the build usesRUN --mount=type=sshagainst non-GitHub hosts - add extra
app_workloadsand template files for any runtime sidecars, workers, or renderer processes - make sure any sidecar process exposed to sibling workloads binds to
0.0.0.0instead of container-locallocalhost - make sure sidecar caches or bundle directories live in writable paths for the runtime user, such as
tmp/, instead of root-owned image paths - keep workflow files generic and put app names, org names, branch names, and Docker build knobs in repository
varsandsecrets
When the agent applies this to a project, it should avoid hardcoding app names or org names into the workflow files. Those belong in repository vars and secrets.