Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
56c53a0
Apply generated cpflow GitHub Actions flow
justin808 Apr 30, 2026
eb1dbb2
Document generated cpflow workflow settings
justin808 Apr 30, 2026
b9095c6
Harden cpflow review app workflow
justin808 Apr 30, 2026
1e46bfe
Improve cpflow workflow review feedback
justin808 Apr 30, 2026
0a148a4
Fix cpflow token propagation
justin808 Apr 30, 2026
65bc286
Clarify cpflow workflow safety tradeoffs
justin808 Apr 30, 2026
9a390c1
Document cpflow workflow updates
justin808 Apr 30, 2026
976b320
Refresh cpflow workflow generator output
justin808 May 1, 2026
c218e38
Fix cpflow setup action metadata
justin808 May 1, 2026
fee6ce9
Handle fork review app comments cleanly
justin808 May 1, 2026
4428ec4
Address cpflow workflow review comments
justin808 May 1, 2026
f13a45f
Address generated workflow review follow-ups
justin808 May 1, 2026
897c42d
Ensure Docker SSH key cleanup
justin808 May 1, 2026
05085b1
Address cpflow review app follow-ups
justin808 May 1, 2026
8424f27
Harden cpflow review follow-ups
justin808 May 1, 2026
adf6902
Address final cpflow review notes
justin808 May 1, 2026
99e7155
Refresh cpflow flow from latest PR 278
justin808 May 1, 2026
1317de3
Fix cpflow setup action metadata
justin808 May 1, 2026
e8e13d0
Persist Control Plane token for cpflow steps
justin808 May 1, 2026
dbcb1c8
Address latest cpflow review polish
justin808 May 1, 2026
fdda08d
Address latest cpflow flow review updates
justin808 May 1, 2026
ce23e0a
Address cpflow review workflow feedback
justin808 May 1, 2026
3331332
Tighten cpflow workflow permissions and triggers
justin808 May 1, 2026
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
76 changes: 66 additions & 10 deletions .controlplane/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ _If you need a free demo account for Control Plane (no CC required), you can con

---

Check [how the `cpflow` gem (this project) is used in the Github actions](https://github.com/shakacode/react-webpack-rails-tutorial/blob/master/.github/actions/deploy-to-control-plane/action.yml).
Check [how the `cpflow` gem is used in the generated GitHub Actions flow](https://github.com/shakacode/react-webpack-rails-tutorial/blob/master/.github/actions/cpflow-build-docker-image/action.yml).
Here is a brief [video overview](https://www.youtube.com/watch?v=llaQoAV_6Iw).

---
Expand Down Expand Up @@ -364,27 +364,83 @@ openssl rand -hex 64

_Note, some of the URL references are internal for the ShakaCode team._

Review Apps (deployment of apps based on a PR) are done via Github Actions.
Review Apps (deployment of apps based on a PR) are done via the generated
`cpflow-*` GitHub Actions flow.

The review apps work by creating isolated deployments for each branch through this automated process. When a branch is pushed, the action:
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:

1. Sets up the necessary environment and tools
2. Creates a unique deployment for that branch if it doesn't exist
3. Builds a Docker image tagged with the branch's commit SHA
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.
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.
The production promotion workflow checks that production has all environment
variable names present in staging; it does not compare secret values, workload
environment variables, or Control Plane secret references.

The repository variables and secrets must match the app names in
`.controlplane/controlplane.yml`. In particular, `REVIEW_APP_PREFIX` should
include the `-pr` suffix for this app, such as
`qa-react-webpack-rails-tutorial-pr`, so generated review apps are named
`qa-react-webpack-rails-tutorial-pr-1234`.

This allows teams to:
- Preview changes in a production-like environment
- Test features independently
- Share working versions with stakeholders
- Validate changes before merging to main branches

The system uses Control Plane's infrastructure to manage these deployments, with each branch getting its own resources as defined in the controlplane.yml configuration.
The system uses Control Plane's infrastructure to manage these deployments, with
each review app getting its own resources as defined in the controlplane.yml
configuration.


### Workflow for Developing Github Actions for Review Apps
### Workflow for Developing GitHub Actions for Review Apps

1. Create a PR with changes to the Github Actions workflow
2. Make edits to file such as `.github/actions/deploy-to-control-plane/action.yml`
1. Create a PR with changes to the GitHub Actions workflow
2. Make edits to files such as `.github/actions/cpflow-build-docker-image/action.yml` or `.github/workflows/cpflow-deploy-review-app.yml`
3. Run a script like `ga .github && gc -m fixes && gp` to commit and push changes (ga = git add, gc = git commit, gp = git push)
4. Check the Github Actions tab in the PR to see the status of the workflow
4. Check the GitHub Actions tab in the PR to see the status of the workflow

### Keeping Generated cpflow Workflows Updated

Treat `.github/actions/cpflow-*` and `.github/workflows/cpflow-*` as generated
workflow files with project-specific settings layered on top. When `cpflow`
releases generator fixes or the upstream `control-plane-flow` repo changes the
GitHub Actions flow, update a project by regenerating the flow from the desired
`cpflow` version or branch, reviewing the diff, and keeping any local app names,
repository variables, secrets, and docs aligned with `.controlplane/controlplane.yml`.

For this app, validate a regenerated flow with:

```bash
bundle exec ruby /path/to/control-plane-flow/bin/cpflow generate-github-actions --staging-branch master
bundle exec ruby /path/to/control-plane-flow/bin/cpflow github-flow-readiness
actionlint .github/workflows/cpflow-*.yml
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:

```bash
gh workflow run cpflow-deploy-review-app.yml --ref <branch> -f pr_number=<pr-number>
```

After the workflow reports a review-app URL, verify the URL returns HTTP 200.
If a project needs to track generator changes automatically, use a scheduled
maintenance PR or Renovate-style workflow that bumps the `cpflow` version,
regenerates these files, and runs the same validation commands.
55 changes: 52 additions & 3 deletions .controlplane/shakacode-team.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ Deployments are handled by Control Plane configuration in this repo and GitHub A

### Review Apps
- Add a comment `/deploy-review-app` 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.

### Staging Environment
- **Automatic**: Any merge to the `master` branch automatically deploys to staging
Expand All @@ -14,14 +21,56 @@ Deployments are handled by Control Plane configuration in this repo and GitHub A
- [Staging App](https://staging.reactrails.com/)

### Production Environment
- **Manual**: Run the [promote-staging-to-production workflow](https://github.com/shakacode/react-webpack-rails-tutorial/actions/workflows/promote-staging-to-production.yml) on GitHub
- **Manual**: Run the [cpflow-promote-staging-to-production workflow](https://github.com/shakacode/react-webpack-rails-tutorial/actions/workflows/cpflow-promote-staging-to-production.yml) on GitHub
- Rollback restores workload images only; database migrations and other
`--run-release-phase` side effects are not reversed automatically.
- **URLs**:
- [Control Plane Console - Production](https://console.cpln.io/console/org/shakacode-open-source-examples-production/gvc/react-webpack-rails-tutorial-production/workload/rails/-info)
- [Production App](https://reactrails.com/)

See [./README.md](./README.md) for more details.
### GitHub Repository Settings

Required repository secrets:

- `CPLN_TOKEN_STAGING`
- `CPLN_TOKEN_PRODUCTION`

Required repository variables:

- `CPLN_ORG_STAGING=shakacode-open-source-examples-staging`
- `CPLN_ORG_PRODUCTION=shakacode-open-source-examples-production`
- `STAGING_APP_NAME=react-webpack-rails-tutorial-staging`
- `PRODUCTION_APP_NAME=react-webpack-rails-tutorial-production`
- `REVIEW_APP_PREFIX=qa-react-webpack-rails-tutorial-pr`
- `STAGING_APP_BRANCH=master`
- `PRIMARY_WORKLOAD=rails`

Optional repository settings:

- `DOCKER_BUILD_SSH_KEY`: secret for private SSH dependencies during Docker builds.
- `DOCKER_BUILD_EXTRA_ARGS`: newline-delimited Docker build tokens, such as `--build-arg=FOO=bar`.
- `DOCKER_BUILD_SSH_KNOWN_HOSTS`: custom `known_hosts` entries when SSH build hosts are not GitHub.com.
- `CPLN_CLI_VERSION`: pin a specific `@controlplane/cli` version; defaults to the generated action pin.
- `CPFLOW_VERSION`: pin a specific cpflow gem version; defaults to the generated action pin.
- `HEALTH_CHECK_ACCEPTED_STATUSES`: production promotion health statuses; defaults to `200 301 302`.
- `HEALTH_CHECK_RETRIES` / `HEALTH_CHECK_INTERVAL`: production health polling controls; defaults to `24` retries and `15` seconds.
- `ROLLBACK_READINESS_RETRIES` / `ROLLBACK_READINESS_INTERVAL`: post-rollback health polling controls; defaults to `24` retries and `15` seconds.

If staging moves off `master`, update both `STAGING_APP_BRANCH` and the branch
filter in `.github/workflows/cpflow-deploy-staging.yml`.

### Keeping cpflow Automation Current

When the upstream `control-plane-flow` repo changes the generated GitHub Actions
flow, regenerate the `cpflow-*` actions/workflows in this repo from the target
`cpflow` version or branch using `--staging-branch master`, review the diff, and
keep the repository variables above aligned with `.controlplane/controlplane.yml`. Validate with
`cpflow github-flow-readiness`, `actionlint .github/workflows/cpflow-*.yml`, and
the normal CI checks before merging.

See [readme.md](readme.md) for more details.

## Links

- [Control Plane Org for Staging and Review Apps](https://console.cpln.io/console/org/shakacode-open-source-examples-staging/-info)
- [Control Plane Org for Deployed App](https://console.cpln.io/console/org/shakacode-open-source-examples/-info)
- [Control Plane Org for Production App](https://console.cpln.io/console/org/shakacode-open-source-examples-production/-info)
39 changes: 0 additions & 39 deletions .github/actions/build-docker-image/action.yml

This file was deleted.

124 changes: 124 additions & 0 deletions .github/actions/cpflow-build-docker-image/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
name: Build Docker Image
description: Builds and pushes the app image for a Control Plane workload

inputs:
app_name:
description: Name of the application
required: true
org:
description: Control Plane organization name
required: true
commit:
description: Commit SHA to tag the image with
required: true
pr_number:
description: Pull request number for status messaging
required: false
docker_build_extra_args:
description: Optional newline-delimited extra docker build tokens. Use key=value forms like --build-arg=FOO=bar.
required: false
docker_build_ssh_key:
description: Optional private SSH key used for Docker builds that fetch private dependencies with RUN --mount=type=ssh
required: false
docker_build_ssh_known_hosts:
description: Optional SSH known_hosts entries used with docker_build_ssh_key. Defaults to pinned GitHub.com host keys; override if GitHub rotates keys or your build uses another SSH host.
required: false

runs:
using: composite
steps:
# Keep SSH key handling in a dedicated step so DOCKER_BUILD_SSH_KEY is never present
# in the main build step's environment. ACTIONS_STEP_DEBUG=true dumps env before any
# command runs, so keeping the key out of env there avoids even admin-triggered exposure.
- name: Prepare SSH agent for Docker build
if: ${{ inputs.docker_build_ssh_key != '' }}
shell: bash
env:
# Pass the key via env so the file write is a single printf call rather than a
# heredoc with a fixed terminator (a heredoc would silently truncate the key if
# any line of the key value happened to match the terminator). Scope is still
# this step only — the build step below does not receive DOCKER_BUILD_SSH_KEY.
DOCKER_BUILD_SSH_KEY: ${{ inputs.docker_build_ssh_key }}
DOCKER_BUILD_SSH_KNOWN_HOSTS: ${{ inputs.docker_build_ssh_known_hosts }}
run: |
set -euo pipefail

umask 077
mkdir -p ~/.ssh
chmod 700 ~/.ssh

if [[ -n "${DOCKER_BUILD_SSH_KNOWN_HOSTS}" ]]; then
printf '%s\n' "${DOCKER_BUILD_SSH_KNOWN_HOSTS}" > ~/.ssh/known_hosts
else
# GitHub.com host keys verified against GitHub's published keys on 2026-05-01.
# Override docker_build_ssh_known_hosts if GitHub rotates keys again.
printf '%s\n' \
'github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl' \
'github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=' \
'github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=' \
> ~/.ssh/known_hosts
Comment on lines +55 to +59
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

These three GitHub host keys are correct today, but GitHub has rotated keys before (the RSA key was replaced in March 2023). If rotation happens again, Docker SSH builds will fail until this action is updated.

Two mitigations worth considering:

  1. Add a brief comment noting the last date these were verified (e.g. # Verified 2025-05) so reviewers know how stale the pins might be.
  2. Document in the action's docker_build_ssh_known_hosts input description that users can override these defaults — callers who need pinning stability can fetch current keys via ssh-keyscan github.com in their own CI and supply them as vars.DOCKER_BUILD_SSH_KNOWN_HOSTS.

fi
chmod 600 ~/.ssh/known_hosts

printf '%s\n' "${DOCKER_BUILD_SSH_KEY}" > ~/.ssh/cpflow_build_key
chmod 600 ~/.ssh/cpflow_build_key

- name: Build Docker image
shell: bash
env:
APP_NAME: ${{ inputs.app_name }}
COMMIT_SHA: ${{ inputs.commit }}
CONTROL_PLANE_ORG: ${{ inputs.org }}
DOCKER_BUILD_EXTRA_ARGS: ${{ inputs.docker_build_extra_args }}
PR_NUMBER: ${{ inputs.pr_number }}
run: |
set -euo pipefail

PR_INFO=""
docker_build_args=()
ssh_agent_started=false

cleanup_build_ssh() {
if [[ "${ssh_agent_started}" == "true" ]]; then
ssh-agent -k >/dev/null || true
fi
rm -f "${HOME}/.ssh/cpflow_build_key"
}
trap cleanup_build_ssh EXIT

if [[ -n "${PR_NUMBER}" ]]; then
PR_INFO=" for PR #${PR_NUMBER}"
fi

if [[ -n "${DOCKER_BUILD_EXTRA_ARGS}" ]]; then
while IFS= read -r arg; do
arg="${arg%$'\r'}"
[[ -n "${arg}" ]] || continue

if [[ "${arg}" =~ [[:space:]] ]]; then
echo "docker_build_extra_args entries must be single docker-build tokens. " \
"Use key=value forms like --build-arg=FOO=bar." >&2
exit 1
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This exit 1 fires before the cleanup trap at line 99 is established. If DOCKER_BUILD_SSH_KEY was provided (step 1 ran) and this exit fires, ~/.ssh/cpflow_build_key persists on the runner for the job's lifetime — a concern on self-hosted runners.

The cleanest fix is a dedicated cleanup step:

- name: Remove SSH key
  if: always()
  shell: bash
  run: rm -f "${HOME}/.ssh/cpflow_build_key"

Alternatively, set the trap at the very top of this step (before the EXTRA_ARGS loop) using rm -f — it is safe to call even when the file does not exist.

fi

docker_build_args+=("${arg}")
done <<< "${DOCKER_BUILD_EXTRA_ARGS}"
fi

if [[ -f "${HOME}/.ssh/cpflow_build_key" ]]; then
eval "$(ssh-agent -s)"
ssh_agent_started=true
ssh-add "${HOME}/.ssh/cpflow_build_key"
docker_build_args+=("--ssh=default")
fi

echo "🏗️ Building Docker image${PR_INFO} (commit ${COMMIT_SHA})..."
cpflow build-image -a "${APP_NAME}" --commit="${COMMIT_SHA}" --org="${CONTROL_PLANE_ORG}" "${docker_build_args[@]}"
echo "✅ Docker image build successful${PR_INFO} (commit ${COMMIT_SHA})"

- name: Remove SSH key
if: ${{ always() }}
shell: bash
run: |
# Defence in depth for cancellations or future refactors around the build trap.
rm -f "${HOME}/.ssh/cpflow_build_key"
24 changes: 24 additions & 0 deletions .github/actions/cpflow-delete-control-plane-app/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Delete Control Plane App
description: Deletes a Control Plane app and all associated resources

inputs:
app_name:
description: Name of the application to delete
required: true
cpln_org:
description: Control Plane organization name
required: true
review_app_prefix:
description: Prefix used for review app names
required: true

runs:
using: composite
steps:
- name: Delete application
shell: bash
run: ${{ github.action_path }}/delete-app.sh
env:
APP_NAME: ${{ inputs.app_name }}
CPLN_ORG: ${{ inputs.cpln_org }}
REVIEW_APP_PREFIX: ${{ inputs.review_app_prefix }}
Loading
Loading