Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
3 changes: 2 additions & 1 deletion .controlplane/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ RUN SECRET_KEY_BASE=precompile_placeholder bin/rails react_on_rails:locale
# and /app/client/app are the client assets that are bundled, so not needed once built
# Helps to have smaller images b/c of smaller Docker Layer Caches and smaller final images

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Minor inconsistency: this step uses bundle exec rake while the very next line uses bin/rails. bin/rails is the project-standard wrapper (it also invokes Bundler) and is consistent with the react_on_rails:locale call above.

Suggested change
# Helps to have smaller images b/c of smaller Docker Layer Caches and smaller final images
RUN SECRET_KEY_BASE=precompile_placeholder bin/rails react_on_rails:generate_packs && \

# SECRET_KEY_BASE is required for asset precompilation but is not persisted in the image
RUN SECRET_KEY_BASE=precompile_placeholder yarn res:build && \
RUN SECRET_KEY_BASE=precompile_placeholder bundle exec rake react_on_rails:generate_packs && \
SECRET_KEY_BASE=precompile_placeholder yarn res:build && \

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 order here (generate_packsres:build) is reversed compared to both config/initializers/react_on_rails.rb and package.json, where res:build always runs first. If generate_packs creates entry points that reference ReScript-compiled modules (.res.js), running it before res:build will operate on stale or absent output. The ordering should be consistent across all build surfaces.

Suggested change
SECRET_KEY_BASE=precompile_placeholder yarn res:build && \
RUN SECRET_KEY_BASE=precompile_placeholder yarn res:build && \
SECRET_KEY_BASE=precompile_placeholder bundle exec rake react_on_rails:generate_packs && \
SECRET_KEY_BASE=precompile_placeholder bin/rails assets:precompile && \
rm -rf /app/lib/bs /app/client/app

SECRET_KEY_BASE=precompile_placeholder bin/rails assets:precompile && \
rm -rf /app/lib/bs /app/client/app
Comment on lines +75 to 78

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 generate_packs runs before res:build, opposite order to every other build path

Every other invocation in this PR (config/initializers/react_on_rails.rb build_test_command/build_production_command, and both package.json build:test/build:dev scripts) runs yarn res:build before react_on_rails:generate_packs. Here the Dockerfile does the reverse. If the pack generator reads or validates any .res.js output — or if future changes add that dependency — the Docker build will silently use stale or absent ReScript artifacts while passing locally. Consider matching the order established by the other build commands, or add a comment explaining why the Docker layer-caching trade-off justifies the inversion.


Expand Down
2 changes: 1 addition & 1 deletion .controlplane/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ echo " -- Waiting for services"
wait_for_service $(echo $DATABASE_URL | sed -e 's|^.*@||' -e 's|/.*$||')
wait_for_service $(echo $REDIS_URL | sed -e 's|redis://||' -e 's|/.*$||')

echo " -- Finishing entrypoint.sh, executing '$@'"
echo " -- Finishing entrypoint.sh, executing '$*'@'"
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated

# Run the main command
exec "$@"
136 changes: 88 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,112 @@ 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`

The legacy workflows in this branch keep their help text inline instead of using
the newer generated `.github/cpflow-help.md` file. The local setup action
installs `cpflow` 5.1.1 by default.

Behavior:

- comment `+review-app-deploy` 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:

- `+review-app-deploy`, `+review-app-delete`, and `+review-app-help` 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

Secret grant notes for `cpflow` 5.1.1:

- this repo keeps the app secret dictionary and policy placeholders,
`{{APP_SECRETS}}` and `{{APP_SECRETS_POLICY}}`
- `shared_secret_grants` is only for a separate shared org-level dictionary
referenced from templates with `{{SHARED_SECRET_<NAME>}}`
- do not add `shared_secret_grants` here unless the app/workload templates start
referencing such a shared dictionary

## 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 +419,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.
2 changes: 1 addition & 1 deletion .controlplane/shakacode-team.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
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

### Staging Environment
- **Automatic**: Any merge to the `master` branch automatically deploys to staging
Expand Down
14 changes: 8 additions & 6 deletions .controlplane/templates/org.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# Org level secrets are used to store sensitive information that is
# shared across multiple apps in the same organization. This is
# useful for storing things like API keys, database credentials, and
# other sensitive information that is shared across multiple apps
# in the same organization.
# App secret dictionaries store sensitive information for apps in the
# organization. This template keeps the cpflow app-secret placeholders
# {{APP_SECRETS}} and {{APP_SECRETS_POLICY}}.
#
# cpflow 5.1.1 shared_secret_grants are only for a separate shared
# org-level dictionary referenced from app/workload templates with
# {{SHARED_SECRET_<NAME>}}.

# This is how you apply this once (not during CI)
# cpl apply-template secrets -a qa-react-webpack-rails-tutorial --org shakacode-open-source-examples-staging
Expand All @@ -15,7 +17,7 @@ data:

---

# Policy is needed to allow identities to access secrets
# App secret policy grants app identities reveal access to this dictionary.
kind: policy
name: {{APP_SECRETS_POLICY}}
targetKind: secret
Expand Down
39 changes: 0 additions & 39 deletions .github/actions/build-docker-image/action.yml

This file was deleted.

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

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
fi

docker_build_args+=("${arg}")
done <<< "${{ inputs.docker_build_extra_args }}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Shell injection risk: template expressions in inline script

${{ inputs.docker_build_extra_args }} is rendered into the script before the shell runs it. GitHub Actions does not shell-escape template values, so a crafted input like "; malicious_command; echo " could escape the here-string context. The same issue applies to ${{ inputs.docker_build_ssh_key }} on line 83.

The GitHub hardening guide recommends routing such values through env: variables instead:

      env:
        DOCKER_BUILD_EXTRA_ARGS: ${{ inputs.docker_build_extra_args }}
      run: |
        ...
        if [[ -n "$DOCKER_BUILD_EXTRA_ARGS" ]]; then
          while IFS= read -r arg; do
            ...
          done <<< "$DOCKER_BUILD_EXTRA_ARGS"
        fi

Since this action is only called from controlled workflows today the practical risk is low, but this pattern prevents future misuse if the action is ever invoked with PR-derived inputs.

fi

if [[ -n "${{ inputs.docker_build_ssh_key }}" ]]; then
mkdir -p ~/.ssh
chmod 700 ~/.ssh

if [[ -n "${{ inputs.docker_build_ssh_known_hosts }}" ]]; then
cat <<'EOF' > ~/.ssh/known_hosts
${{ inputs.docker_build_ssh_known_hosts }}
EOF
else
cat <<'EOF' > ~/.ssh/known_hosts
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=
EOF
fi

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")
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 }}
Loading
Loading