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.
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. 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 repository secrets and variables the workflows expect.
- Push the branch, then comment
+review-app-deployon a PR to spin up a review environment.
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.ymlbin/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.
Required GitHub Repository Settings
For a normal generated review-app setup, configure one repository secret:
CPLN_TOKEN_STAGING: token for the staging Control Plane org
No GitHub repository 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: override the staging/review 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 production health checks; defaults torails
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
- optionally disable administrator bypass and 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.
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 and deploy-image for
image updates; 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 endpoint.
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
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.
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.
Generated Workflow Behavior
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-deploy. - Redeploys an existing review app automatically on later PR pushes.
- Creates a 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.
- 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 from trusted commenters (OWNER,MEMBER, orCOLLABORATOR). - Skips fork-based PR deploys because the workflow builds Docker images with repository secrets.
cpflow-delete-review-app.yml
- Deletes the review app on
+review-app-delete. - Also deletes it automatically when the pull request closes.
- Accepts
+review-app-deleteonly from trusted commenters (OWNER,MEMBER, orCOLLABORATOR).
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-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, comment formatting, Control Plane CLI setup, Docker image build, and
cleanup helpers live in upstream reusable workflows and composite actions in
this repository. Production promotion is expanded into the caller repository so
it can own environment: production, but it still checks out the same upstream
ref for shared composite actions.
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 locks, and they protect different things:
- The GitHub ref locks the reusable workflow and composite action code that GitHub runs.
- The RubyGems version locks the
cpflowCLI/runtime code only when you install or run that gem. It does not make GitHub load reusable workflow YAML from the gem.
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 generated wrappers that reference the matching 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 that point to the matching upstream release tag
such as
v5.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.
Updating Generated GitHub Actions After Gem Updates
Whenever a downstream repo updates the cpflow gem, update the checked-in
GitHub Actions wrappers in the same PR. The gem version does not make GitHub load
new reusable workflow YAML by itself; GitHub loads the uses: ref committed in
.github/workflows/cpflow-*.yml.
Use the installed gem to refresh the generated wrappers:
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 regenerates the generated wrapper and helper
files from the installed gem, pins the wrapper uses: refs to v<gem-version>,
and preserves a single custom staging branch from the existing generated staging
workflow. Pass --staging-branch BRANCH when changing or restoring a custom
staging branch explicitly.
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 and composite actions.
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, run:
bin/pin-cpflow-github-ref <upstream-pr-sha>The helper updates every generated reusable-workflow
uses:ref plus the production workflow's pinnedcontrol-plane-flowcheckout and setup validation ref. It accepts release tags and full commit SHAs by default, 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 same upstream SHA that supplies the reusable workflow and composite actions. IfCPFLOW_VERSIONis set while the wrapper is pinned to a SHA, the setup action fails before deployment because the gem and action code 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 -
Open a downstream PR and trigger the real review app with a comment whose body is exactly:
+review-app-deploy -
Verify the 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 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.
This tests the real reusable workflows, the production workflow's checked-out
shared composite actions, and the source-built cpflow gem from one immutable
upstream commit. It avoids merging upstream blind and avoids running production
automation against a moving 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.
AI Playbook
If you want an AI agent to apply this flow to another project, start with
cpflow github-flow-readiness, then use the standalone
AI rollout prompt. It captures the exact wording,
hard stop conditions, and definition of done for this workflow. You can also
run cpflow ai-github-flow-prompt from inside the target repo to print the
current 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.