diff --git a/CHANGELOG.md b/CHANGELOG.md index bf3fd5e2..56457f8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e1272361..bb3f3860 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 -f pr_number= + gh workflow run cpflow-delete-review-app.yml --ref -f pr_number= + gh workflow run cpflow-help-command.yml --ref -f pr_number= + ``` + ## Releasing See [Releasing the Gem](./docs/releasing.md) for the changelog-first Ruby gem release process. In short: run diff --git a/docs/ai-github-flow-prompt.md b/docs/ai-github-flow-prompt.md index ba9a09f8..9f1dd3da 100644 --- a/docs/ai-github-flow-prompt.md +++ b/docs/ai-github-flow-prompt.md @@ -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`. diff --git a/docs/ci-automation.md b/docs/ci-automation.md index 5b3736a1..51e693ba 100644 --- a/docs/ci-automation.md +++ b/docs/ci-automation.md @@ -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. @@ -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. @@ -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` diff --git a/lib/command/ai_github_flow_prompt.rb b/lib/command/ai_github_flow_prompt.rb index c7c5473e..8ceda7b6 100644 --- a/lib/command/ai_github_flow_prompt.rb +++ b/lib/command/ai_github_flow_prompt.rb @@ -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`. diff --git a/lib/github_flow_templates/.github/cpflow-help.md b/lib/github_flow_templates/.github/cpflow-help.md index 3cc41dc1..dd82ba08 100644 --- a/lib/github_flow_templates/.github/cpflow-help.md +++ b/lib/github_flow_templates/.github/cpflow-help.md @@ -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 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 + +
+Advanced: GitHub Actions secrets and variables -## 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`). | @@ -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 +
-- 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 +
+Advanced: testing changes to generated workflows + +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 -f pr_number= +gh workflow run cpflow-delete-review-app.yml --ref -f pr_number= +gh workflow run cpflow-help-command.yml --ref -f 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. + +
diff --git a/lib/github_flow_templates/.github/workflows/cpflow-delete-review-app.yml b/lib/github_flow_templates/.github/workflows/cpflow-delete-review-app.yml index d4543e28..a4006f8b 100644 --- a/lib/github_flow_templates/.github/workflows/cpflow-delete-review-app.yml +++ b/lib/github_flow_templates/.github/workflows/cpflow-delete-review-app.yml @@ -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' diff --git a/lib/github_flow_templates/.github/workflows/cpflow-deploy-review-app.yml b/lib/github_flow_templates/.github/workflows/cpflow-deploy-review-app.yml index d8bf4aa8..27f90484 100644 --- a/lib/github_flow_templates/.github/workflows/cpflow-deploy-review-app.yml +++ b/lib/github_flow_templates/.github/workflows/cpflow-deploy-review-app.yml @@ -37,7 +37,7 @@ 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' && @@ -45,7 +45,7 @@ jobs: 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) && contains(fromJson('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)) runs-on: ubuntu-latest timeout-minutes: 45 @@ -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 diff --git a/lib/github_flow_templates/.github/workflows/cpflow-help-command.yml b/lib/github_flow_templates/.github/workflows/cpflow-help-command.yml index 65fc3de6..0818dfb2 100644 --- a/lib/github_flow_templates/.github/workflows/cpflow-help-command.yml +++ b/lib/github_flow_templates/.github/workflows/cpflow-help-command.yml @@ -17,10 +17,15 @@ permissions: jobs: help: + # Comment-triggered runs are gated on author_association so only repo + # owners/members/collaborators can invoke them. workflow_dispatch is + # intentionally not gated here: GitHub already restricts manual dispatches + # to users with `actions: write` (write access to the repo), which is a + # stricter bar than COLLABORATOR. if: | (github.event_name == 'issue_comment' && github.event.issue.pull_request && - github.event.comment.body == '/help' && + contains(fromJson('["+review-app-help","+review-app-help\n","+review-app-help\r\n"]'), github.event.comment.body) && contains(fromJson('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)) || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest diff --git a/lib/github_flow_templates/.github/workflows/cpflow-review-app-help.yml b/lib/github_flow_templates/.github/workflows/cpflow-review-app-help.yml index 1b35b65e..74626217 100644 --- a/lib/github_flow_templates/.github/workflows/cpflow-review-app-help.yml +++ b/lib/github_flow_templates/.github/workflows/cpflow-review-app-help.yml @@ -26,16 +26,15 @@ jobs: with: script: | const body = [ - "# Control Plane review app commands", + "# Review app commands", "", - "`/deploy-review-app`", - "Create the review app or redeploy the PR branch to it.", + "Repo owners, members, and collaborators can use these commands:", "", - "`/delete-review-app`", - "Delete the review app and its temporary resources.", + "- `+review-app-deploy` - create or redeploy this PR's review app.", + "- `+review-app-delete` - delete this PR's review app and temporary resources.", + "- `+review-app-help` - show setup details and workflow behavior.", "", - "`/help`", - "Show the required GitHub variables, secrets, and workflow behavior." + "For setup details, repo owners, members, and collaborators can comment `+review-app-help`." ].join("\n"); await github.rest.issues.createComment({ diff --git a/spec/command/generate_github_actions_spec.rb b/spec/command/generate_github_actions_spec.rb index 45adb85b..d37b2b96 100644 --- a/spec/command/generate_github_actions_spec.rb +++ b/spec/command/generate_github_actions_spec.rb @@ -30,6 +30,14 @@ def help_workflow_path playground.join(".github/workflows/cpflow-help-command.yml") end + def pr_open_help_workflow_path + playground.join(".github/workflows/cpflow-review-app-help.yml") + end + + def command_body_match_expression(command) + %(contains(fromJson('["#{command}","#{command}\\n","#{command}\\r\\n"]'), github.event.comment.body)) + end + def promote_workflow_path playground.join(".github/workflows/cpflow-promote-staging-to-production.yml") end @@ -224,6 +232,33 @@ def generated_yaml_paths expect(contents).to include('fs.readFileSync(".github/cpflow-help.md"') end + it "pins the +review-app-* workflow trigger strings" do + expect(review_app_workflow_path.read).to include(command_body_match_expression("+review-app-deploy")) + expect(delete_review_workflow_path.read).to include(command_body_match_expression("+review-app-delete")) + expect(help_workflow_path.read).to include(command_body_match_expression("+review-app-help")) + end + + it "pins the +review-app-* commands in the PR-open message" do + pr_open_help = pr_open_help_workflow_path.read + + expect(pr_open_help).to include('"Repo owners, members, and collaborators can use these commands:"') + expect(pr_open_help).to include("- `+review-app-deploy` - create or redeploy this PR's review app.") + expect(pr_open_help).to include("- `+review-app-delete` - delete this PR's review app and temporary resources.") + expect(pr_open_help).to include("- `+review-app-help` - show setup details and workflow behavior.") + expect(pr_open_help).to include( + '"For setup details, repo owners, members, and collaborators can comment `+review-app-help`."' + ) + end + + it "pins the +review-app-* commands in the long-form help markdown" do + help_md = playground.join(".github/cpflow-help.md").read + + expect(help_md).to include("`+review-app-deploy`") + expect(help_md).to include("`+review-app-delete`") + expect(help_md).to include("`+review-app-help`") + expect(help_md).to include("A single trailing newline from GitHub's comment editor is accepted.") + end + it "documents Docker build vars in the help markdown" do help_md_path = playground.join(".github/cpflow-help.md") contents = help_md_path.read