Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ In addition to the standard keepachangelog.com categories, this project uses a l

## [Unreleased]

_Please add entries here for your pull requests that have not yet been released._
### Changed

- Renamed the generated review-app PR comment commands to a namespaced `+review-app-*` family: `/deploy-review-app` → `+review-app-deploy`, `/delete-review-app` → `+review-app-delete`, and `/help` → `+review-app-help`. The `+` prefix is used instead of `/` to avoid collision with GitHub's reserved slash-command surface and with other bots that listen on `/help`, and to make the three commands obviously part of one namespaced family. Repos that ran `cpflow generate-github-actions` against 5.0.0.rc.0 must regenerate the generated `.github/workflows/cpflow-*.yml` files and `.github/cpflow-help.md`, then update saved instructions or runbooks. [PR 285](https://github.com/shakacode/control-plane-flow/pull/285) by [Justin Gordon](https://github.com/justin808).

## [5.0.0.rc.0] - 2026-05-05

Expand Down
22 changes: 22 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,28 @@ CPLN_ORG=your-org-for-tests bundle exec rspec --tag slow
cpflow test
```

## Developing the GitHub flow generator

`cpflow generate-github-actions` copies templates from `lib/github_flow_templates/` into a target repo's `.github/` directory. To work on this feature:

- **Edit the templates in place.** The generator does no string-mangling beyond a small set of substitutions handled in `lib/command/generate_github_actions.rb`; what you put in `lib/github_flow_templates/.github/` is (almost) exactly what ships into a generated repo. Make changes there, not in a generated copy.
- **Surface area to keep consistent.** A change to a PR command (e.g. `+review-app-deploy`) usually touches three places: the trigger workflow (`lib/github_flow_templates/.github/workflows/cpflow-deploy-review-app.yml`), the PR-open quick reference (`cpflow-review-app-help.yml`), and the long-form help (`lib/github_flow_templates/.github/cpflow-help.md`). The AI flow prompt (`lib/command/ai_github_flow_prompt.rb`) also names commands and should be kept in sync.
- **Run the generator spec on every change:**

```sh
bundle exec rspec spec/command/generate_github_actions_spec.rb
```

It generates the templates into a tmp playground and asserts on their contents — most regressions in the templates will fail there.
- **Lint the templates.** Generated workflows are checked with `actionlint` in CI. Install it locally and run `actionlint lib/github_flow_templates/.github/workflows/*.yml` to catch issues before pushing.
- **Test PR-branch workflow edits in a real repo.** Comment-triggered runs (`+review-app-deploy`, `+review-app-delete`, `+review-app-help`) execute the workflow code from the repository's default branch, so they will not exercise your PR-branch changes. Generate the workflows into a downstream test repo, push to a feature branch, then dispatch each affected workflow with `gh`:

```sh
gh workflow run cpflow-deploy-review-app.yml --ref <your-pr-branch> -f pr_number=<pr-number>
gh workflow run cpflow-delete-review-app.yml --ref <your-pr-branch> -f pr_number=<pr-number>
gh workflow run cpflow-help-command.yml --ref <your-pr-branch> -f pr_number=<pr-number>
```

## Releasing

See [Releasing the Gem](./docs/releasing.md) for the changelog-first Ruby gem release process. In short: run
Expand Down
2 changes: 1 addition & 1 deletion docs/ai-github-flow-prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ prompt tells the agent to stop on.
```text
Set up Control Plane GitHub Flow for this repo. Start with `cpflow github-flow-readiness` and stop on any reported blockers. The repo must be deployable from a clean clone: published package versions, complete runtime scaffold, and a production Dockerfile that can build the app. If any package version is unpublished, inaccessible from CI, or requires credentials that are not already modeled in the repo or GitHub settings, stop and report the blocker instead of generating workflow files. If the repo is a legacy sample pinned to an obsolete Ruby or Bundler toolchain, if it does not even have a production Dockerfile yet, or if it is a monorepo without an already-decided single app boundary for this flow, stop and report that as a prerequisite instead of forcing the rollout.

If `.controlplane/` is missing, run `cpflow generate`. Treat the generated app names as the repo-name default and rename them only if the project needs a different prefix. Then run `cpflow generate-github-actions` (or `cpflow generate-github-actions --staging-branch BRANCH` when staging should deploy from a branch other than `main`/`master`), keep review apps opt-in via `/deploy-review-app`, make sure any `STAGING_APP_BRANCH` repository variable is also present in the generated staging workflow's `on.push.branches` filter, and list the GitHub secrets and variables that must be configured.
If `.controlplane/` is missing, run `cpflow generate`. Treat the generated app names as the repo-name default and rename them only if the project needs a different prefix. Then run `cpflow generate-github-actions` (or `cpflow generate-github-actions --staging-branch BRANCH` when staging should deploy from a branch other than `main`/`master`), keep review apps opt-in via `+review-app-deploy`, make sure any `STAGING_APP_BRANCH` repository variable is also present in the generated staging workflow's `on.push.branches` filter, and list the GitHub secrets and variables that must be configured.

Keep Node available in the final image if asset compilation or SSR depends on ExecJS, Yarn, `pnpm`, or npm after the main install layer. Make sure the generated Dockerfile uses a Ruby base image compatible with the app's declared Ruby requirement. Preserve repo-defined frontend build hooks: if `config/shakapacker.yml` defines a `precompile_hook`, or React on Rails enables `config.auto_load_bundle = true`, confirm the generated Dockerfile runs that codegen step before `rails assets:precompile`. If `config/database.yml` shows SQLite in production, confirm that the generated scaffold uses persistent `db` and `storage` volumes plus a release script that runs `rails db:prepare`; otherwise keep the default Postgres workload. If the public workload is not named `rails`, set `PRIMARY_WORKLOAD` or adjust the generated workflows. Inspect the Dockerfile and package sources for private GitHub dependencies or `RUN --mount=type=ssh`; if present, wire `DOCKER_BUILD_SSH_KEY`, optionally set `DOCKER_BUILD_SSH_KNOWN_HOSTS` for non-GitHub SSH hosts, and keep `DOCKER_BUILD_EXTRA_ARGS` to newline-delimited single tokens such as `--build-arg=FOO=bar`.

Expand Down
14 changes: 7 additions & 7 deletions docs/ci-automation.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This document describes the reusable GitHub Actions scaffolding generated by `cp

The goal is to bring the Heroku Flow model into any `cpflow` project:

1. Comment `/deploy-review-app` on a pull request to create or update a review app.
1. Comment `+review-app-deploy` on a pull request to create or update a review app.
2. Push more commits to the PR to auto-redeploy that review app.
3. Push to the staging branch to auto-deploy staging.
4. Promote the already-built staging artifact to production from the Actions tab.
Expand All @@ -18,7 +18,7 @@ End-to-end rollout in one view:
2. `cpflow generate` — creates `.controlplane/` if missing.
3. `cpflow generate-github-actions` — adds the `cpflow-*` composite actions and workflows.
4. Configure the GitHub [repository secrets and variables](#required-github-repository-settings) the workflows expect.
5. Push the branch, then comment `/deploy-review-app` on a PR to spin up a review environment.
5. Push the branch, then comment `+review-app-deploy` on a PR to spin up a review environment.

See [Bootstrap a Project](#bootstrap-a-project) for command details, [Repo Readiness Checklist](#repo-readiness-checklist) for what "ready" means, and [AI Playbook](#ai-playbook) to run the rollout through an agent.

Expand Down Expand Up @@ -216,22 +216,22 @@ The action will start an SSH agent, add the key, write `known_hosts`, and pass `

`cpflow-help-command.yml`

- Replies to `/help` on a pull request with the commands and required repo settings.
- Replies to `+review-app-help` on a pull request with the commands and required repo settings.

`cpflow-deploy-review-app.yml`

- Creates a review app when someone comments `/deploy-review-app`.
- 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.
- Accepts `/deploy-review-app` only from trusted commenters (`OWNER`, `MEMBER`, or `COLLABORATOR`).
- Accepts `+review-app-deploy` only from trusted commenters (`OWNER`, `MEMBER`, or `COLLABORATOR`).
- Skips fork-based PR deploys because the workflow builds Docker images with repository secrets.

`cpflow-delete-review-app.yml`

- Deletes the review app on `/delete-review-app`.
- Deletes the review app on `+review-app-delete`.
- Also deletes it automatically when the pull request closes.
- Accepts `/delete-review-app` only from trusted commenters (`OWNER`, `MEMBER`, or `COLLABORATOR`).
- Accepts `+review-app-delete` only from trusted commenters (`OWNER`, `MEMBER`, or `COLLABORATOR`).

`cpflow-deploy-staging.yml`

Expand Down
2 changes: 1 addition & 1 deletion lib/command/ai_github_flow_prompt.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def prompt
<<~PROMPT
Set up Control Plane GitHub Flow for this repo. Start with `cpflow github-flow-readiness` and stop on any reported blockers. The repo must be deployable from a clean clone: published package versions, complete runtime scaffold, and a production Dockerfile that can build the app. If any package version is unpublished, inaccessible from CI, or requires credentials that are not already modeled in the repo or GitHub settings, stop and report the blocker instead of generating workflow files. If the repo is a legacy sample pinned to an obsolete Ruby or Bundler toolchain, if it does not even have a production Dockerfile yet, or if it is a monorepo without an already-decided single app boundary for this flow, stop and report that as a prerequisite instead of forcing the rollout.

If `.controlplane/` is missing, run `cpflow generate`. Treat the generated app names as the repo-name default (`#{inferred_app_prefix}`) and rename them only if the project needs a different prefix. Then run `cpflow generate-github-actions` (or `cpflow generate-github-actions --staging-branch BRANCH` when staging should deploy from a branch other than `main`/`master`), keep review apps opt-in via `/deploy-review-app`, make sure any `STAGING_APP_BRANCH` repository variable is also present in the generated staging workflow's `on.push.branches` filter, and list the GitHub secrets and variables that must be configured.
If `.controlplane/` is missing, run `cpflow generate`. Treat the generated app names as the repo-name default (`#{inferred_app_prefix}`) and rename them only if the project needs a different prefix. Then run `cpflow generate-github-actions` (or `cpflow generate-github-actions --staging-branch BRANCH` when staging should deploy from a branch other than `main`/`master`), keep review apps opt-in via `+review-app-deploy`, make sure any `STAGING_APP_BRANCH` repository variable is also present in the generated staging workflow's `on.push.branches` filter, and list the GitHub secrets and variables that must be configured.

Keep Node available in the final image if asset compilation or SSR depends on ExecJS, Yarn, `pnpm`, or npm after the main install layer. Make sure the generated Dockerfile uses a Ruby base image compatible with the app's declared Ruby requirement. Preserve repo-defined frontend build hooks: if `config/shakapacker.yml` defines a `precompile_hook`, or React on Rails enables `config.auto_load_bundle = true`, confirm the generated Dockerfile runs that codegen step before `rails assets:precompile`. If `config/database.yml` shows SQLite in production, confirm that the generated scaffold uses persistent `db` and `storage` volumes plus a release script that runs `rails db:prepare`; otherwise keep the default Postgres workload. If the public workload is not named `rails`, set `PRIMARY_WORKLOAD` or adjust the generated workflows. Inspect the Dockerfile and package sources for private GitHub dependencies or `RUN --mount=type=ssh`; if present, wire `DOCKER_BUILD_SSH_KEY`, optionally set `DOCKER_BUILD_SSH_KNOWN_HOSTS` for non-GitHub SSH hosts, and keep `DOCKER_BUILD_EXTRA_ARGS` to newline-delimited single tokens such as `--build-arg=FOO=bar`.

Expand Down
60 changes: 43 additions & 17 deletions lib/github_flow_templates/.github/cpflow-help.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,49 @@
# Control Plane GitHub Flow
# Review app help

You asked for review app help. These commands are generated by [cpflow](https://github.com/shakacode/control-plane-flow).

## PR commands

`/deploy-review-app`
`+review-app-deploy`
- Creates the review app if it does not exist
- Builds the PR commit image
- Deploys the image and comments with the review URL
- Comment body must be exactly `/deploy-review-app` — no surrounding text, trailing whitespace, or trailing newline. The trigger uses an exact-equality match, so a comment like `please /deploy-review-app now` or `/deploy-review-app ` (with a trailing space) silently no-ops.
- Comment body must be exactly `+review-app-deploy`, with no surrounding text or trailing spaces. A single trailing newline from GitHub's comment editor is accepted. Comments like `please +review-app-deploy now` or `+review-app-deploy ` (with a trailing space) silently no-op.

`/delete-review-app`
`+review-app-delete`
- Deletes the review app when the PR is done
- This also runs automatically when the PR closes
- Same exact-match rule as `/deploy-review-app`: the comment body must be exactly `/delete-review-app`.
- Comment body must be exactly `+review-app-delete`, with no surrounding text or trailing spaces. A single trailing newline from GitHub's comment editor is accepted. Same command-match rule as `+review-app-deploy`.

`+review-app-help`
- Posts this message on the PR.
Comment thread
justin808 marked this conversation as resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor punctuation inconsistency: this bullet ends with a period, but the equivalent first bullets under +review-app-deploy ("Creates the review app if it does not exist") and +review-app-delete ("Deletes the review app when the PR is done") do not.

Suggested change
- Posts this message on the PR.
- Posts this message on the PR

- Comment body must be exactly `+review-app-help`, with no surrounding text or trailing spaces. A single trailing newline from GitHub's comment editor is accepted. Same command-match rule as `+review-app-deploy`.

## Workflow behavior

- Review apps are opt-in and created with `+review-app-deploy`
- New commits redeploy existing review apps automatically
- Pushes to the staging branch deploy staging automatically
- Promotion to production is manual via the Actions tab
- A nightly workflow removes stale review apps

<details>
<summary>Advanced: GitHub Actions secrets and variables</summary>

## Repository secrets
### GitHub Actions secrets

| Name | Required | Notes |
| --- | --- | --- |
| `CPLN_TOKEN_STAGING` | yes | Service-account token scoped to the staging org. |
| `CPLN_TOKEN_PRODUCTION` | yes (for promote) | Service-account token scoped to the production org. |
| `CPLN_TOKEN_STAGING` | yes | Service-account token scoped to the staging Control Plane org on controlplane.com. |
| `CPLN_TOKEN_PRODUCTION` | yes (for promote) | Service-account token scoped to the production Control Plane org on controlplane.com. |
| `DOCKER_BUILD_SSH_KEY` | optional | Private SSH key used when Docker builds fetch private deps via `RUN --mount=type=ssh`. |

## Repository variables
### GitHub Actions variables

| Name | Required | Notes |
| --- | --- | --- |
| `CPLN_ORG_STAGING` | yes | Control Plane org for staging and review apps. |
| `CPLN_ORG_PRODUCTION` | yes (for promote) | Control Plane org for production. |
| `CPLN_ORG_STAGING` | yes | Control Plane org on controlplane.com for staging and review apps. |
| `CPLN_ORG_PRODUCTION` | yes (for promote) | Control Plane org on controlplane.com for production. |
| `STAGING_APP_NAME` | yes | App name in `controlplane.yml` used as the staging deploy target. |
| `PRODUCTION_APP_NAME` | yes (for promote) | App name in `controlplane.yml` used as the production deploy target. |
| `REVIEW_APP_PREFIX` | yes | Prefix for per-PR review app names (e.g. `review-app`). |
Expand All @@ -38,10 +55,19 @@
| `CPLN_CLI_VERSION` | optional | Pin a specific `@controlplane/cli` version; falls back to the action default when unset. |
| `CPFLOW_VERSION` | optional | Pin a specific cpflow gem version; falls back to the generated default when unset. |

## Workflow behavior
</details>

- Review apps are opt-in and created with `/deploy-review-app`
- New commits redeploy existing review apps automatically
- Pushes to the staging branch deploy staging automatically
- Promotion to production is manual via the Actions tab
- A nightly workflow removes stale review apps
<details>
<summary>Advanced: testing changes to generated workflows</summary>

When iterating on the generated workflow YAML on a PR branch, comment-triggered runs (`+review-app-deploy`, `+review-app-delete`, `+review-app-help`) execute the workflow code from the repository's default branch — not your PR branch. To exercise the PR-branch workflow code before merging, dispatch the workflow manually with `gh`:

```sh
gh workflow run cpflow-deploy-review-app.yml --ref <your-pr-branch> -f pr_number=<pr-number>
gh workflow run cpflow-delete-review-app.yml --ref <your-pr-branch> -f pr_number=<pr-number>
gh workflow run cpflow-help-command.yml --ref <your-pr-branch> -f pr_number=<pr-number>
```

`workflow_dispatch` runs use the workflow file from the `--ref` you pass, so this is the supported way to test PR-branch workflow edits before merge. After merge, comment triggers go back to running the default-branch workflow code as usual.

</details>
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
if: |
(github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
github.event.comment.body == '/delete-review-app' &&
contains(fromJson('["+review-app-delete","+review-app-delete\n","+review-app-delete\r\n"]'), github.event.comment.body) &&
contains(fromJson('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)) ||
(github.event_name == 'pull_request_target' && github.event.action == 'closed') ||
github.event_name == 'workflow_dispatch'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,15 @@ jobs:
deploy:
# Skip synchronize/opened events from fork PRs at the job level — they cannot access
# repository secrets anyway, so running any steps just burns billable minutes. Users
# can still manually deploy a fork PR via `/deploy-review-app` (gated below by
# can still manually deploy a fork PR via `+review-app-deploy` (gated below by
# author_association) or workflow_dispatch.
if: |
(github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository) ||
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
github.event.comment.body == '/deploy-review-app' &&
contains(fromJson('["+review-app-deploy","+review-app-deploy\n","+review-app-deploy\r\n"]'), github.event.comment.body) &&

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The contains(fromJson([...])) pattern is correct and handles GitHub's actual trailing-newline behavior well. One note for future maintainers: the three-element array covers the exact string, \n-terminated, and \r\n-terminated variants. If GitHub ever adds a leading/trailing space (e.g., from a mobile editor auto-correction), those would silently no-op. The help text already documents this boundary clearly, so this is just a heads-up rather than a request to change anything.

contains(fromJson('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association))
runs-on: ubuntu-latest
timeout-minutes: 45
Expand Down Expand Up @@ -236,7 +236,7 @@ jobs:
run: |
{
echo "Review app ${APP_NAME} does not exist yet."
echo "Create it with a PR comment that is exactly /deploy-review-app."
echo "Create it with +review-app-deploy as the PR comment body."
} >> "$GITHUB_STEP_SUMMARY"

- name: Setup review app if it does not exist yet
Expand Down
Loading
Loading