Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
123 changes: 75 additions & 48 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).
See the reusable `cpflow-*` GitHub Actions files in this repo's [`.github`](https://github.com/shakacode/react-webpack-rails-tutorial/tree/master/.github) directory for review apps, staging deploys, and production promotion.
Here is a brief [video overview](https://www.youtube.com/watch?v=llaQoAV_6Iw).

---
Expand Down Expand Up @@ -69,55 +69,99 @@ You should be able to see this information in the Control Plane UI.
and not `cpln` which is the Control Plane CLI.

```sh
# Use environment variable to prevent repetition
export APP_NAME=react-webpack-rails-tutorial
# Use the staging app defined in .controlplane/controlplane.yml
export APP_NAME=react-webpack-rails-tutorial-staging

# Provision all infrastructure on Control Plane.
# app react-webpack-rails-tutorial will be created per definition in .controlplane/controlplane.yml
cpflow setup-app -a $APP_NAME
cpflow setup-app -a "$APP_NAME"

# Build and push docker image to Control Plane repository
# Note, may take many minutes. Be patient.
# Check for error messages, such as forgetting to run `cpln image docker-login --org <your-org>`
cpflow build-image -a $APP_NAME
# Build and push the Docker image to the Control Plane registry.
cpflow build-image -a "$APP_NAME"

# Promote image to app after running `cpflow build-image command`
# Note, the UX of images may not show the image for up to 5 minutes.
# However, it's ready.
cpflow deploy-image -a $APP_NAME
# Run the configured release phase before cutting staging over to the new image.
cpflow deploy-image -a "$APP_NAME" --run-release-phase

# See how app is starting up
cpflow logs -a $APP_NAME
# See how the app is starting up
cpflow logs -a "$APP_NAME"

# Open app in browser (once it has started up)
cpflow open -a $APP_NAME
cpflow open -a "$APP_NAME"
```

### Promoting code updates

After committing code, you will update your deployment of `react-webpack-rails-tutorial` with the following commands:
After committing code, you will update your staging deployment with the following commands:

```sh
# Assuming you have already set APP_NAME env variable to react-webpack-rails-tutorial
# Build and push new image with sequential image tagging, e.g. 'react-webpack-rails-tutorial:1', then 'react-webpack-rails-tutorial:2', etc.
cpflow build-image -a $APP_NAME
# Assuming APP_NAME is still react-webpack-rails-tutorial-staging
cpflow build-image -a "$APP_NAME"

# Run database migrations (or other release tasks) with latest image,
# while app is still running on previous image.
# This is analogous to the release phase.
cpflow run -a $APP_NAME --image latest -- rails db:migrate
cpflow run -a "$APP_NAME" --image latest -- rails db:migrate

# Pomote latest image to app after migrations run
cpflow deploy-image -a $APP_NAME
cpflow deploy-image -a "$APP_NAME" --run-release-phase
```

If you needed to push a new image with a specific commit SHA, you can run the following command:

```sh
# Build and push with sequential image tagging and commit SHA, e.g. 'react-webpack-rails-tutorial:123_ABCD'
cpflow build-image -a $APP_NAME --commit ABCD
# Build and push with sequential image tagging and commit SHA
cpflow build-image -a "$APP_NAME" --commit ABCD
```

## GitHub Actions Flow

This repo now uses the shared `cpflow-*` GitHub Actions scaffolding:

- `.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.yml`

Behavior:

- comment `/deploy-review-app` on a PR to create or update a review app
- later pushes to that PR auto-redeploy the existing review app
- pushes to `master` auto-deploy staging unless `STAGING_APP_BRANCH` overrides it
- production promotion happens manually from the Actions tab
- stale review apps are cleaned up nightly

This repo keeps its historical `qa-react-webpack-rails-tutorial` prefix for review apps, so:

- `REVIEW_APP_PREFIX=qa-react-webpack-rails-tutorial`
- PR 123 deploys to `qa-react-webpack-rails-tutorial-123`

Required GitHub repository secrets:

- `CPLN_TOKEN_STAGING`
- `CPLN_TOKEN_PRODUCTION`

Required GitHub repository variables:

- `CPLN_ORG_STAGING`
- `CPLN_ORG_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`

Optional variables:

- `STAGING_APP_BRANCH=master`
- `PRIMARY_WORKLOAD=rails`
- `DOCKER_BUILD_EXTRA_ARGS`

Operational notes:

- `/deploy-review-app` and `/delete-review-app` only run for trusted commenters (`OWNER`, `MEMBER`, `COLLABORATOR`)
- fork PRs still receive help comments, but review app deploys are skipped because the workflow builds Docker images with repository secrets
- PR pushes do not auto-create review apps; the first deploy remains opt-in

## HTTP/2 and Thruster Configuration

This application uses [Thruster](https://github.com/basecamp/thruster), a zero-config HTTP/2 proxy from Basecamp, for optimized performance on Control Plane.
Expand Down Expand Up @@ -362,29 +406,12 @@ openssl rand -hex 64

## CI Automation, Review Apps and Staging

_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.

The review apps work by creating isolated deployments for each branch through this automated process. When a branch is pushed, 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
4. Deploys this image to Control Plane with its own isolated environment

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.

Review apps, staging deploys, and production promotion are all driven by the
`cpflow-*` workflows in `.github/workflows/`.

### 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`
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
1. Create a PR with changes to the GitHub Actions workflow.
2. Make edits to files such as `.github/workflows/cpflow-deploy-review-app.yml` or `.github/actions/cpflow-build-docker-image/action.yml`.
3. Commit and push the `.github` changes.
4. Check the GitHub Actions tab in the PR to see the status of the workflow.
39 changes: 0 additions & 39 deletions .github/actions/build-docker-image/action.yml

This file was deleted.

70 changes: 70 additions & 0 deletions .github/actions/cpflow-build-docker-image/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
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 arguments passed through to docker build
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

outputs:
image_tag:
description: Fully qualified image tag
value: ${{ steps.build.outputs.image_tag }}

runs:
using: composite
steps:
- name: Build Docker image
id: build
shell: bash
run: |
set -euo pipefail

PR_INFO=""
docker_build_args=()

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

if [[ -n "${{ inputs.docker_build_extra_args }}" ]]; then
while IFS= read -r arg; do
arg="${arg%$'\r'}"
[[ -n "${arg}" ]] || continue
docker_build_args+=("${arg}")
done <<< "${{ inputs.docker_build_extra_args }}"
fi

if [[ -n "${{ inputs.docker_build_ssh_key }}" ]]; then
mkdir -p ~/.ssh
chmod 700 ~/.ssh
ssh-keyscan -H github.com >> ~/.ssh/known_hosts
chmod 600 ~/.ssh/known_hosts

eval "$(ssh-agent -s)"
trap 'ssh-agent -k >/dev/null' EXIT
ssh-add - <<< "${{ inputs.docker_build_ssh_key }}"
docker_build_args+=("--ssh default")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: --ssh default is added as a single array element instead of two.

When "${docker_build_args[@]}" is expanded, cpflow will receive the argument --ssh default as one token rather than --ssh and default as two separate flags. Docker's --ssh flag expects id[=socket] as its value in the next argument.

Suggested change
docker_build_args+=("--ssh default")
docker_build_args+=("--ssh" "default")

Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
fi

echo "🏗️ Building Docker image${PR_INFO} (commit ${{ inputs.commit }})..."
cpflow build-image -a "${{ inputs.app_name }}" --commit="${{ inputs.commit }}" --org="${{ inputs.org }}" "${docker_build_args[@]}"

image_tag="${{ inputs.org }}/${{ inputs.app_name }}:${{ inputs.commit }}"
echo "image_tag=${image_tag}" >> "$GITHUB_OUTPUT"
echo "✅ Docker image build successful${PR_INFO} (commit ${{ inputs.commit }})"
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 }}
37 changes: 37 additions & 0 deletions .github/actions/cpflow-delete-control-plane-app/delete-app.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/bin/bash

set -euo pipefail

: "${APP_NAME:?APP_NAME environment variable is required}"
: "${CPLN_ORG:?CPLN_ORG environment variable is required}"
: "${REVIEW_APP_PREFIX:?REVIEW_APP_PREFIX environment variable is required}"

expected_prefix="${REVIEW_APP_PREFIX}-"
if [[ "$APP_NAME" != "${expected_prefix}"* ]]; then
echo "❌ ERROR: refusing to delete an app outside the review app prefix" >&2
echo "App name: $APP_NAME" >&2
echo "Expected prefix: ${expected_prefix}" >&2
exit 1
fi

echo "🔍 Checking if application exists: $APP_NAME"
exists_output=""
if ! exists_output="$(cpflow exists -a "$APP_NAME" --org "$CPLN_ORG" 2>&1)"; then
if [[ -z "$exists_output" ]]; then
echo "⚠️ Application does not exist: $APP_NAME"
exit 0
fi

echo "❌ ERROR: failed to determine whether application exists: $APP_NAME" >&2
printf '%s\n' "$exists_output" >&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.

The "does not exist vs. real error" disambiguation here relies on cpflow exists emitting nothing to stdout/stderr when the app is absent but emitting text when a genuine error occurs. That's a reasonable assumption given how cpflow is written, but it's not a documented contract — a future cpflow version that prints "App not found" to stderr would silently route the "not found" path into the error branch instead.

A more defensive approach would be to check the exit code meaning explicitly, e.g. by looking at cpflow exists's documented exit codes, or by passing a flag like --quiet. If cpflow doesn't expose that, at minimum add a comment explaining the assumption so the next reader doesn't have to guess.

fi

if [[ -n "$exists_output" ]]; then
printf '%s\n' "$exists_output"
fi

echo "🗑️ Deleting application: $APP_NAME"
cpflow delete -a "$APP_NAME" --org "$CPLN_ORG" --yes

echo "✅ Successfully deleted application: $APP_NAME"
Loading
Loading