Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5b81dc2
Add Control Plane flow to migration example
justin808 Apr 15, 2026
9238039
Sync Control Plane flow hardening
justin808 Apr 15, 2026
ea290be
Preserve frontend precompile hooks in Dockerfile
justin808 Apr 15, 2026
1e769a0
Tighten Docker build action arguments
justin808 Apr 15, 2026
e8bc403
docs: clarify review app security
justin808 Jun 2, 2026
6783fcf
Merge remote-tracking branch 'origin/main' into add-cpflow-github-flow
justin808 Sep 17, 2026
2ceb6b9
Update cpflow deployment flow to v5.3.0
justin808 Sep 17, 2026
55270b1
Harden generated deployment defaults
justin808 Sep 17, 2026
56c8922
Fix production promotion and rendering
justin808 Sep 17, 2026
3cf019b
Document review app secret bootstrap
justin808 Sep 17, 2026
87c86aa
Keep Navbar SSR deterministic
justin808 Sep 17, 2026
6c3f884
Keep SQLite data separate from migrations
justin808 Sep 17, 2026
f4bd700
Address deployment flow review feedback
justin808 Sep 19, 2026
037b0f0
Merge remote-tracking branch 'origin/main' into add-cpflow-github-flow
justin808 Oct 15, 2026
de57965
Cover migrated routes with SSR regression tests
justin808 Oct 15, 2026
0368ee1
Keep preview deployment focused and load hydration bundles
justin808 Oct 15, 2026
80503ee
Trim trailing blank lines in deployment configuration
justin808 Oct 15, 2026
8a9a5dc
Meet preview volume limits and record the deployed revision
justin808 Oct 15, 2026
570364e
Use a stateful SQLite workload and tighten deployment guidance
justin808 Oct 15, 2026
2f96b84
Prepare SQLite in the stateful entrypoint instead of a cron runner
justin808 Oct 15, 2026
8b5bc03
Keep CI deployment outside secret-policy administration
justin808 Oct 17, 2026
0b887a3
Align CI help with maintainer-owned secret bootstrap
justin808 Oct 17, 2026
d6e8ea2
Pin Ruby for the deployment runner
justin808 Oct 17, 2026
a73baa3
Merge remote-tracking branch 'origin/main' into add-cpflow-github-flow
justin808 Oct 18, 2026
b77c2f9
Use temporary verification and upgrade React on Rails to 17.2.0
justin808 Oct 18, 2026
0f8ff18
Handle malformed Unicode in the server encoder shim
justin808 Oct 18, 2026
2e927f9
Apply server environment overrides before database preparation
justin808 Oct 18, 2026
8467bc5
Limit temporary database preparation to direct Rails commands
justin808 Oct 18, 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
2 changes: 1 addition & 1 deletion .agents/bin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ means that capability is n/a here.
| `validate` | Pre-push gate | JavaScript and Tailwind CSS builds, production Shakapacker compilation, then `bin/rails test` |
| `test` | Run tests | `bin/rails test` |
| `lint` | Lint / format | n/a |
| `build` | Build / type-check | `bin/yarn build`, `bin/yarn build:css`, then `RAILS_ENV=production bin/shakapacker` |
| `build` | Build / type-check | JavaScript and Tailwind builds, React on Rails pack generation, then production Shakapacker compilation |
| `docs` | Validate documentation | n/a |
| `ci-detect` | Detect CI impact | n/a |

Expand Down
3 changes: 2 additions & 1 deletion .agents/bin/build
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
set -euo pipefail
cd "$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
bin/yarn build
bin/yarn build:css
bin/rails tailwindcss:build
bin/shakapacker-precompile-hook
exec env RAILS_ENV=production bin/shakapacker "$@"
96 changes: 96 additions & 0 deletions .controlplane/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
ARG RUBY_VERSION=3.4.6

# Node and Ruby base images share the same Debian release so that glibc, libssl, and
# other system libraries line up between stages.
FROM docker.io/library/node:22-bookworm-slim AS node
FROM ruby:$RUBY_VERSION-slim-bookworm

# Yarn Classic / pnpm fallbacks for projects that haven't adopted corepack via
# `packageManager` in package.json. Override at build time (`--build-arg=YARN_CLASSIC_VERSION=...`)
# rather than editing this file so regenerating the Dockerfile keeps your pin.
ARG YARN_CLASSIC_VERSION=1.22.22
ARG PNPM_FALLBACK_VERSION=9.12.3

WORKDIR /app

# Keep Node.js available both for asset compilation and for SSR runtimes that
# rely on ExecJS in production. Narrowed to just what the node stage actually
# ships under /usr/local so we don't drag in unused Debian libs from that image.
COPY --from=node /usr/local/bin/node /usr/local/bin/node
COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules
COPY --from=node /usr/local/include/node /usr/local/include/node

RUN ln -sf ../lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm && \
ln -sf ../lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx && \
ln -sf ../lib/node_modules/corepack/dist/corepack.js /usr/local/bin/corepack && \
chmod +x /usr/local/lib/node_modules/npm/bin/npm-cli.js \
/usr/local/lib/node_modules/npm/bin/npx-cli.js \
/usr/local/lib/node_modules/corepack/dist/corepack.js && \
node --version && npm --version && corepack --version

# Expose Corepack-managed shims so later build steps can call yarn/pnpm
# directly during asset precompilation hooks.
RUN printf '%s\n' '#!/bin/sh' 'exec corepack yarn "$@"' > /usr/bin/yarn && \
chmod +x /usr/bin/yarn && \
printf '%s\n' '#!/bin/sh' 'exec corepack pnpm "$@"' > /usr/bin/pnpm && \
chmod +x /usr/bin/pnpm

# install ruby gems
COPY Gemfile* ./

RUN apt-get update -qq && \
apt-get install --no-install-recommends -y build-essential && \
bundle config set without 'development test' && \
bundle config set with 'production' && \
bundle install --jobs=3 --retry=3 && \
apt-get purge -y --auto-remove build-essential && \
rm -rf /var/lib/apt/lists /var/cache/apt/archives

COPY . ./

# Install JavaScript dependencies only when the project actually has them.
# Pin `packageManager` in package.json to take the corepack path and avoid the
# YARN_CLASSIC_VERSION / PNPM_FALLBACK_VERSION fallbacks below; the fallbacks exist for
# projects that haven't adopted corepack and should be reviewed periodically as they go stale.
RUN if [ -f package.json ]; then \
package_manager="$(node -p "require('./package.json').packageManager || ''")"; \
if [ -f yarn.lock ]; then \
if printf '%s' "$package_manager" | grep -q '^yarn@'; then \
corepack prepare "$package_manager" --activate && \
(corepack yarn install --immutable || corepack yarn install --frozen-lockfile); \
else \
npm install -g "yarn@${YARN_CLASSIC_VERSION}" && \
(yarn install --immutable || yarn install --frozen-lockfile); \
fi; \
elif [ -f pnpm-lock.yaml ]; then \
if printf '%s' "$package_manager" | grep -q '^pnpm@'; then \
corepack prepare "$package_manager" --activate && \
corepack pnpm install --frozen-lockfile; \
else \
corepack prepare "pnpm@${PNPM_FALLBACK_VERSION}" --activate && \
corepack pnpm install --frozen-lockfile; \
fi; \
elif [ -f package-lock.json ]; then \
npm ci; \
else \
npm install; \
fi; \
fi

ENV RAILS_ENV=production

# Compiling assets requires any non-blank value, but the placeholder must not
# remain in the final image environment.
RUN SECRET_KEY_BASE=NOT_USED_NON_BLANK bin/shakapacker-precompile-hook


RUN SECRET_KEY_BASE=NOT_USED_NON_BLANK rails assets:precompile

# add entrypoint
COPY .controlplane/entrypoint.sh ./
RUN chmod +x /app/entrypoint.sh
ARG GIT_COMMIT=unknown
ENV GIT_COMMIT=$GIT_COMMIT
ENTRYPOINT ["/app/entrypoint.sh"]

CMD ["rails", "s"]
41 changes: 41 additions & 0 deletions .controlplane/controlplane.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Keys beginning with "cpln_" correspond to your settings in Control Plane.
#
# Generated baseline for a Rails app that uses SQLite in production. This setup
# keeps SQLite data under `/app/data` and Active Storage under `/app/storage`
# on persistent volumes without hiding migrations under `/app/db`.

allow_org_override_by_env: true
allow_app_override_by_env: true

aliases:
common: &common
cpln_org: shakacode-open-source-examples-staging
default_location: aws-us-east-2
setup_app_templates:
- app
- db
- storage
- rails

one_off_workload: rails
app_workloads:
- rails
additional_workloads: []

stale_app_image_deployed_days: 5
image_retention_days: 7

apps:
react-on-rails-migration-example-staging:
<<: *common

react-on-rails-migration-example-review:
<<: *common
match_if_app_name_starts_with: true
# Optional: if a review app needs an existing shared org-level secret,
# declare its policy here and reference it in templates with
# {{SHARED_SECRET_<NAME>}}.
# shared_secret_grants:
# - name: database
# secret_name: react-on-rails-migration-example-review-database-secrets
# policy_name: react-on-rails-migration-example-review-database-secrets-policy
48 changes: 48 additions & 0 deletions .controlplane/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/bin/sh
set -e
# Runs before the main command

is_rails_server_command() {
if [ "${1:-}" = "env" ]; then
shift
while [ "$#" -gt 0 ]; do
case "${1}" in
*=*) shift ;;
--) shift; break ;;
-*) return 1 ;;
*) break ;;
esac
done
fi

if [ "${1:-}" = "bundle" ] && [ "${2:-}" = "exec" ]; then
shift 2
fi

# Matches generated, optionally env-prefixed, flag-free Thruster invocations.
# Hand-edited commands with env flags or Thruster flags before rails skip
# generated DB prep.
if [ "${1:-}" = "thrust" ] || [ "${1:-}" = "bin/thrust" ] || [ "${1:-}" = "./bin/thrust" ]; then
shift
fi

# Thruster may be wrapped with its own `bundle exec`, and the Rails command
# it proxies may also be wrapped with `bundle exec`.
if [ "${1:-}" = "bundle" ] && [ "${2:-}" = "exec" ]; then
shift 2
fi

{ [ "${1:-}" = "rails" ] || [ "${1:-}" = "bin/rails" ] || [ "${1:-}" = "./bin/rails" ]; } &&
{ [ "${2:-}" = "server" ] || [ "${2:-}" = "s" ]; }
}

# Match generated Rails server commands; workers and renderers skip DB prep.
# Generated Dockerfiles use WORKDIR /app; adjust this path if your hand-edited
# image runs the entrypoint from a different working directory.
if is_rails_server_command "$@"; then
echo " -- Preparing database"
./bin/rails db:prepare
fi

echo " -- Finishing entrypoint.sh, executing command"
exec "$@"
Comment thread
justin808 marked this conversation as resolved.
Outdated
55 changes: 55 additions & 0 deletions .controlplane/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Control Plane Deployment Notes

This repository uses `cpflow` for opt-in pull-request review apps, automatic
staging deploys from `main`. Production promotion is not configured.
The generated GitHub Actions use `cpflow` v5.3.0 and pin the immutable release
commit `b1e5ff4a04adfccfd8b59996e8abdbb5defb3fd6`; see
[`.github/cpflow-help.md`](../.github/cpflow-help.md) for the complete commands,
settings, and upgrade procedure. After regenerating wrappers for a future
release, repin them with `bin/pin-cpflow-github-ref <release-commit-sha>`.

## Runtime Shape

The app uses SQLite and local Active Storage in production. The Control Plane
templates therefore mount persistent volumes at `/app/data` and `/app/storage`,
set `DATABASE_PATH=/app/data/production.sqlite3`, and leave the image's
`/app/db` migrations visible. The Rails entrypoint runs `bin/rails db:prepare`
before starting the server; no cron release runner mounts these volumes. The workload
uses one `stateful` replica because SQLite and Active Storage use ext4 volumes.
Each volume starts at the provider's 10 GiB minimum. CPU and memory are fixed;
Capacity AI is not enabled for this stateful workload.

## One-Time Bootstrap

Create the shared review-app secret before enabling review deployments. Review
apps execute pull-request code, so use a disposable value that grants no access
to staging, production, or third-party services:

```sh
cpln secret create-dictionary \
--name react-on-rails-migration-example-review-secrets \
--org "$CPLN_ORG_STAGING" \
--entry "SECRET_KEY_BASE=$(bin/rails secret)"
```

Bootstrap persistent staging before its first deploy:

```sh
cpflow setup-app \
-a react-on-rails-migration-example-staging \
--org "$CPLN_ORG_STAGING" \
--skip-post-creation-hook
```

Add a distinct `SECRET_KEY_BASE` to the generated staging app
secret dictionary. For later template changes, run `cpflow apply-template` and
ensure the app identity can `reveal` the app secret policy.

## GitHub Configuration

Store `CPLN_TOKEN_STAGING` as a repository secret. Set the repository variables
`CPLN_ORG_STAGING` to `shakacode-open-source-examples-staging` and
`STAGING_APP_NAME` to `react-on-rails-migration-example-staging`; both review
apps and automatic staging deploys use that staging organization. The review
app prefix is inferred from `.controlplane/controlplane.yml` unless
`REVIEW_APP_PREFIX` overrides it.
19 changes: 19 additions & 0 deletions .controlplane/templates/app.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
kind: gvc
name: "{{APP_NAME}}"
spec:
env:
- name: RAILS_ENV
value: production
- name: RAILS_LOG_TO_STDOUT
value: "true"
- name: RAILS_SERVE_STATIC_FILES
value: "true"
- name: DATABASE_PATH
value: /app/data/production.sqlite3
# Review apps execute pull request code, so only mount review-safe values
# through this generated secret dictionary.
- name: SECRET_KEY_BASE
value: "cpln://secret/{{APP_SECRETS}}.SECRET_KEY_BASE"
Comment thread
justin808 marked this conversation as resolved.
Outdated
staticPlacement:
locationLinks:
- "{{APP_LOCATION_LINK}}"
6 changes: 6 additions & 0 deletions .controlplane/templates/db.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: volumeset
name: app-db
spec:
fileSystemType: ext4
initialCapacity: 10
performanceClass: general-purpose-ssd
40 changes: 40 additions & 0 deletions .controlplane/templates/rails.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
kind: workload
name: rails
spec:
type: stateful
containers:
- name: rails
cpu: 300m
inheritEnv: true
image: "{{APP_IMAGE_LINK}}"
memory: 512Mi
ports:
- number: 3000
# Keep this as `http` even when fronting Rails with Thruster — the Control Plane
# load balancer still serves HTTP/2 to end users. Setting `http2` here causes
# `502 Bad Gateway` with "protocol error".
# See https://www.shakacode.com/control-plane-flow/docs/thruster/ for details.
protocol: http
volumes:
# Keep migrations and schema from the image visible under /app/db;
# only the SQLite data file belongs on persistent storage.
- path: /app/data
recoveryPolicy: retain
uri: cpln://volumeset/app-db
- path: /app/storage
recoveryPolicy: retain
uri: cpln://volumeset/app-storage
defaultOptions:
autoscaling:
minScale: 1
maxScale: 1
metric: disabled
timeoutSeconds: 60
firewallConfig:
external:
inboundAllowCIDR:
- 0.0.0.0/0
outboundAllowCIDR:
- 0.0.0.0/0
# cpflow apply-template replaces {{APP_IDENTITY_LINK}} with the app workload identity link.
identityLink: "{{APP_IDENTITY_LINK}}"
6 changes: 6 additions & 0 deletions .controlplane/templates/storage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: volumeset
name: app-storage
spec:
fileSystemType: ext4
initialCapacity: 10
performanceClass: general-purpose-ssd
19 changes: 19 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.git
.github
.agents
.bundle
.env
.env.*
config/master.key
config/credentials/*.key
db/*.sqlite3
log/*
storage/*
tmp/*
node_modules
vendor/bundle
app/assets/builds/*
public/assets
public/packs
public/packs-test
coverage
Loading
Loading