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
15 changes: 8 additions & 7 deletions .controlplane/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -369,17 +369,18 @@ Review Apps (deployment of apps based on a PR) are done via the generated

The review apps work by creating isolated deployments for pull requests through
this automated process. When an approved collaborator comments exactly
`/deploy-review-app` on a PR, the action:
`+review-app-deploy` on a PR, the action:

1. Sets up the necessary environment and tools
2. Creates a unique review app if it doesn't exist
3. Builds a Docker image tagged with the PR commit SHA
4. Deploys this image to Control Plane with its own isolated environment

After the review app exists, new pushes to the PR redeploy it automatically.
Use `/delete-review-app` to delete it manually; closing the PR deletes it
automatically. Pushes to the staging branch deploy staging, and production
promotion is manual from the `cpflow-promote-staging-to-production` workflow.
Use `+review-app-delete` to delete it manually; closing the PR deletes it
automatically. Use `+review-app-help` for the review-app command reference.
Pushes to the staging branch deploy staging, and production promotion is manual
from the `cpflow-promote-staging-to-production` workflow.
If staging moves off `master`, update both the `STAGING_APP_BRANCH` repository
variable and the `branches:` filter in `.github/workflows/cpflow-deploy-staging.yml`;
GitHub does not allow repository variables in trigger branch filters.
Expand Down Expand Up @@ -432,9 +433,9 @@ bundle exec rubocop
Then open a normal PR and let GitHub Actions prove the generated review-app,
staging, lint, JS, and RSpec workflows before merging. For review-app workflow
changes, test both the local workflow syntax and a real deployment. GitHub runs
`issue_comment` workflows from the default branch, so a `/deploy-review-app`
comment on the PR does not fully exercise slash-command changes that are only on
the PR branch. Before merge, run the PR branch workflow explicitly:
`issue_comment` workflows from the default branch, so a `+review-app-deploy`
comment on the PR does not fully exercise command changes that are only on the
PR branch. Before merge, run the PR branch workflow explicitly:

```bash
gh workflow run cpflow-deploy-review-app.yml --ref <branch> -f pr_number=<pr-number>
Expand Down
6 changes: 3 additions & 3 deletions .controlplane/shakacode-team.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
Deployments are handled by Control Plane configuration in this repo and GitHub Actions.

### Review Apps
- Add a comment `/deploy-review-app` to any PR to deploy a review app
- Add a comment `+review-app-deploy` to any PR to deploy a review app
- The generated app name is `${REVIEW_APP_PREFIX}-${PR_NUMBER}`. Keep
`REVIEW_APP_PREFIX` set to `qa-react-webpack-rails-tutorial-pr` so review
apps use names like `qa-react-webpack-rails-tutorial-pr-1234`, matching the
prefix-backed config in `.controlplane/controlplane.yml`.
- New pushes to a PR redeploy only after the review app already exists.
- Add `/delete-review-app` to delete a review app manually; closing the PR also
deletes it automatically.
- Add `+review-app-delete` to delete a review app manually; closing the PR also
deletes it automatically. Use `+review-app-help` for the command reference.

### Staging Environment
- **Automatic**: Any merge to the `master` branch automatically deploys to staging
Expand Down
6 changes: 3 additions & 3 deletions .github/actions/cpflow-setup-environment/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ inputs:
cpln_cli_version:
description: >-
@controlplane/cli version. Empty string falls back to the action's pinned default
so callers can pass the repository variable value unconditionally.
so callers can pass `${{ vars.CPLN_CLI_VERSION }}` unconditionally.
required: false
default: ""
cpflow_version:
description: >-
cpflow gem version. Empty string falls back to the action's pinned default
so callers can pass the repository variable value unconditionally.
so callers can pass `${{ vars.CPFLOW_VERSION }}` unconditionally.
required: false
default: ""

Expand Down Expand Up @@ -54,7 +54,7 @@ runs:
# Override per-repo by setting `CPLN_CLI_VERSION` / `CPFLOW_VERSION` repo variables;
# an empty input falls back to the action's pinned default below.
default_cpln_cli_version="3.3.1"
default_cpflow_version="5.0.0.rc.0"
default_cpflow_version="5.0.0.rc.1"

CPLN_CLI_VERSION="${CPLN_CLI_VERSION:-${default_cpln_cli_version}}"
CPFLOW_VERSION="${CPFLOW_VERSION:-${default_cpflow_version}}"
Expand Down
63 changes: 43 additions & 20 deletions .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 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 @@ -35,16 +52,22 @@
| `DOCKER_BUILD_EXTRA_ARGS` | optional | Newline-delimited extra docker build tokens (e.g. `--build-arg=FOO=bar`). |
| `DOCKER_BUILD_SSH_KNOWN_HOSTS` | optional | SSH known_hosts entries when SSH build hosts are not GitHub.com. |
| `HEALTH_CHECK_ACCEPTED_STATUSES` | optional | Space-separated HTTP statuses considered healthy on promote (default `200 301 302`). |
| `HEALTH_CHECK_RETRIES` / `HEALTH_CHECK_INTERVAL` | optional | Production health polling controls; defaults to `24` retries and `15` seconds. |
| `ROLLBACK_READINESS_RETRIES` / `ROLLBACK_READINESS_INTERVAL` | optional | Post-rollback health polling controls; defaults to `24` retries and `15` seconds. |
| `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
- Slash command workflows run from the default branch until merged. Test PR-branch edits with `gh workflow run cpflow-deploy-review-app.yml --ref <branch> -f pr_number=<pr-number>`.
- 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>
1 change: 0 additions & 1 deletion .github/workflows/cpflow-cleanup-stale-review-apps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ jobs:

- name: Remove stale review apps
env:
CPLN_TOKEN: ${{ secrets.CPLN_TOKEN_STAGING }}
REVIEW_APP_PREFIX: ${{ vars.REVIEW_APP_PREFIX }}
CPLN_ORG_STAGING: ${{ vars.CPLN_ORG_STAGING }}
shell: bash
Expand Down
35 changes: 1 addition & 34 deletions .github/workflows/cpflow-delete-review-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ on:

permissions:
contents: read
deployments: write
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Deploy creates GitHub deployments but delete never cleans them

Low Severity

The deploy workflow still creates GitHub deployment records (via createDeployment and createDeploymentStatus) and retains the deployments: write permission, but the delete workflow removed both the deployments: write permission and the "Mark GitHub deployment inactive" step. This asymmetry means deployment records are created on every review-app deploy but never marked inactive when the review app is deleted or the PR is closed, causing stale environment entries to accumulate on the repository's GitHub Environments page.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9d3b37e. Configure here.

issues: write
pull-requests: write

Expand All @@ -34,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 Expand Up @@ -109,43 +108,11 @@ jobs:
- name: Delete review app
if: steps.config.outputs.ready == 'true'
uses: ./.github/actions/cpflow-delete-control-plane-app
env:
CPLN_TOKEN: ${{ secrets.CPLN_TOKEN_STAGING }}
with:
app_name: ${{ env.APP_NAME }}
cpln_org: ${{ vars.CPLN_ORG_STAGING }}
review_app_prefix: ${{ vars.REVIEW_APP_PREFIX }}

- name: Mark GitHub deployment inactive
if: steps.config.outputs.ready == 'true'
uses: actions/github-script@v7
with:
script: |
const environment = `review/${process.env.APP_NAME}`;
const deployments = await github.paginate(github.rest.repos.listDeployments, {
owner: context.repo.owner,
repo: context.repo.repo,
environment,
per_page: 100
});

if (deployments.length === 0) {
core.info(`No GitHub deployments found for ${environment}.`);
return;
}

for (const deployment of deployments) {
await github.rest.repos.createDeploymentStatus({
owner: context.repo.owner,
repo: context.repo.repo,
deployment_id: deployment.id,
state: "inactive",
environment,
log_url: process.env.WORKFLOW_URL,
description: "Review app deleted"
});
}

- name: Finalize delete status
if: always() && steps.config.outputs.ready == 'true'
uses: actions/github-script@v7
Expand Down
35 changes: 3 additions & 32 deletions .github/workflows/cpflow-deploy-review-app.yml
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) &&
contains(fromJson('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association))
runs-on: ubuntu-latest
timeout-minutes: 45
Expand All @@ -61,26 +61,6 @@ jobs:
ref: ${{ github.event.repository.default_branch }}
persist-credentials: false

- name: Set up Ruby for cpflow bootstrap
if: ${{ hashFiles('.github/actions/cpflow-validate-config/action.yml') == '' }}
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.4"

- name: Bootstrap generated cpflow actions
if: ${{ hashFiles('.github/actions/cpflow-validate-config/action.yml') == '' }}
shell: bash
run: |
set -euo pipefail
gem install cpflow -v "5.0.0.rc.0" --no-document
ruby -S cpflow generate-github-actions --staging-branch master
# shellcheck disable=SC2016
ruby -0pi -e '$_.gsub!(/so callers can pass `\$\{\{ vars\.CPLN_CLI_VERSION \}\}` unconditionally\./, "so callers can pass the repository variable value unconditionally."); $_.gsub!(/so callers can pass `\$\{\{ vars\.CPFLOW_VERSION \}\}` unconditionally\./, "so callers can pass the repository variable value unconditionally.")' .github/actions/cpflow-setup-environment/action.yml
if grep -n '\$''{{ vars\.\(CPLN_CLI_VERSION\|CPFLOW_VERSION\) }}' .github/actions/cpflow-setup-environment/action.yml; then
echo "::error::Bootstrapped cpflow setup action still contains GitHub metadata expressions in input descriptions."
exit 1
fi

- name: Validate required secrets and variables
id: config
uses: ./.github/actions/cpflow-validate-config
Expand Down Expand Up @@ -220,8 +200,6 @@ jobs:
if: steps.config.outputs.ready == 'true' && steps.source.outputs.allowed == 'true'
id: check-app
working-directory: app
env:
CPLN_TOKEN: ${{ secrets.CPLN_TOKEN_STAGING }}
shell: bash
run: |
set -euo pipefail
Expand Down Expand Up @@ -260,15 +238,13 @@ 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
id: setup-review-app
if: steps.config.outputs.ready == 'true' && steps.source.outputs.allowed == 'true' && steps.check-app.outputs.exists != 'true' && github.event_name != 'pull_request'
working-directory: app
env:
CPLN_TOKEN: ${{ secrets.CPLN_TOKEN_STAGING }}
shell: bash
run: |
set -euo pipefail
Expand Down Expand Up @@ -355,8 +331,6 @@ jobs:
- name: Build Docker image
if: steps.config.outputs.ready == 'true' && steps.source.outputs.allowed == 'true' && (steps.check-app.outputs.exists == 'true' || steps.setup-review-app.outcome == 'success')
uses: ./.github/actions/cpflow-build-docker-image
env:
CPLN_TOKEN: ${{ secrets.CPLN_TOKEN_STAGING }}
with:
app_name: ${{ env.APP_NAME }}
org: ${{ vars.CPLN_ORG_STAGING }}
Expand Down Expand Up @@ -397,7 +371,6 @@ jobs:
if: steps.config.outputs.ready == 'true' && steps.source.outputs.allowed == 'true' && (steps.check-app.outputs.exists == 'true' || steps.setup-review-app.outcome == 'success')
working-directory: app
env:
CPLN_TOKEN: ${{ secrets.CPLN_TOKEN_STAGING }}
RELEASE_PHASE_FLAG: ${{ steps.release-phase.outputs.flag }}
shell: bash
run: |
Expand All @@ -415,8 +388,6 @@ jobs:
if: steps.config.outputs.ready == 'true' && steps.source.outputs.allowed == 'true' && (steps.check-app.outputs.exists == 'true' || steps.setup-review-app.outcome == 'success')
id: workload
working-directory: app
env:
CPLN_TOKEN: ${{ secrets.CPLN_TOKEN_STAGING }}
shell: bash
run: |
set -euo pipefail
Expand Down
11 changes: 3 additions & 8 deletions .github/workflows/cpflow-deploy-staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:
# deploy branches unless `cpflow generate-github-actions --staging-branch BRANCH`
# was used. If STAGING_APP_BRANCH is later changed in repository variables, keep
# this list in sync so pushes to that branch actually trigger the workflow.
branches: ["master"]
branches: ["main", "master"]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Listing both "main" and "master" means a push to either branch triggers a staging deploy. If only one should ever be active, a short note here (or in the readme) would help future maintainers avoid accidentally running concurrent staging deploys during a branch rename. The cancel-in-progress: false concurrency guard serializes rather than drops them, so correctness is preserved — but the extra deploy run wastes CI minutes.

If this repo will always use master and main is there only for forward-compatibility, consider documenting that expectation inline.

workflow_dispatch:

permissions:
Expand All @@ -17,7 +17,7 @@ permissions:
env:
APP_NAME: ${{ vars.STAGING_APP_NAME }}
CPLN_ORG: ${{ vars.CPLN_ORG_STAGING }}
STAGING_APP_BRANCH: ${{ vars.STAGING_APP_BRANCH || 'master' }}
STAGING_APP_BRANCH: ${{ vars.STAGING_APP_BRANCH }}

concurrency:
group: cpflow-deploy-staging-${{ github.ref_name }}
Expand Down Expand Up @@ -56,8 +56,6 @@ jobs:
- name: Checkout repository
if: steps.check-branch.outputs.is_deployable == 'true'
uses: actions/checkout@v4
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The build (line 82) and deploy (line 111) checkouts both pass persist-credentials: false, but this validate-branch checkout does not. Low-risk since nothing authenticated happens after checkout in this job, but worth aligning for consistency.

Suggested change
uses: actions/checkout@v4
uses: actions/checkout@v4
with:
persist-credentials: false

with:
persist-credentials: false

- name: Validate required secrets and variables
if: steps.check-branch.outputs.is_deployable == 'true'
Expand Down Expand Up @@ -93,8 +91,6 @@ jobs:

- name: Build Docker image
uses: ./.github/actions/cpflow-build-docker-image
env:
CPLN_TOKEN: ${{ secrets.CPLN_TOKEN_STAGING }}
with:
app_name: ${{ env.APP_NAME }}
org: ${{ vars.CPLN_ORG_STAGING }}
Expand All @@ -105,7 +101,7 @@ jobs:

deploy:
needs: [validate-branch, build]
if: needs.validate-branch.outputs.is_deployable == 'true' && needs.build.result == 'success'
if: needs.validate-branch.outputs.is_deployable == 'true'
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Deploy runs even when build fails

GitHub Actions documents that when a job with needs: [A, B] carries an explicit if condition, that condition overrides the default "skip when any needed job fails" behaviour. With the old guard && needs.build.result == 'success' the condition was false on build failure, so deploy skipped. With the new condition it evaluates to true whenever validate-branch sets is_deployable=true, so a failed Docker build will be followed by a cpflow deploy-image call that either errors out on a missing image tag or, worse, promotes a previously-pushed stale image to staging.

runs-on: ubuntu-latest
timeout-minutes: 30
steps:
Expand All @@ -130,7 +126,6 @@ jobs:

- name: Deploy staging image
env:
CPLN_TOKEN: ${{ secrets.CPLN_TOKEN_STAGING }}
RELEASE_PHASE_FLAG: ${{ steps.release-phase.outputs.flag }}
shell: bash
run: |
Expand Down
Loading
Loading