Skip to content

Commit 9bd3cbd

Browse files
authored
[codex] Update cpflow review app guidance (#764)
* Modernize Control Plane GitHub flow * Sync Control Plane flow hardening * Tighten Docker build action arguments * Update cpflow review app guidance
1 parent 5d185bc commit 9bd3cbd

19 files changed

Lines changed: 328 additions & 88 deletions

File tree

.controlplane/Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ RUN SECRET_KEY_BASE=precompile_placeholder bin/rails react_on_rails:locale
7272
# and /app/client/app are the client assets that are bundled, so not needed once built
7373
# Helps to have smaller images b/c of smaller Docker Layer Caches and smaller final images
7474
# SECRET_KEY_BASE is required for asset precompilation but is not persisted in the image
75-
RUN SECRET_KEY_BASE=precompile_placeholder yarn res:build && \
75+
RUN SECRET_KEY_BASE=precompile_placeholder bundle exec rake react_on_rails:generate_packs && \
76+
SECRET_KEY_BASE=precompile_placeholder yarn res:build && \
7677
SECRET_KEY_BASE=precompile_placeholder bin/rails assets:precompile && \
7778
rm -rf /app/lib/bs /app/client/app
7879

.controlplane/templates/org.yml

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
# Org level secrets are used to store sensitive information that is
2-
# shared across multiple apps in the same organization. This is
3-
# useful for storing things like API keys, database credentials, and
4-
# other sensitive information that is shared across multiple apps
5-
# in the same organization.
1+
# App secret dictionaries store sensitive information for apps in the
2+
# organization. This template keeps the cpflow app-secret placeholders
3+
# {{APP_SECRETS}} and {{APP_SECRETS_POLICY}}.
4+
#
5+
# cpflow 5.1.1 shared_secret_grants are only for a separate shared
6+
# org-level dictionary referenced from app/workload templates with
7+
# {{SHARED_SECRET_<NAME>}}.
68

79
# The qa-* dictionary is bootstrapped via this template for review apps.
810
# Review apps run pull request code, so values in this dictionary must be
@@ -29,7 +31,7 @@ data:
2931

3032
---
3133

32-
# Policy is needed to allow identities to access secrets
34+
# App secret policy grants app identities reveal access to this dictionary.
3335
kind: policy
3436
name: {{APP_SECRETS_POLICY}}
3537
targetKind: secret
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
name: Build Docker Image
2+
description: Builds and pushes the app image for a Control Plane workload
3+
4+
inputs:
5+
app_name:
6+
description: Name of the application
7+
required: true
8+
org:
9+
description: Control Plane organization name
10+
required: true
11+
commit:
12+
description: Commit SHA to tag the image with
13+
required: true
14+
pr_number:
15+
description: Pull request number for status messaging
16+
required: false
17+
docker_build_extra_args:
18+
description: Optional newline-delimited extra docker build tokens. Use key=value forms like --build-arg=FOO=bar.
19+
required: false
20+
docker_build_ssh_key:
21+
description: Optional private SSH key used for Docker builds that fetch private dependencies with RUN --mount=type=ssh
22+
required: false
23+
docker_build_ssh_known_hosts:
24+
description: Optional SSH known_hosts entries used with docker_build_ssh_key. Defaults to pinned GitHub.com host keys.
25+
required: false
26+
27+
outputs:
28+
image_tag:
29+
description: Fully qualified image tag
30+
value: ${{ steps.build.outputs.image_tag }}
31+
32+
runs:
33+
using: composite
34+
steps:
35+
- name: Build Docker image
36+
id: build
37+
shell: bash
38+
env:
39+
APP_NAME: ${{ inputs.app_name }}
40+
COMMIT: ${{ inputs.commit }}
41+
DOCKER_BUILD_EXTRA_ARGS: ${{ inputs.docker_build_extra_args }}
42+
DOCKER_BUILD_SSH_KEY: ${{ inputs.docker_build_ssh_key }}
43+
DOCKER_BUILD_SSH_KNOWN_HOSTS: ${{ inputs.docker_build_ssh_known_hosts }}
44+
ORG: ${{ inputs.org }}
45+
PR_NUMBER: ${{ inputs.pr_number }}
46+
run: |
47+
set -euo pipefail
48+
49+
PR_INFO=""
50+
docker_build_args=()
51+
52+
if [[ -n "$PR_NUMBER" ]]; then
53+
PR_INFO=" for PR #${PR_NUMBER}"
54+
fi
55+
56+
if [[ -n "$DOCKER_BUILD_EXTRA_ARGS" ]]; then
57+
while IFS= read -r arg; do
58+
arg="${arg%$'\r'}"
59+
[[ -n "${arg}" ]] || continue
60+
61+
if [[ "${arg}" =~ [[:space:]] ]]; then
62+
echo "docker_build_extra_args entries must be single docker-build tokens. " \
63+
"Use key=value forms like --build-arg=FOO=bar." >&2
64+
exit 1
65+
fi
66+
67+
docker_build_args+=("${arg}")
68+
done <<< "$DOCKER_BUILD_EXTRA_ARGS"
69+
fi
70+
71+
if [[ -n "$DOCKER_BUILD_SSH_KEY" ]]; then
72+
mkdir -p ~/.ssh
73+
chmod 700 ~/.ssh
74+
75+
if [[ -n "$DOCKER_BUILD_SSH_KNOWN_HOSTS" ]]; then
76+
printf '%s\n' "$DOCKER_BUILD_SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
77+
else
78+
cat <<'EOF' > ~/.ssh/known_hosts
79+
github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl
80+
github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=
81+
github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=
82+
EOF
83+
fi
84+
85+
chmod 600 ~/.ssh/known_hosts
86+
87+
eval "$(ssh-agent -s)"
88+
trap 'ssh-agent -k >/dev/null' EXIT
89+
ssh-add - <<< "$DOCKER_BUILD_SSH_KEY"
90+
unset DOCKER_BUILD_SSH_KEY
91+
docker_build_args+=("--ssh=default")
92+
fi
93+
94+
echo "🏗️ Building Docker image${PR_INFO} (commit ${COMMIT})..."
95+
cpflow build-image -a "$APP_NAME" --commit="$COMMIT" --org="$ORG" "${docker_build_args[@]}"
96+
97+
image_tag="${ORG}/${APP_NAME}:${COMMIT}"
98+
echo "image_tag=${image_tag}" >> "$GITHUB_OUTPUT"
99+
echo "✅ Docker image build successful${PR_INFO} (commit ${COMMIT})"
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: Delete Control Plane App
2+
description: Deletes a Control Plane app and all associated resources
3+
4+
inputs:
5+
app_name:
6+
description: Name of the application to delete
7+
required: true
8+
cpln_org:
9+
description: Control Plane organization name
10+
required: true
11+
review_app_prefix:
12+
description: Prefix used for review app names
13+
required: true
14+
15+
runs:
16+
using: composite
17+
steps:
18+
- name: Delete application
19+
shell: bash
20+
run: ${{ github.action_path }}/delete-app.sh
21+
env:
22+
APP_NAME: ${{ inputs.app_name }}
23+
CPLN_ORG: ${{ inputs.cpln_org }}
24+
REVIEW_APP_PREFIX: ${{ inputs.review_app_prefix }}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#!/bin/bash
2+
3+
set -euo pipefail
4+
5+
: "${APP_NAME:?APP_NAME environment variable is required}"
6+
: "${CPLN_ORG:?CPLN_ORG environment variable is required}"
7+
: "${REVIEW_APP_PREFIX:?REVIEW_APP_PREFIX environment variable is required}"
8+
9+
expected_prefix="${REVIEW_APP_PREFIX}-"
10+
if [[ "$APP_NAME" != "${expected_prefix}"* ]]; then
11+
echo "❌ ERROR: refusing to delete an app outside the review app prefix" >&2
12+
echo "App name: $APP_NAME" >&2
13+
echo "Expected prefix: ${expected_prefix}" >&2
14+
exit 1
15+
fi
16+
17+
echo "🔍 Checking if application exists: $APP_NAME"
18+
exists_output=""
19+
set +e
20+
exists_output="$(cpflow exists -a "$APP_NAME" --org "$CPLN_ORG" 2>&1)"
21+
exists_status=$?
22+
set -e
23+
24+
case "$exists_status" in
25+
0)
26+
;;
27+
3)
28+
if [[ -n "$exists_output" ]]; then
29+
printf '%s\n' "$exists_output"
30+
fi
31+
32+
echo "⚠️ Application does not exist: $APP_NAME"
33+
exit 0
34+
;;
35+
*)
36+
echo "❌ ERROR: failed to determine whether application exists: $APP_NAME" >&2
37+
printf '%s\n' "$exists_output" >&2
38+
exit 1
39+
;;
40+
esac
41+
42+
if [[ -n "$exists_output" ]]; then
43+
printf '%s\n' "$exists_output"
44+
fi
45+
46+
echo "🗑️ Deleting application: $APP_NAME"
47+
cpflow delete -a "$APP_NAME" --org "$CPLN_ORG" --yes
48+
49+
echo "✅ Successfully deleted application: $APP_NAME"
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
name: Setup Control Plane Environment
2+
description: Sets up Ruby, installs the Control Plane CLI and cpflow gem, and configures a default profile
3+
4+
inputs:
5+
token:
6+
description: Control Plane token
7+
required: true
8+
org:
9+
description: Control Plane organization
10+
required: true
11+
ruby_version:
12+
description: Ruby version used for cpflow
13+
required: false
14+
default: "3.4.6"
15+
cpln_cli_version:
16+
description: "@controlplane/cli version"
17+
required: false
18+
default: "3.3.1"
19+
cpflow_version:
20+
description: cpflow gem version
21+
required: false
22+
default: "5.1.1"
23+
24+
runs:
25+
using: composite
26+
steps:
27+
- name: Set up Ruby
28+
uses: ruby/setup-ruby@v1
29+
with:
30+
ruby-version: ${{ inputs.ruby_version }}
31+
32+
- name: Install Control Plane CLI and cpflow gem
33+
shell: bash
34+
run: |
35+
set -euo pipefail
36+
37+
sudo npm install -g @controlplane/cli@${{ inputs.cpln_cli_version }}
38+
cpln --version
39+
40+
gem install cpflow -v ${{ inputs.cpflow_version }}
41+
cpflow --version
42+
43+
- name: Setup Control Plane profile and registry login
44+
shell: bash
45+
run: |
46+
set -euo pipefail
47+
48+
TOKEN="${{ inputs.token }}"
49+
ORG="${{ inputs.org }}"
50+
51+
if [[ -z "$TOKEN" ]]; then
52+
echo "Error: Control Plane token not provided" >&2
53+
exit 1
54+
fi
55+
56+
if [[ -z "$ORG" ]]; then
57+
echo "Error: Control Plane organization not provided" >&2
58+
exit 1
59+
fi
60+
61+
create_output=""
62+
if ! create_output="$(cpln profile create default --token "$TOKEN" --org "$ORG" 2>&1)"; then
63+
if ! echo "$create_output" | grep -qi "already exists"; then
64+
echo "$create_output" >&2
65+
exit 1
66+
fi
67+
fi
68+
69+
cpln profile update default --org "$ORG" --token "$TOKEN"
70+
cpln image docker-login --org "$ORG"

Gemfile.lock

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ GEM
134134
coffee-script-source (1.12.2)
135135
concurrent-ruby (1.3.6)
136136
connection_pool (3.0.2)
137-
console (1.35.1)
137+
console (1.36.0)
138138
fiber-annotation
139139
fiber-local (~> 1.1)
140140
json
@@ -201,7 +201,7 @@ GEM
201201
jbuilder (2.12.0)
202202
actionview (>= 5.0.0)
203203
activesupport (>= 5.0.0)
204-
json (2.19.5)
204+
json (2.19.8)
205205
jwt (3.2.0)
206206
base64
207207
language_server-protocol (3.17.0.5)
@@ -505,7 +505,7 @@ GEM
505505
bindex (>= 0.4.0)
506506
railties (>= 6.0.0)
507507
websocket (1.2.10)
508-
websocket-driver (0.8.0)
508+
websocket-driver (0.8.1)
509509
base64
510510
websocket-extensions (>= 0.1.0)
511511
websocket-extensions (0.1.5)

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
ShakaCode recently migrated [HiChee.com](https://hichee.com) to Control Plane, resulting in a two-thirds reduction in server hosting costs!
1313

14-
See doc in [./.controlplane/readme.md](./.controlplane/readme.md) for how to easily deploy this app to Control Plane.
14+
See [./.controlplane/readme.md](./.controlplane/readme.md) for local `cpflow` setup plus the shared `cpflow-*` GitHub Actions flow for review apps, automatic staging deploys, and manual promotion to production.
1515

1616
The instructions leverage the `cpflow` CLI, with source code and many more tips on how to migrate from Heroku to Control Plane
1717
in https://github.com/shakacode/heroku-to-control-plane.

client/app/bundles/comments/components/CommentBox/CommentList/CommentList.spec.jsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,7 @@ describe('CommentList', () => {
2727
);
2828

2929
it('renders a list of Comments in normal order', () => {
30-
render(
31-
<CommentList $$comments={comments} cssTransitionGroupClassNames={cssTransitionGroupClassNames} />,
32-
);
30+
render(<CommentList $$comments={comments} cssTransitionGroupClassNames={cssTransitionGroupClassNames} />);
3331

3432
// Verify both authors are rendered in order
3533
expect(screen.getByText('Frank')).toBeInTheDocument();

client/app/bundles/comments/components/SimpleCommentScreen/ror_components/SimpleCommentScreen.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/* eslint-disable max-classes-per-file */
2+
23
'use client';
34

45
import React from 'react';

0 commit comments

Comments
 (0)