Skip to content

Validate cpflow gem and workflow pins#318

Merged
justin808 merged 13 commits into
mainfrom
jg-codex/validate-cpflow-version-pin
May 25, 2026
Merged

Validate cpflow gem and workflow pins#318
justin808 merged 13 commits into
mainfrom
jg-codex/validate-cpflow-version-pin

Conversation

@justin808

@justin808 justin808 commented May 24, 2026

Copy link
Copy Markdown
Member

Summary

  • fail early when CPFLOW_VERSION does not match the control_plane_flow_ref release tag
  • require SHA/branch testing to leave CPFLOW_VERSION unset so the gem is built from the same checked-out source as the reusable workflow/actions
  • make generated entrypoints run DB preparation only for Rails server commands, through ./bin/rails, and fail fast on errors
  • document the release-tag locking model and add spec coverage for setup-action propagation

Why

GitHub loads reusable workflow and composite action code from the repo ref in uses: before Ruby runs. The gem alone cannot select that YAML/action code. This guard prevents a downstream app from accidentally combining workflow/action code from one control-plane-flow ref with a cpflow gem from another version.

Upgrade note

The generated entrypoint now uses set -e and prepares the database only when the command is a Rails server command (rails s, rails server, ./bin/rails ..., bundle exec rails ..., or the generated Thruster wrapper). Apps with hand-edited .controlplane/entrypoint.sh files should audit custom commands before regenerating.

Validation

  • env CPLN_ORG=dummy bundle exec rspec spec/command/generate_github_actions_spec.rb spec/command/generate_spec.rb
  • env RUBOCOP_CACHE_ROOT=/private/tmp/rubocop-cache bundle exec rubocop
  • git diff --check
  • actionlint -ignore "SC2129" .github/workflows/cpflow-*.yml
  • sh -n lib/generator_templates/entrypoint.sh

Summary by CodeRabbit

  • New Features

    • Added optional control_plane_flow_ref pin propagated into environment setup; workflows now infer review‑app config and forward resolved app/org; promote workflow adds production_environment input.
  • Bug Fixes

    • Entrypoint now fails fast and runs DB prep only for Rails server commands; workflows error early when version pin and ref disagree.
  • Documentation

    • Clarified CI pinning, review‑app variable guidance, and failure behavior.
  • Tests

    • Added coverage for ref propagation, config inference, and version validation.

Review Change Stack


Note

High Risk
Changes production promotion secret handling, CI deploy/delete/cleanup behavior, and generated container entrypoints (DB prep and fail-fast), any of which can block deploys or alter runtime startup if misconfigured.

Overview
This release tightens GitHub Actions so workflow/action code from control_plane_flow_ref cannot drift from a pinned CPFLOW_VERSION: setup now requires a matching release tag, validates the remote tag commit against the checked-out actions, and rejects gem pins when testing SHAs or branches (leave CPFLOW_VERSION unset to build from source).

Review apps get a shared cpflow-resolve-review-config composite action that infers prefix, staging org, and app_name from .controlplane/controlplane.yml (with optional variable overrides). Deploy/delete/cleanup workflows drop mandatory REVIEW_APP_PREFIX / CPLN_ORG_STAGING validation, pass only named secrets instead of secrets: inherit, and wire resolved values through build/delete steps.

Production promotion adds a production_environment input, runs the job on that GitHub Environment, validates CPLN_TOKEN_PRODUCTION there, and generated callers pass only CPLN_TOKEN_STAGING—with bin/test-cpflow-github-flow guarding against inheriting secrets or forwarding the production token.

Generated runtime changes: entrypoints use set -e, run ./bin/rails db:prepare only for Rails server commands (not every workload), and Dockerfiles chmod +x the copied entrypoint. PR-open help no longer gates on REVIEW_APP_PREFIX; docs and templates describe minimal review-app setup and environment-based production tokens.

Reviewed by Cursor Bugbot for commit a944641. Bugbot is set up for automated code reviews on this repo. Configure here.

@coderabbitai

coderabbitai Bot commented May 24, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a control_plane_flow_ref input to cpflow-setup-environment with normalization/validation that enforces CPFLOW_VERSION matching release-tag refs, propagates that input through generated workflows while resolving review-app config from .controlplane/controlplane.yml, makes generated entrypoint DB preparation conditional with fail-fast, and updates tests/docs.

Changes

CPFLOW version pinning validation

Layer / File(s) Summary
Setup action input and version pinning validation
.github/actions/cpflow-setup-environment/action.yml
Adds control_plane_flow_ref input, exports it as CONTROL_PLANE_FLOW_REF, and implements normalize_release_ref/validate_cpflow_version_pin shell helpers to require that when CPFLOW_VERSION is set the ref is a matching release tag; mismatches emit ::error:: and fail.
Resolve review-app config & propagate ref
.github/workflows/cpflow-cleanup-stale-review-apps.yml, .github/workflows/cpflow-delete-review-app.yml, .github/workflows/cpflow-deploy-review-app.yml, .github/workflows/cpflow-deploy-staging.yml, .github/workflows/cpflow-promote-staging-to-production.yml, .github/workflows/cpflow-review-app-help.yml
Adds "Resolve review app config" steps that parse .controlplane/controlplane.yml to infer review-app prefix/org/app-name, reduces required secret/var lists to only CPLN_TOKEN_STAGING, exports resolved outputs used downstream, and forwards control_plane_flow_ref from workflow inputs into every cpflow-setup-environment invocation.
Generated entrypoint script: conditional database prep and error handling
lib/generator_templates/entrypoint.sh
Adds set -e and replaces unconditional rails db:prepare with ./bin/rails db:prepare only when the detected command is a Rails server invocation.
Setup action test coverage for validation
spec/command/generate_github_actions_spec.rb
Extends assertions to require the control_plane_flow_ref input, CONTROL_PLANE_FLOW_REF env wiring, presence of the validation logic and error messages, and that all reusable workflows pass the input.
Generated entrypoint test coverage
spec/command/generate_spec.rb
Verifies generated entrypoint.sh contains set -e, conditional DB prep, server-invocation detection logic, and argument forwarding via exec "$@".
Documentation and changelog updates
CHANGELOG.md, docs/ci-automation.md, lib/github_flow_templates/.github/cpflow-help.md, lib/github_flow_templates/bin/pin-cpflow-github-ref, lib/github_flow_templates/.github/workflows/cpflow-promote-staging-to-production.yml
Documents the new CPFLOW_VERSION vs control_plane_flow_ref guard, review-app workflow config inference, production-environment protections for promotion jobs, regeneration guidance, and clarifies pinning script usage.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

  • shakacode/control-plane-flow#307: Related changes around review-app deletion and ensuring .controlplane/controlplane.yml is available during delete workflows.
  • shakacode/control-plane-flow#305: Upstream refactor touching cpflow-setup-environment install/version handling that is code-level related to the new validation and ref propagation.

Poem

"I'm a rabbit in the CI shrubbery, I hop and tidy refs with glee,
I match your tag to gem and song, so builds won't go awry or long,
I nibble mismatched pins away, and skip the workers at midday,
I parse the YAML paths with care, infer prefixes from the lair,
With carrots, tests, and tidy docs — our pipelines dance in sync and locks." 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title "Validate cpflow gem and workflow pins" directly and concisely describes the main objective of the PR, which is to add validation for the cpflow gem version (CPFLOW_VERSION) and workflow reference pins (control_plane_flow_ref) to ensure they match.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jg-codex/validate-cpflow-version-pin

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented May 24, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds an early-failure guard to the cpflow-setup-environment composite action that ensures the installed cpflow gem version always matches the reusable workflow/action code version. It propagates a new control_plane_flow_ref input through every reusable workflow to the setup action, and validates the pairing in bash before any deployment work begins.

  • Adds normalize_release_ref and validate_cpflow_version_pin bash functions that reject CPFLOW_VERSION when control_plane_flow_ref is not a matching release tag (preventing silent gem/workflow version skew).
  • Propagates control_plane_flow_ref through all five cpflow-*.yml reusable workflows to the setup action.
  • Adds spec coverage that reads every cpflow-*.yml from disk and verifies each cpflow-setup-environment step passes the new input.

Confidence Score: 4/5

Safe to merge; the guard logic is correct and the five workflow files are all updated consistently.

The validation bash logic handles all the expected cases — empty CPFLOW_VERSION, SHA/branch refs, and mismatched release tags — correctly. The new spec test reads the live YAML files and verifies every setup-environment call carries the new input, providing good regression coverage. The only gap is that the pre-release label allow-list in the regex (test|beta|alpha|rc|pre) is narrower than what teams commonly use, which could produce a confusing error for tags like v5.0.0-dev-1. That is a forward-looking quality concern, not a current breakage.

.github/actions/cpflow-setup-environment/action.yml — the normalize_release_ref pre-release label allow-list is worth a second look if the project ever cuts dev or snapshot pre-release tags.

Important Files Changed

Filename Overview
.github/actions/cpflow-setup-environment/action.yml Adds control_plane_flow_ref input and validate_cpflow_version_pin bash logic; core guard implementation is sound with correct empty-string and SHA/branch short-circuit handling.
spec/command/generate_github_actions_spec.rb New spec asserts control_plane_flow_ref is wired to every setup-environment invocation in all cpflow-*.yml files and verifies validation strings are present in the action content.
.github/workflows/cpflow-deploy-staging.yml Both setup-environment calls in this workflow now correctly forward control_plane_flow_ref.
.github/workflows/cpflow-cleanup-stale-review-apps.yml Straightforward one-line addition of control_plane_flow_ref forwarding to the setup action call.
.github/workflows/cpflow-delete-review-app.yml Straightforward one-line addition of control_plane_flow_ref forwarding.
.github/workflows/cpflow-deploy-review-app.yml Straightforward one-line addition of control_plane_flow_ref forwarding.
.github/workflows/cpflow-promote-staging-to-production.yml Straightforward one-line addition of control_plane_flow_ref forwarding.
docs/ci-automation.md Documentation accurately describes the new validation behaviour and the SHA/branch testing constraint.

Sequence Diagram

sequenceDiagram
    participant Caller as Downstream Workflow
    participant RW as Reusable Workflow (cpflow-*.yml)
    participant SA as cpflow-setup-environment action
    participant Bash as validate_cpflow_version_pin

    Caller->>RW: "uses: cpflow-deploy-review-app@vX.Y.Z"
    RW->>SA: "control_plane_flow_ref=vX.Y.Z, cpflow_version=X.Y.Z"
    SA->>Bash: "CONTROL_PLANE_FLOW_REF=vX.Y.Z, CPFLOW_VERSION=X.Y.Z"
    Bash->>Bash: normalize_release_ref(vX.Y.Z) → X.Y.Z
    alt CPFLOW_VERSION matches ref tag
        Bash-->>SA: return 0 (pass)
        SA-->>RW: gem install cpflow -v X.Y.Z
    else CPFLOW_VERSION unset
        Bash-->>SA: return 0 (skip validation)
        SA-->>RW: build gem from source
    else ref is SHA/branch or version mismatch
        Bash-->>SA: exit 1 with error message
        SA-->>RW: workflow fails before deploy
    end
Loading

Reviews (1): Last reviewed commit: "Validate cpflow gem and workflow pins" | Re-trigger Greptile

normalize_release_ref() {
local ref="${1#refs/tags/}"

if [[ "${ref}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([.-](test|beta|alpha|rc|pre)[.-][0-9]+)?$ ]]; then

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 The normalize_release_ref regex only recognises the labels test|beta|alpha|rc|pre. Any tag using a different label (e.g. dev, snapshot, nightly) would produce an empty expected_version, causing the validation to fail with the generic "not a release tag" message even though the tag looks release-like. This affects anyone who cuts a tagged pre-release under a different convention. Consider either expanding the allowed set or documenting that only these five labels are supported.

Suggested change
if [[ "${ref}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([.-](test|beta|alpha|rc|pre)[.-][0-9]+)?$ ]]; then
if [[ "${ref}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([.-](test|beta|alpha|rc|pre|dev|snapshot|nightly)[.-][0-9]+)?$ ]]; then

Comment on lines +260 to +274
it "passes control_plane_flow_ref to every setup action call for gem/ref validation" do
Dir.glob(Cpflow.root_path.join(".github/workflows/cpflow-*.yml").to_s).each do |path|
workflow = YAML.load_file(path, aliases: true)

workflow.fetch("jobs").each_value do |job|
Array(job["steps"]).each do |step|
next unless step["uses"] == "./.cpflow/.github/actions/cpflow-setup-environment"

expect(step.fetch("with")).to include(
"control_plane_flow_ref" => "${{ inputs.control_plane_flow_ref }}"
), "#{path} setup action call must pass control_plane_flow_ref"
end
end
end
end

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 Spec only iterates steps, not reusable-workflow job definitions

The test skips jobs that use uses: at the job level (reusable-workflow call syntax) because job["steps"] is nil and Array(nil) returns []. Currently all cpflow-*.yml jobs use inline steps:, so nothing is missed today. If a future workflow wraps a reusable job without inline steps, a missing control_plane_flow_ref pass-through would silently go undetected by this test. A comment noting the assumption (# jobs using top-level 'uses:' are skipped – they cannot call composite actions directly) would prevent confusion.

expected_version="$(normalize_release_ref "${CONTROL_PLANE_FLOW_REF}")"

if [[ -z "${expected_version}" ]]; then
echo "::error::CPFLOW_VERSION can only be used when control_plane_flow_ref is a release tag like v${CPFLOW_VERSION}. Current control_plane_flow_ref: ${CONTROL_PLANE_FLOW_REF:-<empty>}. Leave CPFLOW_VERSION unset when testing a commit SHA or branch so cpflow is built from the same source as the reusable workflow." >&2

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

::error:: annotations must go to stdout, not stderr.

GitHub Actions runner only scans stdout for :: workflow commands. Redirecting to stderr with >&2 means the annotation won't appear as a formatted error in the GitHub UI (the job still fails via exit 1, but users lose the annotation that surfaces at the top of the workflow run).

Suggested change
echo "::error::CPFLOW_VERSION can only be used when control_plane_flow_ref is a release tag like v${CPFLOW_VERSION}. Current control_plane_flow_ref: ${CONTROL_PLANE_FLOW_REF:-<empty>}. Leave CPFLOW_VERSION unset when testing a commit SHA or branch so cpflow is built from the same source as the reusable workflow." >&2
echo "::error::CPFLOW_VERSION can only be used when control_plane_flow_ref is a release tag like v${CPFLOW_VERSION}. Current control_plane_flow_ref: ${CONTROL_PLANE_FLOW_REF:-<empty>}. Leave CPFLOW_VERSION unset when testing a commit SHA or branch so cpflow is built from the same source as the reusable workflow."

fi

if [[ "${CPFLOW_VERSION}" != "${expected_version}" ]]; then
echo "::error::CPFLOW_VERSION must match control_plane_flow_ref. CPFLOW_VERSION=${CPFLOW_VERSION}, control_plane_flow_ref=${CONTROL_PLANE_FLOW_REF}, expected CPFLOW_VERSION=${expected_version}." >&2

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same issue — >&2 suppresses the GitHub Actions annotation. Drop the redirect so the runner processes the ::error:: command.

Suggested change
echo "::error::CPFLOW_VERSION must match control_plane_flow_ref. CPFLOW_VERSION=${CPFLOW_VERSION}, control_plane_flow_ref=${CONTROL_PLANE_FLOW_REF}, expected CPFLOW_VERSION=${expected_version}." >&2
echo "::error::CPFLOW_VERSION must match control_plane_flow_ref. CPFLOW_VERSION=${CPFLOW_VERSION}, control_plane_flow_ref=${CONTROL_PLANE_FLOW_REF}, expected CPFLOW_VERSION=${expected_version}."

normalize_release_ref() {
local ref="${1#refs/tags/}"

if [[ "${ref}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([.-](test|beta|alpha|rc|pre)[.-][0-9]+)?$ ]]; then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The regex requires a separator character before the trailing digit ([.-][0-9]+), so tags like v5.0.0-rc1 or v5.0.0-beta2 (digit immediately after the label, which is common in practice) are silently treated as non-release refs. A user setting CPFLOW_VERSION=5.0.0.rc.1 with control_plane_flow_ref: v5.0.0-rc1 would hit "not a release tag" rather than the more helpful "mismatch" error.

If the project's tagging convention always includes a separator (e.g. v5.0.0-rc.1), this is fine as-is — but it's worth a comment documenting that restriction so future release tooling knows to include the separator.

@@ -252,6 +257,22 @@ def shared_workflow_path(name)
expect(contents).not_to include("gem install cpflow -v ${{ inputs.cpflow_version }}")
end

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This test reads from Cpflow.root_path (the source tree) rather than the generated playground like the surrounding tests. That means it validates the checked-in workflow files but not the output of the generate_github_actions command.

This is fine as a static regression guard, but it's placed inside a spec that otherwise tests generated output, which could confuse a future contributor. A brief comment noting the intent (e.g. # validates source workflows, not generated output) would help, or move it into a separate describe block with a descriptive name like "source reusable workflows" to make the distinction explicit.

@claude

claude Bot commented May 24, 2026

Copy link
Copy Markdown
Contributor

Review: Validate cpflow gem and workflow pins

This PR addresses a real operational hazard — mismatched gem/workflow versions are a silent failure mode that's hard to debug after the fact. The implementation is clean and the error messages are actionable.

What the PR does

Adds a validate_cpflow_version_pin guard to the cpflow-setup-environment composite action. When CPFLOW_VERSION is set, it:

  1. Normalizes control_plane_flow_ref to a RubyGems version string (strips refs/tags/, leading v, converts -.).
  2. Rejects refs that aren't release tags (SHAs, branches, bare labels).
  3. Rejects mismatches between the gem version and the ref.

All five reusable workflows are updated to propagate control_plane_flow_ref into the setup action call.


Issues

Medium — GitHub Actions annotation suppressed by >&2 (two occurrences)

echo "::error::..." only creates a GitHub Actions annotation when written to stdout. The >&2 redirect means the annotation is lost; users see a raw stderr line in the step log instead of a formatted error surfaced at the top of the workflow run. The job still fails via exit 1, but the UX is degraded. See inline comments on action.yml lines 121 and 126.

Low — Regex misses common prerelease formats

The pattern [.-](label)[.-][0-9]+ requires a separator before the trailing digit, so tags like v5.0.0-rc1 or v5.0.0-beta2 (no separator before the number) are silently classified as "not a release tag" rather than producing the more informative "mismatch" error. If the project's tagging convention always includes a separator this is harmless, but it's worth documenting the constraint. See inline comment on action.yml line 108.

Low — Spec test reads source files instead of generated output

The new "passes control_plane_flow_ref to every setup action call" test globs Cpflow.root_path/.github/workflows/cpflow-*.yml directly. Unlike the surrounding tests it is validating the checked-in source workflows, not the generate_github_actions command's output. This works fine as a static regression guard but is architecturally inconsistent with the rest of the spec. See inline comment on spec line 258.


Strengths

  • The guard fires early (before any npm install / gem install) — fast feedback for misconfiguration.
  • Error messages include all three relevant values (CPFLOW_VERSION, control_plane_flow_ref, expected version), making diagnosis straightforward.
  • normalize_release_ref handles both refs/tags/v5.0.0 and bare v5.0.0 via ${1#refs/tags/}.
  • Documentation in docs/ci-automation.md and cpflow-help.md is clear and directly addresses the failure modes.
  • Changelog entry is placed correctly in [Unreleased].
  • The regression spec will catch any future workflow that omits control_plane_flow_ref propagation.

@justin808 justin808 force-pushed the jg-codex/validate-cpflow-version-pin branch from 6bfae53 to 6a72379 Compare May 24, 2026 07:34
Comment thread lib/generator_templates/entrypoint.sh

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6a72379ef8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread lib/generator_templates/entrypoint.sh Outdated
Comment on lines +5 to +6
if { [ "${1:-}" = "./bin/rails" ] && [ "${2:-}" = "server" ]; } ||
{ [ "${1:-}" = "./bin/thrust" ] && [ "${2:-}" = "./bin/rails" ] && [ "${3:-}" = "server" ]; }; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Handle default Rails command when gating db:prepare

The new guard only runs db:prepare for ./bin/rails server (or the Thruster variant), but the generated Dockerfile still starts the app with CMD ["rails", "s"] (lib/generator_templates/Dockerfile), so this condition is false in the default generated runtime and database preparation is skipped entirely. That changes behavior from “always prepare DB before app boot” to “often skip prepare,” which can break startup on pending migrations or first-time boot.

Useful? React with 👍 / 👎.

normalize_release_ref() {
local ref="${1#refs/tags/}"

if [[ "${ref}" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)([.-](test|beta|alpha|rc|pre|dev|snapshot|nightly)[.-]?([0-9]+))?$ ]]; then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The regex requires at least one digit after the pre-release label ([0-9]+), so tags like v5.0.0-beta (no trailing number) fall through to normalize_release_ref returning empty string. The caller then fails with the "must be a release tag" error rather than a mismatch error — which is technically correct but potentially confusing.

More importantly, the regex doesn't recognise a leading dot separator for the label itself — v5.0.0.pre.1 won't match because the first separator group is [.-] and the match expects exactly one separator token before the label. Let me expand:

  • v5.0.0-pre.1 ✅ matches (- before pre, . before 1)
  • v5.0.0.pre.1 ❌ does not match — the .pre segment is caught by [.-], but the trailing .1 needs [.-]?([0-9]+) which requires the ? to be . here; this should actually work. Let me recheck: .pre.1 → separator ., label pre, separator ., number 1 — this does match.

The actual gap is numbered pre-release tags like v5.0.0-beta (no number) returning empty/no match, silently falling back to the SHA-style error. Consider documenting that bare pre-release labels (without a number) are not supported, or extend the regex to accept them:

Suggested change
if [[ "${ref}" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)([.-](test|beta|alpha|rc|pre|dev|snapshot|nightly)[.-]?([0-9]+))?$ ]]; then
if [[ "${ref}" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)([.-](test|beta|alpha|rc|pre|dev|snapshot|nightly)([.-]([0-9]+))?)?$ ]]; then

This makes the trailing number optional (group 5 becomes the number) and avoids the confusing empty-string path for v5.0.0-beta.


if [[ "${ref}" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)([.-](test|beta|alpha|rc|pre|dev|snapshot|nightly)[.-]?([0-9]+))?$ ]]; then
if [[ -n "${BASH_REMATCH[3]}" ]]; then
echo "${BASH_REMATCH[1]}.${BASH_REMATCH[3]}.${BASH_REMATCH[4]}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

When BASH_REMATCH[3] is non-empty but BASH_REMATCH[4] is empty (which the current regex prevents via [0-9]+, but would occur if the regex is loosened to allow bare labels), this produces a trailing dot — e.g. 1.2.3.beta.. Worth adding a guard now if the regex is extended:

Suggested change
echo "${BASH_REMATCH[1]}.${BASH_REMATCH[3]}.${BASH_REMATCH[4]}"
echo "${BASH_REMATCH[1]}.${BASH_REMATCH[3]}${BASH_REMATCH[4]:+.${BASH_REMATCH[4]}}"

This appends .N only when the number capture is non-empty, so v5.0.0-beta5.0.0.beta and v5.0.0-beta.15.0.0.beta.1.

@@ -1,8 +1,12 @@
#!/bin/sh
set -e

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

set -e is a generated-file breaking change for anyone with an existing entrypoint.sh who has already customised it. Before this change, a failing intermediate command (e.g. a curl health-check probe that temporarily returns non-zero) would be silently ignored; now it terminates the container.

The changelog entry covers this, but it's worth flagging in the PR that existing apps need to audit their entrypoint after upgrading. A regeneration (cpflow generate) would produce the new template, but handwritten additions could still break.

Also note that this file uses #!/bin/sh, not #!/bin/bash. set -e is valid POSIX sh, but its edge-case behaviour differs between shells (especially inside if/&& chains). Since the template only performs simple commands this is fine in practice, but worth being aware of.

Comment thread lib/generator_templates/entrypoint.sh Outdated
Comment on lines +5 to +6
if { [ "${1:-}" = "./bin/rails" ] && [ "${2:-}" = "server" ]; } ||
{ [ "${1:-}" = "./bin/thrust" ] && [ "${2:-}" = "./bin/rails" ] && [ "${3:-}" = "server" ]; }; then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The check is exact-string matching on argv[0..2]. A few gaps that could silently skip DB prep:

  • ./bin/rails s (the common short alias for server) — does not match the server literal.
  • bundle exec rails server — not matched (different argv[0]).
  • ./bin/puma or a custom Procfile entry that ultimately starts the server without going through ./bin/rails server explicitly.

If the intent is to only cover the exact commands that cpflow generate emits, that's fine — just worth a comment so maintainers know this is a deliberate narrowing. If you want to also catch the short alias:

Suggested change
if { [ "${1:-}" = "./bin/rails" ] && [ "${2:-}" = "server" ]; } ||
{ [ "${1:-}" = "./bin/thrust" ] && [ "${2:-}" = "./bin/rails" ] && [ "${3:-}" = "server" ]; }; then
if { [ "${1:-}" = "./bin/rails" ] && { [ "${2:-}" = "server" ] || [ "${2:-}" = "s" ]; }; } ||
{ [ "${1:-}" = "./bin/thrust" ] && [ "${2:-}" = "./bin/rails" ] && { [ "${3:-}" = "server" ] || [ "${3:-}" = "s" ]; }; }; then

Comment on lines 257 to 278
expect(contents).not_to include("gem install cpflow -v ${{ inputs.cpflow_version }}")
end

it "passes control_plane_flow_ref to every setup action call for gem/ref validation" do
# This validates checked-in reusable workflows, not generated app wrappers.
Dir.glob(Cpflow.root_path.join(".github/workflows/cpflow-*.yml").to_s).each do |path|
workflow = YAML.load_file(path, aliases: true)

workflow.fetch("jobs").each_value do |job|
# Job-level `uses:` entries call reusable workflows; they cannot call composite actions directly.
Array(job["steps"]).each do |step|
next unless step["uses"] == "./.cpflow/.github/actions/cpflow-setup-environment"

expect(step.fetch("with")).to include(
"control_plane_flow_ref" => "${{ inputs.control_plane_flow_ref }}"
), "#{path} setup action call must pass control_plane_flow_ref"
end
end
end
end

it "exposes Docker build action inputs" do

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Good regression guard. One gap: the test verifies that control_plane_flow_ref is passed in the with: block but not that it is declared as a workflow_call input in each reusable workflow file. If the declaration were accidentally removed, GitHub Actions would silently resolve ${{ inputs.control_plane_flow_ref }} to an empty string rather than failing loudly.

Consider extending the check to verify the input declaration exists at the workflow level:

workflow_inputs = workflow.dig("on", "workflow_call", "inputs") || {}
expect(workflow_inputs).to have_key("control_plane_flow_ref"),
  "#{path} must declare control_plane_flow_ref as a workflow_call input"

This is low-urgency since all current workflows already have the declaration — it's purely a future-proofing guard.

@claude

claude Bot commented May 24, 2026

Copy link
Copy Markdown
Contributor

Review: Validate cpflow gem and workflow pins

Overall: Well-structured safety guard. The version-pin validation, workflow propagation, and spec coverage are all solid. A few items to address before merge.


What the PR does

  1. Adds validate_cpflow_version_pin in the composite setup action — fails early when CPFLOW_VERSION is set but does not match the release tag implied by control_plane_flow_ref.
  2. Propagates control_plane_flow_ref through all five reusable workflow files to the setup action.
  3. Tightens entrypoint.sh — adds set -e and limits rails db:prepare to server workloads only.
  4. Adds spec coverage — a YAML-walking test that catches any future workflow that forgets to pass the input.

Issues

normalize_release_ref — bare pre-release labels silently rejected

Tags like v5.0.0-beta (no trailing number) do not match the regex because [0-9]+ requires at least one digit. The function returns an empty string, and the caller fails with the misleading "must be a release tag" error rather than a useful mismatch message. See inline comment on line 108.

Trailing-dot risk if regex is ever relaxed

The echo "${BASH_REMATCH[1]}.${BASH_REMATCH[3]}.${BASH_REMATCH[4]}" path produces a trailing dot (e.g. 5.0.0.beta.) when group 4 is empty. The current regex prevents this, but a future edit could reintroduce it. A defensive parameter expansion avoids the trap entirely. See inline comment on line 110.

entrypoint.sh./bin/rails s short alias skips DB prep

The condition only matches the literal string server, not s. Anyone who runs ./bin/rails s as their workload command gets no DB prep. Low probability if cpflow generate always emits the full form, but worth either matching s explicitly or documenting the intent. See inline comment on lines 5-6.

set -e is a generated-file breaking change

Existing apps with hand-edited entrypoints could break silently on upgrade if they have intermediate commands that currently fail-and-continue. The changelog documents it, but an explicit upgrade note pointing users to re-run cpflow generate would help. See inline comment on line 2.


Minor / non-blocking

  • Spec gap: The YAML-walking test confirms control_plane_flow_ref is passed in with:, but does not assert it is declared as a workflow_call input. All current files are correct; this would just be a future-proofing guard. See inline comment on spec lines 257-278.
  • Pre-release gem version format: normalize_release_ref normalises v1.2.3-beta.1 to 1.2.3.beta.1 (RubyGems pre-release convention). That is correct — just confirming the mapping is intentional.
  • Security: CONTROL_PLANE_FLOW_REF is interpolated directly into the ::error:: annotation string. This is safe because the value comes from a trusted workflow input (not user-controlled PR data), but note that newlines in the value could emit multiple annotation lines. Not a real risk in this context.

Verdict

Approve after addressing the ./bin/rails s alias gap (or explicitly documenting it as out-of-scope) and confirming pre-release tag handling is intended. The spec and propagation work are done correctly.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
CHANGELOG.md (1)

17-21: ⚡ Quick win

Match Unreleased entries to existing changelog attribution format.

These new bullets are clear, but they currently skip the file’s normal pattern (bold lead + PR link + author), which makes traceability uneven across releases.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` around lines 17 - 21, Update the two new changelog bullets to
follow the repository’s standard attribution format: for the “Added a generated
workflow guard that fails early when `CPFLOW_VERSION`…” entry and the “Fixed
generated Control Plane entrypoints…” entry, prepend the bold lead token (e.g.,
**Added** / **Fixed**) if missing, and append the PR reference and author in the
normal pattern used throughout the file (for example: “(PR #<number> by
@<author>)”). Ensure both bullets match the surrounding entries’ exact
punctuation and spacing so traceability is consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/actions/cpflow-setup-environment/action.yml:
- Around line 105-115: The normalize_release_ref function is too strict: update
its regex to accept general SemVer prerelease identifiers (alphanumeric, hyphens
and dot-separated parts) instead of only the small set with numeric suffixes,
and when a prerelease is present return a normalized version that includes the
prerelease component (for example capture the base version in BASH_REMATCH[1]
and the full prerelease in another capture and echo them combined), so
normalize_release_ref accepts tags like v1.2.3-alpha, v1.2.3-beta.1,
v1.2.3-rc.2+build etc. and still strips the leading refs/tags/.

---

Nitpick comments:
In `@CHANGELOG.md`:
- Around line 17-21: Update the two new changelog bullets to follow the
repository’s standard attribution format: for the “Added a generated workflow
guard that fails early when `CPFLOW_VERSION`…” entry and the “Fixed generated
Control Plane entrypoints…” entry, prepend the bold lead token (e.g., **Added**
/ **Fixed**) if missing, and append the PR reference and author in the normal
pattern used throughout the file (for example: “(PR #<number> by @<author>)”).
Ensure both bullets match the surrounding entries’ exact punctuation and spacing
so traceability is consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d83c7c3e-63aa-4625-91c5-6bf49811fc86

📥 Commits

Reviewing files that changed from the base of the PR and between 8ad88b0 and 6a72379.

📒 Files selected for processing (12)
  • .github/actions/cpflow-setup-environment/action.yml
  • .github/workflows/cpflow-cleanup-stale-review-apps.yml
  • .github/workflows/cpflow-delete-review-app.yml
  • .github/workflows/cpflow-deploy-review-app.yml
  • .github/workflows/cpflow-deploy-staging.yml
  • .github/workflows/cpflow-promote-staging-to-production.yml
  • CHANGELOG.md
  • docs/ci-automation.md
  • lib/generator_templates/entrypoint.sh
  • lib/github_flow_templates/.github/cpflow-help.md
  • spec/command/generate_github_actions_spec.rb
  • spec/command/generate_spec.rb

Comment thread .github/actions/cpflow-setup-environment/action.yml
@justin808 justin808 force-pushed the jg-codex/validate-cpflow-version-pin branch from 6a72379 to 5f575b9 Compare May 24, 2026 07:42

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5f575b931a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +128 to +129
if [[ "${CPFLOW_VERSION}" != "${expected_version}" ]]; then
echo "::error::CPFLOW_VERSION must match control_plane_flow_ref. CPFLOW_VERSION=${CPFLOW_VERSION}, control_plane_flow_ref=${CONTROL_PLANE_FLOW_REF}, expected CPFLOW_VERSION=${expected_version}."

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 Badge Normalize prerelease version forms before pin check

validate_cpflow_version_pin compares raw strings, but normalize_release_ref rewrites tags like v5.1.0-rc1 to 5.1.0.rc.1; a caller setting CPFLOW_VERSION=5.1.0.rc1 (the common RubyGems form) will be rejected even though both forms resolve to the same gem version. This makes prerelease tag + gem pin combinations fail early despite being semantically equivalent, so prerelease workflows can be blocked by formatting differences alone.

Useful? React with 👍 / 👎.

@claude

claude Bot commented May 24, 2026

Copy link
Copy Markdown
Contributor

Code Review: Validate cpflow gem and workflow pins

Overview

This PR adds a version-consistency guard between the `cpflow` gem and the reusable workflow/action code loaded from a `control_plane_flow_ref` Git ref, and tightens the generated `entrypoint.sh` to run `db:prepare` only for Rails server workloads. Both changes address real operational risks.

Strengths

  • Clear failure mode: `validate_cpflow_version_pin` surfaces the mismatch at setup time and emits a structured `::error::` annotation visible in the Actions summary — before any deploy work begins.
  • Regex normalization: `normalize_release_ref` correctly converts both `refs/tags/v5.0.0-rc.1` and bare `v5.0.0-rc.1` to the dot-separated RubyGems version string, handling the dash-to-dot convention difference between Git tags and RubyGems.
  • Entrypoint `set -e`: A failed `db:prepare` now stops the container immediately rather than silently continuing.
  • Regression spec: The new spec that iterates over all `cpflow-*.yml` files will catch any future workflow that forgets to declare or propagate `control_plane_flow_ref`.

Issues

Medium — Spec raises `KeyError` (not an assertion failure) for non-reusable-workflow files

In `spec/command/generate_github_actions_spec.rb`, the test does:

```ruby
workflow_inputs = (workflow["on"] || workflow[true]).fetch("workflow_call").fetch("inputs")
```

If a future `cpflow-*.yml` is not a reusable workflow (e.g., only `schedule:` trigger, no `workflow_call:` key), `.fetch("workflow_call")` raises `KeyError` with no indication of which file caused it. A `next unless` guard or an explicit `have_key` expectation with the path interpolated in the message would make future failures much easier to diagnose.

Low — Pre-release hint in first error message may suggest the wrong tag format

The first validation error reads `release tag like v${CPFLOW_VERSION}`. For a dot-separated RubyGems pre-release like `CPFLOW_VERSION=5.0.0.rc.1` it renders as `v5.0.0.rc.1`, which `normalize_release_ref` does accept—but users familiar with Git tag conventions may have published `v5.0.0-rc.1` (dash-separated). The second error message (`expected CPFLOW_VERSION=...`) provides the correct value, so this is a minor UX nit rather than a correctness bug.

Low — `is_rails_server_command` skips DB prep for `./bin/thrust bundle exec rails server`

The function strips `./bin/thrust` after the `bundle exec` check, so `./bin/thrust bundle exec rails server` leaves `bundle` as `$1` and returns false (DB prep skipped). In practice Thruster wraps the Rails binary directly (`./bin/thrust rails server`), not `bundle exec`, so no generated command hits this. Re-ordering—strip `bundle exec` first, then `./bin/thrust`, then check `rails`/`./bin/rails`—would handle all combinations defensively.

Minor Nits

  • `normalize_release_ref` includes `snapshot` and `nightly` labels that are not standard RubyGems identifiers and have no known use in this gem's release history. Trimming to `alpha`, `beta`, `rc`, `pre` would reduce regex complexity.
  • The comment "workers and renderers skip DB prep" uses non-standard Rails terminology; "non-server workloads" or "other workloads" would be clearer to future readers.
  • The entrypoint change is logged under Fixed in the CHANGELOG, but the PR body describes it as a meaningful behavioral change (DB prep gated on server commands + `set -e` fast-fail). Consider whether apps with hand-edited entrypoints deserve a Breaking Changes entry, or at least a cross-reference to the upgrade note already in the PR description.

Dir.glob(Cpflow.root_path.join(".github/workflows/cpflow-*.yml").to_s).each do |path|
workflow = YAML.load_file(path, aliases: true)
workflow_inputs = (workflow["on"] || workflow[true]).fetch("workflow_call").fetch("inputs")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

.fetch("workflow_call") raises KeyError with no file context if a future cpflow-*.yml doesn't have a workflow_call: trigger. Consider guarding with next unless or replacing with an explicit expectation that includes path in the failure message:

Suggested change
workflow_on = workflow["on"] || workflow[true]
next unless workflow_on.key?("workflow_call")
workflow_inputs = workflow_on.fetch("workflow_call").fetch("inputs")

Comment thread lib/generator_templates/entrypoint.sh Outdated
shift 2
fi

if [ "${1:-}" = "./bin/thrust" ]; then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The bundle exec strip runs before this Thruster check, so bundle exec ./bin/thrust rails server is handled correctly. However, ./bin/thrust bundle exec rails server (Thruster wrapping bundle exec) reaches this shift and leaves bundle as $1, causing the function to return false and skip DB prep.

In practice the generated Thruster wrapper uses ./bin/thrust rails server (not bundle exec), so this won't occur. If full defense-in-depth is desired, re-ordering to strip bundle exec last (after stripping ./bin/thrust) would cover this case:

Suggested change
if [ "${1:-}" = "./bin/thrust" ]; then
if [ "${1:-}" = "./bin/thrust" ]; then
shift
fi
if [ "${1:-}" = "bundle" ] && [ "${2:-}" = "exec" ]; then
shift 2
fi

@justin808 justin808 force-pushed the jg-codex/validate-cpflow-version-pin branch from 5f575b9 to 6ab284f Compare May 24, 2026 07:50
normalize_version "${ref}"
}

validate_cpflow_version_pin() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The hint v${CPFLOW_VERSION} constructs the expected tag name from the user-supplied gem version. For stable releases this is clear (v5.0.0). For dot-separated pre-release versions (e.g. CPFLOW_VERSION=5.0.0.rc.1) it renders as v5.0.0.rc.1, which normalize_release_ref does accept—but users who published v5.0.0-rc.1 (dash-separated, the more common Git tag convention) may be confused. Adding a note that both formats are accepted (or showing the dash form) would reduce ambiguity:

Suggested change
validate_cpflow_version_pin() {
echo "::error::CPFLOW_VERSION can only be used when control_plane_flow_ref is a release tag (e.g. v${CPFLOW_VERSION} or v$(echo "${CPFLOW_VERSION}" | tr '.' '-' | sed 's/-/-/2')). Current control_plane_flow_ref: ${CONTROL_PLANE_FLOW_REF:-<empty>}. Leave CPFLOW_VERSION unset when testing a commit SHA or branch so cpflow is built from the same source as the reusable workflow."

Or more simply, appending (both v5.0.0-rc.1 and v5.0.0.rc.1 are accepted for pre-release tags) to the message would be sufficient.

Comment thread lib/generator_templates/entrypoint.sh

local actual_version
local expected_version
actual_version="$(normalize_version "${CPFLOW_VERSION}")"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

normalize_version is silently lossy for unrecognized formats

If CPFLOW_VERSION doesn't match the regex, normalize_version exits 0 with empty output. set -euo pipefail won't catch this, and actual_version becomes an empty string. The second if then fires with normalized CPFLOW_VERSION=<unrecognized>, which is decent, but a caller that checks only the exit code would see success incorrectly.

Consider an explicit guard immediately after computing actual_version:

Suggested change
actual_version="$(normalize_version "${CPFLOW_VERSION}")"
actual_version="$(normalize_version "${CPFLOW_VERSION}")"
if [[ -z "${actual_version}" ]]; then
echo "::error::CPFLOW_VERSION='${CPFLOW_VERSION}' is not a recognized semver format (expected X.Y.Z or X.Y.Z-prerelease such as X.Y.Z-rc.1)."
exit 1
fi
expected_version="$(normalize_release_ref "${CONTROL_PLANE_FLOW_REF}")"

normalize_release_ref() {
local ref="${1#refs/tags/}"

[[ "${ref}" == v* ]] || return 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Silent no-op is easy to misread

return 0 conventionally signals success, but here it means "I recognised this as a non-release ref and intentionally produced no output." The caller in validate_cpflow_version_pin checks the captured output — not the exit code — so the logic is correct. But a future maintainer seeing return 0 may assume the function succeeded and "fix" it to return 1, breaking the empty-output signal.

Suggested change
[[ "${ref}" == v* ]] || return 0
[[ "${ref}" == v* ]] || return 0 # non-v refs produce empty output intentionally; caller checks output, not exit code

@@ -15,10 +15,6 @@ permissions:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Removing this guard means the help comment fires on every PR open for any repo that has committed this generated wrapper — even repos that have not yet configured CPLN_TOKEN_STAGING. The new text inside the help message mentions the token requirement, which softens the impact, but contributors to repos mid-migration may see confusing workflow comments.

Consider an alternative guard that is less likely to produce false positives than vars.REVIEW_APP_PREFIX != '' but more informative than no guard — for example, checking whether .controlplane/controlplane.yml exists in the repo. That check would require a lightweight checkout step, though, which adds complexity. The current approach (document it, let consumers remove the wrapper) is pragmatically sound; just make sure the breaking change notice in the CHANGELOG is prominent.

Comment on lines +53 to +58
rescue ArgumentError, TypeError => e
raise unless e.message.include?("unknown keyword") ||
e.message.include?("wrong number of arguments") ||
e.message.include?("no implicit conversion of Hash")

YAML.safe_load(contents, [], [], true)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Fragile Ruby version compatibility check via error message strings

The rescue guard detects old-Ruby YAML API by matching specific exception message substrings. This can break if Psych changes its error messages, and the "no implicit conversion of Hash" string is particularly obscure (it would suggest a calling bug, not a version compat issue).

A cleaner approach is to branch on RUBY_VERSION directly before attempting the call:

Suggested change
rescue ArgumentError, TypeError => e
raise unless e.message.include?("unknown keyword") ||
e.message.include?("wrong number of arguments") ||
e.message.include?("no implicit conversion of Hash")
YAML.safe_load(contents, [], [], true)
rescue ArgumentError, TypeError => e
raise unless e.message.include?("unknown keyword") ||
e.message.include?("wrong number of arguments")
YAML.safe_load(contents, [], [], true)

Or, more robustly:

if RUBY_VERSION >= "3.1"
  YAML.safe_load(contents, aliases: true)
else
  YAML.safe_load(contents, [], [], true)
end

The "no implicit conversion of Hash" string doesn't correspond to a known Psych/Ruby version gate and would silently swallow unrelated TypeErrors.

normalize_release_ref() {
local ref="${1#refs/tags/}"

[[ "${ref}" == v* ]] || return 0 # Non-v refs intentionally produce empty output.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

return 0 used to signal "no output" — counterintuitive contract

return 0 is the standard success exit code, so normalize_release_ref exits successfully whether it produced output or not. Callers must infer "nothing produced" from the empty stdout, but return 0 gives no hint that empty output is the intentional signal for non-v refs. The comment helps, but callers that forget to check for empty output and proceed anyway will silently normalize a SHA or branch name as if it had matched.

Consider returning 1 (and documenting it) to make the "non-release ref" path a distinct, checkable failure, or rename to try_normalize_release_ref to signal the nullable contract.

Suggested change
[[ "${ref}" == v* ]] || return 0 # Non-v refs intentionally produce empty output.
[[ "${ref}" == v* ]] || return 1 # Non-v refs produce no output; callers must check.

Comment on lines +131 to +133
if ! tag_refs="$(git ls-remote --tags "${remote_url}" "refs/tags/${ref}" "refs/tags/${ref}^{}")"; then
echo "::error::Could not verify control_plane_flow_ref against ${remote_url}. Leave CPFLOW_VERSION unset when testing a commit SHA or branch."
exit 1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Outbound git ls-remote on every CI run that sets CPFLOW_VERSION

This is a necessary guard, but it adds a synchronous network round-trip to https://github.com before any build work starts. Two things worth noting:

  1. Enterprise/restricted runners that block outbound HTTPS traffic (or route through a proxy) will fail here with the generic "Could not verify" error, which may be confusing. Documenting this pre-requisite in ci-automation.md under "Testing Unreleased Changes" would help those users.

  2. Error path is exit 1 inside a function called without !: if git ls-remote itself fails (network timeout, non-zero exit), the if ! captures that and the error branch runs — this is correct and intentional, just worth confirming the exit 1 path is expected to be terminal here rather than return 1 so callers can decide.

Comment thread lib/generator_templates/entrypoint.sh Outdated
Comment on lines +10 to +14
# Matches generated, flag-free Thruster invocations. Hand-edited commands with
# env prefixes or Thruster flags before rails skip generated DB prep.
if [ "${1:-}" = "thrust" ] || [ "${1:-}" = "bin/thrust" ] || [ "${1:-}" = "./bin/thrust" ]; then
shift
fi

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

env VAR=value rails s is a common pattern that silently skips DB prep

The comment documents this gap but the gap is wider than it looks: env RAILS_LOG_TO_STDOUT=1 rails s, env PORT=3000 ./bin/rails s, or any use of the POSIX env builtin before the command will not be detected as a Rails server command, so db:prepare won't run. This is a documented limitation, but it means any user who customizes the container CMD with an env … prefix will lose DB prep silently.

An alternative that handles the env case without major complexity:

# Strip leading `env KEY=VALUE` pairs before matching
while [ "${1:-}" = "env" ]; do
  shift
  while [ $# -gt 0 ] && echo "${1}" | grep -q '='; do
    shift
  done
done

If keeping the current scope is preferred, consider making the comment on line 10-11 a visible # WARNING: so it is harder to miss when auditing hand-edited entrypoints.

Comment on lines +20 to +22
CPLN_TOKEN_PRODUCTION:
description: Production Control Plane token. Store it on the protected production Environment; do not pass it from a repository secret.
required: false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

required: false means callers can pass CPLN_TOKEN_PRODUCTION from a repo secret

Declaring CPLN_TOKEN_PRODUCTION as a callable secret (even with required: false) means any caller can override the production environment by passing CPLN_TOKEN_PRODUCTION: ${{ secrets.CPLN_TOKEN_PRODUCTION }} from a repository secret. That would bypass the environment: production approval gate entirely, since the job would receive the token from the caller's secret scope before the environment gate has a chance to inject it.

The validate production token step only checks that the token is present, not that it came from the environment. A caller that passes the repo secret would pass that check without any reviewer approval.

Consider removing CPLN_TOKEN_PRODUCTION from the workflow_call secrets block entirely and relying exclusively on the environment: production injection. The token will still be available as secrets.CPLN_TOKEN_PRODUCTION in the production job because of environment: ${{ inputs.production_environment }}; it just won't be an explicit callable secret anymore.

@claude

claude Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

Code Review: Validate cpflow gem and workflow pins

Overview

This PR delivers three well-scoped improvements: (1) a fail-fast guard that rejects mismatched CPFLOW_VERSION/control_plane_flow_ref combinations, (2) a new cpflow-resolve-review-config composite action that infers review-app config from controlplane.yml eliminating several required repo variables, and (3) hardened secret passing — removing secrets: inherit across all generated wrappers and scoping the production token to a protected GitHub Environment. The direction is right and the spec coverage is thorough.


Critical / Security

CPLN_TOKEN_PRODUCTION required: false leaves the environment-gate bypassable (inline comment on cpflow-promote-staging-to-production.yml lines 20–22)

Declaring CPLN_TOKEN_PRODUCTION in the reusable workflow's on.workflow_call.secrets block (even as required: false) means a caller can pass CPLN_TOKEN_PRODUCTION: ${{ secrets.CPLN_TOKEN_PRODUCTION }} from a repository secret. When a caller does that, the token arrives in the job's secrets context before the environment: production gate runs, so the approval step is bypassed and the validate-token check still passes. Removing the entry from the workflow_call.secrets block entirely — and relying solely on environment injection — closes this gap without any loss of functionality. The token will still be available as secrets.CPLN_TOKEN_PRODUCTION inside the production job because of environment: ${{ inputs.production_environment }}.


Code Quality

Fragile Ruby version compat detection in safe_load_yaml_file (inline comment on cpflow-resolve-review-config/action.yml lines 53–58)

The rescue block detects the old pre-3.1 YAML.safe_load positional-argument API by matching exception message substrings. The "no implicit conversion of Hash" string is not a Psych version-compat signal — it would indicate a calling bug — and silently swallowing it could mask real errors. Branching on RUBY_VERSION >= "3.1" or using Gem::Version comparison would be cleaner and more correct.

normalize_release_ref uses return 0 to mean "no output" (inline comment on cpflow-setup-environment/action.yml line 118)

Exiting with success (0) to signal "this ref did not match" is an unusual contract. Callers must infer the absence of output rather than checking an exit code. Using return 1 for the non-v case would make the distinction explicit and catchable.


Performance / Reliability

git ls-remote adds a synchronous outbound network call on every CI run with CPFLOW_VERSION set (inline comment on cpflow-setup-environment/action.yml lines 131–133)

This is a necessary guard, but it has two practical implications: it adds latency (typically small, but variable), and it will hard-fail on restricted CI runners that block outbound HTTPS. The error message is clear, but the network prerequisite is not mentioned in the docs section on setting CPFLOW_VERSION. One line in ci-automation.md would prevent confusion in enterprise environments.


Entrypoint

env VAR=value rails s silently skips DB prep (inline comment on lib/generator_templates/entrypoint.sh lines 10–14)

This is documented, but the documentation is a soft inline comment that can easily be missed when auditing a hand-edited entrypoint. Promoting the note to a # WARNING: prefix or adding a short strip-env-prefixes loop would make the limitation harder to miss. Patterns like env RAILS_LOG_TO_STDOUT=1 rails s are common in containerised Rails setups and won't trigger db:prepare.


Positives

  • Replacing secrets: inherit with explicit named secrets across all generated wrappers is a meaningful security improvement aligned with least-privilege.
  • The validate_github_env_value! regex ([A-Za-z0-9-]+) correctly prevents newline/special-character injection into GITHUB_ENV / GITHUB_OUTPUT before writes happen.
  • Annotated-tag handling in verify_release_ref_matches_checkout (prioritising the ^{} peeled ref over the tag-object SHA) is correct for both lightweight and annotated tags.
  • cpflow-resolve-review-config uses YAML.safe_load (not YAML.load), preventing arbitrary code execution from a malicious controlplane.yml.
  • The test-cpflow-github-flow additions (secrets: inherit guard, production_environment requirement, CPLN_TOKEN_PRODUCTION exclusion from caller) make the new invariants machine-checkable.
  • Spec coverage is comprehensive and tests the right things — content assertions against the real generated files rather than mocks.

@claude

claude Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

Code Review

Overview

This PR tightens the cpflow GitHub Actions security posture in three key areas: (1) version pin validation between the cpflow gem and the control_plane_flow_ref GitHub ref, (2) replacing broad secrets: inherit with explicit secret maps across all generated caller workflows, and (3) gating CPLN_TOKEN_PRODUCTION behind a GitHub Environment approval gate rather than passing it as a caller secret. The generated entrypoint also gets meaningful hardening — set -e for fail-fast behavior and scoping DB prep to recognised Rails server commands only.

The changes are well-structured, test coverage is thorough, and the documentation is unusually detailed. A few things worth discussing:


Security — Strong improvements

secrets: inherit → explicit maps (all generated callers)
This is the most impactful security fix in the PR. secrets: inherit passes the entire caller's secret namespace into the upstream reusable workflow; the upstream workflow only reads what it declares, but a compromised or updated upstream step could read anything. Switching to named-only maps (CPLN_TOKEN_STAGING, DOCKER_BUILD_SSH_KEY) closes that attack surface and makes the secret boundary visible in the caller's YAML. Good call.

CPLN_TOKEN_PRODUCTION on a protected GitHub Environment
Removing CPLN_TOKEN_PRODUCTION from the workflow_call secrets block and instead relying on GitHub Environment injection is the correct approach for production secrets. The bin/test-cpflow-github-flow guard that aborts if a caller passes CPLN_TOKEN_PRODUCTION as a caller secret is a solid enforcement mechanism — it prevents downstream repos from inadvertently widening the blast radius.

cpflow-resolve-review-config: YAML injection prevention
Values read from controlplane.yml are validated against /\A[A-Za-z0-9-]+\z/ before being written to GITHUB_ENV and GITHUB_OUTPUT. This correctly prevents newline-injection attacks even on adversarially crafted YAML. Worth noting that safe_load is used (not load), so arbitrary object deserialisation is also off the table.


Breaking change — cpflow-review-app-help.yml guard removal

Removing if: vars.REVIEW_APP_PREFIX != '' (and the equivalent wrapper-level guard) means the help comment will post on every PR in every repo that has the generated cpflow-review-app-help.yml wrapper, regardless of whether review apps are configured. The CHANGELOG documents this, and the help text now includes:

"They require the repository to have cpflow review apps configured, including the CPLN_TOKEN_STAGING secret."

This is a reasonable trade-off — the old guard prevented repos that hadn't set REVIEW_APP_PREFIX from seeing any onboarding help at all, which made initial setup opaque. The updated approach of removing the wrapper being the opt-out mechanism is cleaner. Worth making clear in upgrade comms that the opt-out is to delete the wrapper file.


verify_release_ref_matches_checkout: network requirement on runners

The function unconditionally calls git ls-remote https://github.com/... when CPFLOW_VERSION is set. The error message documents this:

"Runners that set CPFLOW_VERSION need outbound HTTPS access to GitHub for this tag check."

One edge to be aware of: if the runner can reach GitHub but is under heavy load and git ls-remote times out, the error message will be the git transport error rather than the cpflow error. Consider wrapping with a timeout 15 git ls-remote ... to ensure a predictable failure mode in network-constrained environments.


Potential confusion: cpflow-delete-review-app.ymlif: guard inconsistency vs cleanup workflow

cpflow-delete-review-app.yml guards both resolve-review-config and setup-environment with if: steps.config.outputs.ready == 'true'. cpflow-cleanup-stale-review-apps.yml has no such guards on those same steps. This is actually correct because cleanup doesn't use pull_request_friendly: "true" (it runs on schedule/dispatch, never on fork PRs), so validate-config always runs in full and fails early if CPLN_TOKEN_STAGING is absent — the guards would be redundant but harmless if added for symmetry.


Minor notes

  • normalize_version returns 0 (success) with empty stdout for unrecognised version formats. This is intentional and documented, but it means callers must guard on empty output rather than non-zero exit, which is unconventional for bash. The code does guard correctly everywhere — just something to be aware of if the function is reused.
  • The YAML compat shim (YAML.method(:safe_load).parameters.any? { ... }) for Ruby 2.x vs 3.x is present in both cpflow-resolve-review-config and the promote workflow. If Ruby 2.x support is ever dropped, both copies should be simplified.
  • Spec coverage (especially passes control_plane_flow_ref to every setup action call) is excellent — the structural test over all checked-in reusable workflows will catch regressions mechanically.

Summary

The PR is in good shape. The security improvements are meaningful and correctly implemented, test coverage is solid, and the documentation is clear about the trade-offs. The git ls-remote timeout note above is the one actionable hardening suggestion. The breaking change is documented and the opt-out path is clear. Ready to approve pending consideration of the timeout edge case.

shift
fi

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The second bundle exec strip here (after stripping thrust) handles the pattern bundle exec thrust bundle exec rails server, where Thruster is invoked via bundle exec thrust and the Rails server is also invoked via bundle exec rails server. This is correct, but could use a short comment explaining the double-strip, since it looks like a copy-paste error at first glance:

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


[[ "${ref}" == v* ]] || return 1

if ! tag_refs="$(git ls-remote --tags "${remote_url}" "refs/tags/${ref}" "refs/tags/${ref}^{}")"; then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If git ls-remote hangs (e.g. a runner that can reach GitHub but times out slowly), the workflow job will wait until the git transport's own timeout fires rather than failing quickly. Consider bounding this:

Suggested change
if ! tag_refs="$(git ls-remote --tags "${remote_url}" "refs/tags/${ref}" "refs/tags/${ref}^{}")"; then
if ! tag_refs="$(timeout 15 git ls-remote --tags "${remote_url}" "refs/tags/${ref}" "refs/tags/${ref}^{}" 2>&1)"; then

This ensures a predictable ~15 s failure instead of a multi-minute stall when the runner has degraded outbound connectivity. The 2>&1 captures any timeout or git error text so it doesn't vanish silently.


validate_github_env_value!("REVIEW_APP_PREFIX", prefix)
validate_github_env_value!("CPLN_ORG", cpln_org)
validate_github_env_value!("APP_NAME", app_name) unless app_name.empty?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

When pr_number is empty, app_name is "" and the app_name output is never written to GITHUB_OUTPUT. Callers that reference ${{ steps.review-config.outputs.app_name }} will receive an empty string in that case — which is the correct behaviour for the cleanup workflow (which doesn't need an app_name).

This is fine as-is, but worth calling out explicitly in the output description so callers know to expect a missing-vs-empty distinction:

Suggested change
validate_github_env_value!("APP_NAME", app_name) unless app_name.empty?
validate_github_env_value!("APP_NAME", app_name) unless app_name.empty?
# Note: app_name output is absent (not empty string) when pr_number is not provided.
# Callers that need app_name must pass pr_number.

@claude

claude Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

Code Review — PR #318: Validate cpflow gem and workflow pins

Overview

This PR delivers three interconnected improvements:

  1. Version-pin guard — fails early when CPFLOW_VERSION doesn't match the control_plane_flow_ref release tag.
  2. Review-app config inference — derives prefix and org from .controlplane/controlplane.yml, reducing required GitHub repository variables to just CPLN_TOKEN_STAGING.
  3. Security hardening — replaces secrets: inherit with named secrets across all generated wrappers, and moves CPLN_TOKEN_PRODUCTION into a protected GitHub Environment.

Overall the approach is sound and the security improvements are significant. A few targeted notes below.


Security — Positive Changes

  • secrets: inherit → named secrets: Removing broad secret inheritance from all six generated workflow wrappers is a meaningful least-privilege improvement. The generated callers now declare exactly what they need.
  • CPLN_TOKEN_PRODUCTION scoped to protected environment: The promotion job correctly declares environment: ${{ inputs.production_environment }} so GitHub's approval gate must pass before the production token is injected. The early "Validate production token" step provides clear feedback when the secret is misconfigured.
  • CPFLOW_VERSION / control_plane_flow_ref integrity check: The verify_release_ref_matches_checkout guard prevents a moving branch named like a release tag from being combined with a pinned gem version.

Issues / Observations

1. Hardcoded remote URL in verify_release_ref_matches_checkout

local remote_url="https://github.com/shakacode/control-plane-flow.git"

(.github/actions/cpflow-setup-environment/action.yml ~line 126)

This URL is hardcoded, so any downstream fork of control-plane-flow would fail the tag-commit check regardless of what ref they use. The function also requires outbound HTTPS to GitHub which is documented in the error message, but it isn't obvious that CPFLOW_VERSION triggers a network call at all. A warning in the action input description like "setting this triggers a remote tag check" would help users on restricted runners know to leave it unset.

2. validate_github_env_value! blocks valid org names with underscores

return if value.match?(/\A[A-Za-z0-9-]+\z/)

(.github/actions/cpflow-resolve-review-config/action.yml line 61)

Control Plane org names and GVC names shouldn't have underscores by convention, but this regex would silently block any cpln_org value that happens to contain an underscore or dot (e.g., some enterprise org naming schemes). If validation intentionally enforces the Control Plane naming spec, that constraint should be documented in the error message — the current message says "valid Control Plane name" but doesn't state what characters Control Plane actually restricts.

3. is_rails_server_command misses options-before-verb form

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

(lib/generator_templates/entrypoint.sh lines 35–36)

rails -e production server or rails --environment=production server — where Rails flags precede the subcommand verb — won't be detected, and db:prepare will be silently skipped. In practice generated Dockerfiles use CMD ["rails", "s"] so this won't bite the default case, but apps that pass environment flags before the verb (e.g. in a CMD override) would get a quiet surprise. The comment on line 23 ("Hand-edited commands with env flags...skip generated DB prep") covers Thruster but not Rails-level flags. Worth extending the comment or adding a note in the CHANGELOG entry.

4. Breaking change scope for cpflow-review-app-help.yml

Removing the if: vars.REVIEW_APP_PREFIX != '' guard means any existing repository that already committed the generated cpflow-review-app-help.yml file will immediately start posting PR-open help comments — even before CPLN_TOKEN_STAGING is configured. The CHANGELOG documents this as a breaking change, but the in-file comment ("Remove this wrapper or add a repo-specific if: guard if review apps should not be advertised") understates the impact for teams that have already committed the old generated file and aren't actively watching the CHANGELOG.

Consider whether a generated migration note in the upgrade docs or a cpflow generate-github-actions dry-run warning would help here.

5. Minor: CPLN_TOKEN_PRODUCTION appears required: false but is semantically required

The workflow-call secret declaration:

CPLN_TOKEN_PRODUCTION:
  required: false

This is intentional (the caller must not pass it — GitHub injects it from the environment), but a reader scanning the workflow inputs might miss the nuance. The adjacent comment explains it well, but consider whether a more explicit note like # injected by GitHub from the 'production' environment; callers must not pass this in the secret declaration would catch future maintainers faster.


Test Coverage

The new specs in generate_github_actions_spec.rb are thorough — they verify control_plane_flow_ref propagation, secrets: inherit removal, production_environment presence, and the new validation shell functions by name/content. The cpflow-resolve-review-config action's YAML-parsing logic is tested by inspecting the action source, which is appropriate for a composite action.

One gap: there is no test exercising is_rails_server_command at the shell level (e.g., sh -c 'source entrypoint.sh; is_rails_server_command rails s'). The sh -n syntax check in the PR description validates parse correctness but not logic. Given the function's branching complexity, a few shell-level unit tests would give more confidence.


Summary

The security improvements (named secrets, protected-environment production token) are well-executed and the version-pin guard is a meaningful safety net. The main things to address before merge are: documenting the network-call side-effect of CPFLOW_VERSION, confirming the validate_github_env_value! regex matches Control Plane's documented naming rules, and extending the is_rails_server_command comment to cover the options-before-verb edge case.


verify_release_ref_matches_checkout() {
local ref="${1#refs/tags/}"
local remote_url="https://github.com/shakacode/control-plane-flow.git"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The remote URL is hardcoded to the canonical upstream repo. Any fork that sets CPFLOW_VERSION will fail this check because the fork's checked-out commit will never match a tag on shakacode/control-plane-flow.

This is probably acceptable (forks should leave CPFLOW_VERSION unset), but it's not obvious from the input description that setting CPFLOW_VERSION at all triggers an outbound HTTPS call and a cross-repo commit check. Consider adding a note to the cpflow_version input description: "Setting this value triggers a remote tag-integrity check against the upstream shakacode/control-plane-flow repository; runners without outbound HTTPS access to GitHub should leave this unset."

end

def validate_github_env_value!(name, value)
return if value.match?(/\A[A-Za-z0-9-]+\z/)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The allowed charset [A-Za-z0-9-] rejects underscores and dots. If a cpln_org value ever contains an underscore (e.g., some enterprise naming schemes), the workflow fails with the generic "must contain only letters, numbers, and hyphens" message — not a hint about what to fix.

Two suggestions:

  1. If Control Plane's documented naming restriction truly forbids underscores, say so in the error message: "Control Plane GVC and org names must match /^[a-z0-9][a-z0-9-]*[a-z0-9]$/" (or whatever the real rule is).
  2. If the restriction is only to make the value safe to write to a GitHub env file, a comment clarifying that would help future maintainers understand why dots are also excluded.

Comment on lines +35 to +36
{ [ "${1:-}" = "rails" ] || [ "${1:-}" = "bin/rails" ] || [ "${1:-}" = "./bin/rails" ]; } &&
{ [ "${2:-}" = "server" ] || [ "${2:-}" = "s" ]; }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This detection works for rails s, rails server, ./bin/rails s, etc., but misses the case where Rails-level options precede the subcommand verb, e.g.:

rails -e production server
rails --environment=production server

In both cases $2 is -e / --environment=production, not server, so db:prepare is silently skipped.

This is unlikely for generated CMD entries (which always use rails s or ./bin/rails s), but it's a sharp edge for hand-edited commands. The existing comment on line 23 ("Hand-edited commands with env flags or Thruster flags before rails skip generated DB prep") should be extended to mention Rails-native flags before the verb as well, so developers know this is a known limitation rather than a bug to report.

case "${1}" in
*=*) shift ;;
--) shift; break ;;
-*) return 1 ;;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The -*) return 1 case silently skips DB prep for any command prefixed with env -<flag> … (e.g. env -i rails s or env -u VAR rails s). The comment above mentions this intentional behaviour for "hand-edited commands with env flags", but it's worth being explicit in user-facing docs that only flag-free env invocations receive DB prep, since env -i is commonly used to strip the inherited environment before running a service.

Suggested change
-*) return 1 ;;
-*) return 1 ;;

No code change needed — just flagging for docs/changelog awareness.

exit 1
end

begin

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[A-Za-z0-9-]+ deliberately excludes underscores. That's correct for Control Plane GVC/org names (DNS-label rules allow only letters, digits, and hyphens), but if a downstream project's cpln_org or app prefix ever contains an underscore the action will fail here with "must contain only letters, numbers, and hyphens" rather than a more specific message about the offending character.

A small improvement would be to surface the actual value in the error so the user immediately knows what to fix:

Suggested change
begin
warn "::error::#{name} ('#{value}') must contain only letters, numbers, and hyphens so it is a valid Control Plane name and can be safely written to GitHub environment files."


npm_global_prefix="${HOME}/.npm-global"
mkdir -p "${npm_global_prefix}"
echo "${npm_global_prefix}/bin" >> "$GITHUB_PATH"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

When git ls-remote fails (network unreachable, DNS error, etc.) the raw git stderr is interpolated directly into the ::error:: message via ${tag_refs}. This is fine for debugging, but ${tag_refs} is also used in the [[ -z "${tag_refs}" ]] check below — if the command fails with exit code 0 (unusual but possible for some timeout implementations), the error branch is never taken and the empty-string branch fires instead, producing a less informative message.

Minor: consider splitting stdout and stderr into separate variables so the emptiness check is unambiguous:

if ! tag_refs="$(git_ls_remote_tag 2>/tmp/git_ls_err)"; then
  git_err="$(cat /tmp/git_ls_err)"
  echo "::error::Could not verify ... Details: ${git_err}"
  exit 1
fi

Not blocking — the 2>&1 approach works correctly in practice since a failed git command always exits non-zero.

set -euo pipefail

if [[ -z "${CPLN_TOKEN_PRODUCTION}" ]]; then
echo "::error::CPLN_TOKEN_PRODUCTION is not set. Add it as a secret on the '${PRODUCTION_ENVIRONMENT}' GitHub Environment."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Good fail-fast guard. One edge case worth documenting: this check verifies the token is non-empty, but it cannot distinguish whether it arrived from the protected production Environment or was passed from a caller's named secrets: block. A hand-edited caller that passes CPLN_TOKEN_PRODUCTION explicitly (the generated test-cpflow-github-flow script rejects that, but hand-edited callers are not checked) would satisfy this guard without going through the Environment approval gate.

Consider adding a note in the docs / inline comment that passing CPLN_TOKEN_PRODUCTION as a named caller secret defeats the protection — the generator guard in test-cpflow-github-flow is the main enforcement mechanism for generated callers.

@claude

claude Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

Code Review

Overview

This PR tightens the contract between the cpflow gem version and the reusable workflow/action code by adding early version-pin validation, replaces secrets: inherit with explicit named secrets across all generated caller templates, and introduces a shared cpflow-resolve-review-config composite action to eliminate duplicated YAML-parsing logic. It also fixes the entrypoint to run DB prep only for Rails server commands and adds set -e fail-fast behaviour.


Security — net positive

  • Removing secrets: inherit is the most impactful change. Generated callers now pass only the secrets each upstream workflow explicitly needs, preventing accidental propagation of unrelated repository secrets (e.g. production tokens, deploy keys) into review-app or staging jobs.
  • Production token isolation via GitHub Environment is well-architected. The reusable promotion workflow declares environment: ${{ inputs.production_environment }} so GitHub only injects CPLN_TOKEN_PRODUCTION after required reviewers approve. The generated caller correctly passes only CPLN_TOKEN_STAGING, and test-cpflow-github-flow now aborts if it detects CPLN_TOKEN_PRODUCTION in a caller's secrets: block. The one gap (noted inline) is that this check only applies to generated callers; a hand-edited wrapper could bypass the gate.
  • validate_github_env_value! before GITHUB_ENV writes correctly prevents newline injection into the environment file.
  • secrets: inherit guard in test-cpflow-github-flow is a good regression barrier for future template changes.

Version-pin validation (cpflow-setup-environment)

The validate_cpflow_version_pin / verify_release_ref_matches_checkout logic is sound:

  • normalize_version strips leading v and converts all - to . via ${version//-/.} (double-slash replaces all), correctly aligning v5.0.0-rc.1 (tag) with 5.0.0.rc.1 (gem version).
  • Annotated vs. lightweight tag handling in the ls-remote loop is correct — the ^{} peeled ref takes priority for annotated tags; the base ref is used as fallback for lightweight tags.
  • The outbound git ls-remote call (with 20-second timeout when available) is the right approach and the error message already calls out the air-gapped-runner limitation.
  • One minor robustness note left inline about mixing stdout+stderr in the same variable.

cpflow-resolve-review-config composite action

Well-implemented. The Ruby-in-bash heredoc pattern is consistent with other actions in the repo. Error handling covers the expected failure modes (missing YAML, wrong types, multiple review-app entries, non-integer PR number). Outputs are validated before being written to GITHUB_ENV and GITHUB_OUTPUT.

Minor suggestion left inline: include the offending value in the validation error message so users can immediately see what to fix without re-reading their config.


Entrypoint is_rails_server_command

The logic correctly handles the main generated patterns:

  • env VAR=x [--] rails s
  • bundle exec rails s
  • [bundle exec] thrust [bundle exec] rails s
  • All three rails / bin/rails / ./bin/rails spellings

One behavioural note left inline: any env -<flag> (e.g. env -i rails s or env -u VAR rails s) causes the function to return non-zero, silently skipping DB prep. The comment mentions this for "hand-edited commands", but it may surprise users who wrap their server command with env -i to clear the inherited environment.


Behaviour change: unconditional PR-open help

Removing if: vars.REVIEW_APP_PREFIX != '' from the reusable workflow means all repos that commit the generated cpflow-review-app-help.yml wrapper will post a comment on every new PR, even before Control Plane is fully configured. The changelog entry and template comment both document this — but it's a breaking change for any repo that was relying on the REVIEW_APP_PREFIX guard as a "not yet configured" signal. The suggested mitigation (remove the wrapper or add a repo-level if:) is reasonable.


Tests

New spec coverage for:

  • control_plane_flow_ref propagation through all setup action calls
  • secrets: inherit rejection
  • production_environment requirement and CPLN_TOKEN_PRODUCTION caller-passing rejection
  • cpflow-resolve-review-config YAML parsing, validation, and output writing

The integration-style tests that load actual workflow YAML and assert structural properties are a good pattern and will catch future regressions.


Minor nits (non-blocking)

  • CHANGELOG.md records [5.0.1] - 2026-05-24 but the tag doesn't exist yet; that's expected pre-release practice, just worth double-checking the date before the actual release cut.
  • The normalize_version function silently returns empty output for unrecognised formats (no echo in the no-match branch). Callers check for empty output, so this is correct — just an unusual function signature to be aware of.

Summary

The PR is well-structured and the security improvements are meaningful. The four inline comments are minor suggestions rather than blockers. Approve once the team agrees on the env -i DB-prep skip behaviour and the unconditional PR-help comment trade-off.

@justin808 justin808 merged commit abaf019 into main May 25, 2026
16 checks passed
@justin808 justin808 deleted the jg-codex/validate-cpflow-version-pin branch May 25, 2026 03:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant