Skip to content

Validate selected setup templates and review-app health#363

Open
justin808 wants to merge 3 commits into
mainfrom
jg-codex/selected-template-validation
Open

Validate selected setup templates and review-app health#363
justin808 wants to merge 3 commits into
mainfrom
jg-codex/selected-template-validation

Conversation

@justin808

@justin808 justin808 commented Jun 4, 2026

Copy link
Copy Markdown
Member

Summary

  • validate only the templates selected by setup_app_templates for doctor/setup-app
  • keep the existing all-template fallback when no setup_app_templates are configured
  • preserve apply-template's existing missing-template handling for explicit args
  • avoid fetching latest image data when templates do not use image placeholders
  • make review-app deploys wait for workload readiness and an accepted HTTP status before marking GitHub deployment success

Why

TanStack review apps hit cpflow 5.1.1 duplicate-template validation because the app intentionally has app.yml and app-review.yml that both render the same GVC name, but only one is selected for a review app setup. The validator was parsing every template file instead of the selected set.

While validating the rollout, the legacy review app also exposed a deploy-status gap: the reusable review-app deploy workflow could mark a deployment successful after updating the image and finding an endpoint, even if the primary workload was crash-looping. Review-app deploys now reuse the existing cpflow-wait-for-health action, with REVIEW_APP_HEALTH_CHECK_* repo-variable overrides.

Verification

  • CPLN_ORG=dummy-test-org bundle exec rspec spec/command/doctor_spec.rb spec/core/template_parser_spec.rb spec/command/apply_template_spec.rb:8
  • bundle exec rubocop lib/core/doctor_service.rb lib/core/template_parser.rb spec/command/doctor_spec.rb spec/core/template_parser_spec.rb spec/command/apply_template_spec.rb
  • ruby -c lib/core/doctor_service.rb && ruby -c lib/core/template_parser.rb
  • CPLN_ORG=dummy-test-org bundle exec rspec spec/command/generate_github_actions_spec.rb spec/command/deploy_image_unit_spec.rb
  • bundle exec rubocop spec/command/generate_github_actions_spec.rb
  • YAML parse for .github/workflows/cpflow-*.yml and .github/actions/cpflow-*/*.yml
  • git diff --check
  • autoreview clean after addressing the accepted health-policy finding

Notes

  • actionlint .github/workflows/cpflow-deploy-review-app.yml still reports pre-existing false positives for GitHub's job.workflow_repository, job.workflow_sha, and job.workflow_ref context fields; it did not report issues for the new health-check expressions.
  • A broader setup_app/apply_template spec run was not used as merge evidence here because this sandbox lacks working Docker and unrestricted Control Plane API access; failures there were environment/network related, not from the focused selected-template change.

Summary by CodeRabbit

  • New Features

    • Added support for new template variables (APP_IDENTITY, APP_IDENTITY_LINK, APP_SECRETS, APP_SECRETS_POLICY).
    • Enhanced template selection to respect CLI and app configuration choices.
  • Bug Fixes

    • Clearer "Missing templates" errors listing missing items.
    • Improved image-variable handling with backward-compatibility for legacy tokens.
  • CI

    • Review-app deploy workflow now waits for deployment health before proceeding.
  • Tests

    • Updated and added specs covering template selection, missing templates, duplicates, and image behavior.

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR makes template parsing selective (CLI args or configured setup_app_templates, with missing-template validation) and extends template variable replacement (identity/secrets/policy, refactored image handling). It also adds review-app health-check env/defaults, a workflow wait-for-health step, corresponding generator spec, docs, and test fixtures.

Changes

Template selection and variable substitution

Layer / File(s) Summary
Template selection and validation in DoctorService
lib/core/doctor_service.rb
validate_templates now uses a template_filenames helper to choose templates from CLI args (only if all referenced files exist) or config.current[:setup_app_templates], falling back to glob-all. Missing configured templates raise ValidationError with a “Missing templates” listing.
Variable substitution enhancements in TemplateParser
lib/core/template_parser.rb
replace_variables now substitutes {{APP_IDENTITY}}, {{APP_IDENTITY_LINK}}, {{APP_SECRETS}}, and {{APP_SECRETS_POLICY}}. Image handling is refactored: replace_image_variables covers {{APP_IMAGE}}/{{APP_IMAGE_LINK}} and replace_legacy_image_variable handles legacy APP_IMAGE via cp.latest_image.
Template parser behavior verification
spec/core/template_parser_spec.rb
New test asserts Controlplane#latest_image is not called when parsed templates lack image placeholders.
Test fixtures and dummy app configurations
spec/dummy/.controlplane/controlplane.yml, spec/dummy/.controlplane/templates/app-review.yml, spec/dummy/.controlplane/templates/app-with-deprecated-variables-no-image.yml
Adds dummy apps and templates exercising duplicate, alternate, deprecated, and missing-template scenarios; new templates define gvc and identity resources with templated fields.
Doctor command template validation tests
spec/command/doctor_spec.rb
Updates tests to use dummy_test_app("default"), adds assertions that missing selected templates fail with “Missing templates”, allows duplicates from unselected templates, and adjusts deprecated-variable expectations so APP_IMAGE is not reported as deprecated.

Review-app workflow health checks and docs

Layer / File(s) Summary
Workflow env defaults and wait-for-health step
.github/workflows/cpflow-deploy-review-app.yml
Adds default REVIEW_APP_HEALTH_CHECK_* env vars and a cpflow-wait-for-health step wired to the primary workload and health-check settings (retries, interval, accepted statuses, curl timeout).
Docs and generator spec updates
docs/ci-automation.md, docs/commands.md, spec/command/generate_github_actions_spec.rb
Documents the new review-app health-check variables and behavior; updates example release-tag; adds a spec verifying the generated reusable workflow includes the wait-for-health step with expected inputs.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

A rabbit hops through code with care,
Selecting templates with judicious flair,
Identity and secrets tucked in a row,
Legacy images bowed out with a bow,
Tests nibble bugs — the CI fields glow! 🐰

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly and concisely summarizes the two main changes: validating selected setup templates and adding review-app health checks. It directly reflects the primary objectives without being vague or overly broad.

✏️ 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/selected-template-validation

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.

@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: 5c0e1617b4

ℹ️ 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".

def template_filenames
return existing_arg_template_filenames if config.args.any?

template_names = config.current[:setup_app_templates]

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 Handle unmatched apps before reading setup templates

When templates validation runs without explicit template args (for example cpflow doctor --validations templates -a <unknown-app> or the preflight validation in setup-app) and the app name does not match any entry in controlplane.yml, config.current is nil; indexing it here raises a NoMethodError that is not rescued by run_validations or the command wrapper, so the CLI crashes instead of reporting the existing “Can't find current config” style error. Guard the nil case or go through config[:setup_app_templates] so unknown apps fail cleanly.

Useful? React with 👍 / 👎.

@claude

claude Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Code Review

Overview

This PR fixes a real bug: the duplicate-template validator was scanning every *.yml in the templates directory instead of only the templates that would actually be applied. The fix limits validation to the selected set (setup_app_templates, explicit CLI args, or the full glob as fallback), and adds lazy cp.latest_image evaluation so image API calls are skipped when no template uses image placeholders.

The change is well-scoped and the core logic is correct. A few findings below.


Issues

1. existing_arg_template_filenames silently eats missing-template errors

When config.args names a template that does not exist on disk, the method returns [] and the template validation becomes a no-op — no [FAIL], no error message, the command just moves on. The intent (let apply-template report missing templates itself) is correct, but it is invisible to a reader. A comment explaining the deliberate skip would prevent future confusion; see inline comment.

2. replace_legacy_image_variable check is a substring of the modern placeholder

yaml_file.include?("APP_IMAGE") also matches {{APP_IMAGE}} and {{APP_IMAGE_LINK}} because APP_IMAGE is a substring of both. The current call order — replace_image_variables always runs first and removes those modern forms before replace_legacy_image_variable is reached — makes this safe today. But correctness now depends silently on call order. If the order ever changes, a template with only {{APP_IMAGE_LINK}} would cause cp.latest_image to be called a second time and APP_IMAGE_LINK would be mangled into {latest_image}_LINK. See inline comment for a more defensive guard.

3. Test coverage gap: deprecated APP_IMAGE variable warning

The warns about deprecated variables spec was updated to use app-with-deprecated-variables-no-image.yml, which intentionally omits the legacy APP_IMAGE placeholder, and the assertion was flipped from to includenot_to include for that variable. This means no test currently exercises the code path that detects and reports the APP_IMAGE → {{APP_IMAGE}} deprecation. Adding a dedicated spec (or restoring coverage via a separate template fixture that contains APP_IMAGE but no network-requiring image calls) would prevent a silent regression in that branch.


Minor / Positive Notes

  • The lazy-image optimization in replace_image_variables / replace_legacy_image_variable is clean and the new template_parser_spec test pins the behaviour well.
  • Replacing Dir.glob-stubbing with dedicated dummy apps makes the doctor specs significantly more realistic and readable.
  • ensure_templates_exist! raises a properly-formatted ValidationError with per-template file paths — consistent with the rest of the service.


def existing_arg_template_filenames
filenames = config.args.map { |name| @template_parser.template_filename(name) }
return [] unless filenames.all? { |filename| File.exist?(filename) }

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 silent [] return when any template is missing is intentional (the calling command — e.g. apply-template — handles missing-template errors itself and produces a better message). But nothing in the code communicates that intent, making this look like a bug to future readers. A short comment would help:

Suggested change
return [] unless filenames.all? { |filename| File.exist?(filename) }
# Return [] so that apply-template can report its own "Missing templates" error;
# don't raise a ValidationError here.
return [] unless filenames.all? { |filename| File.exist?(filename) }

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

🧹 Nitpick comments (1)
spec/command/doctor_spec.rb (1)

36-77: ⚡ Quick win

Add explicit regression coverage for fallback-to-all template validation.

The updated tests now focus on selected-template behavior, but there’s no direct example asserting the fallback path when setup_app_templates is unset. Since that fallback is a stated objective, adding one case here would guard against regressions.

Proposed spec addition
   it "passes if selected templates have no issues" do
     result = run_cpflow_command("doctor", "--validations", "templates", "-a", app)

     expect(result[:status]).to eq(0)
     expect(result[:stderr]).to include("[PASS] templates")
     expect(result[:stderr]).not_to include("DEPRECATED")
   end
+
+  it "falls back to validating all templates when setup_app_templates is not configured" do
+    app = dummy_test_app("nothing")
+
+    result = run_cpflow_command("doctor", "--validations", "templates", "-a", app)
+
+    expect(result[:status]).not_to eq(0)
+    expect(result[:stderr]).to include("[FAIL] templates")
+    expect(result[:stderr]).to include("- kind: gvc, name: #{app}")
+  end
🤖 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 `@spec/command/doctor_spec.rb` around lines 36 - 77, Add a regression spec that
verifies the fallback-to-all template validation when setup_app_templates is
unset: create a new example in the doctor_spec that uses
run_cpflow_command("doctor", "--validations", "templates", "-a", app) (or
without -a to emulate unset setup_app_templates) against a dummy_test_app that
relies on the fallback (e.g., "default" or a new fixture) and assert the command
exits with success or failure as expected and that stderr contains the fallback
behavior markers (e.g., "[PASS] templates" or expected failure messages);
specifically reference setup_app_templates being nil/unset and ensure the test
checks that templates validation runs over all templates rather than only
selected ones to guard the fallback path.
🤖 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.

Nitpick comments:
In `@spec/command/doctor_spec.rb`:
- Around line 36-77: Add a regression spec that verifies the fallback-to-all
template validation when setup_app_templates is unset: create a new example in
the doctor_spec that uses run_cpflow_command("doctor", "--validations",
"templates", "-a", app) (or without -a to emulate unset setup_app_templates)
against a dummy_test_app that relies on the fallback (e.g., "default" or a new
fixture) and assert the command exits with success or failure as expected and
that stderr contains the fallback behavior markers (e.g., "[PASS] templates" or
expected failure messages); specifically reference setup_app_templates being
nil/unset and ensure the test checks that templates validation runs over all
templates rather than only selected ones to guard the fallback path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ef8ae8ec-721e-4119-8bac-9b1f8d39bf8e

📥 Commits

Reviewing files that changed from the base of the PR and between 610dd36 and 5c0e161.

📒 Files selected for processing (7)
  • lib/core/doctor_service.rb
  • lib/core/template_parser.rb
  • spec/command/doctor_spec.rb
  • spec/core/template_parser_spec.rb
  • spec/dummy/.controlplane/controlplane.yml
  • spec/dummy/.controlplane/templates/app-review.yml
  • spec/dummy/.controlplane/templates/app-with-deprecated-variables-no-image.yml

end

def replace_legacy_image_variable(yaml_file)
return yaml_file unless yaml_file.include?("APP_IMAGE")

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.

"APP_IMAGE" is a substring of both "{{APP_IMAGE}}" and "{{APP_IMAGE_LINK}}". The current call order (modern vars replaced first in replace_image_variables) keeps this safe, but the guard is fragile — if ordering ever shifts, a template containing only {{APP_IMAGE_LINK}} would pass this check and gsub("APP_IMAGE", ...) would corrupt APP_IMAGE_LINK into {latest_image}_LINK.

A more defensive check:

Suggested change
return yaml_file unless yaml_file.include?("APP_IMAGE")
return yaml_file unless yaml_file.include?("APP_IMAGE") && !yaml_file.include?("{{APP_IMAGE")

Or simply invert the responsibility: since replace_image_variables already detects the modern patterns, the legacy check only needs to fire when the modern patterns were not present — which could be expressed by checking for APP_IMAGE not surrounded by {{.

expect(result[:stderr]).to include("APP_GVC -> {{APP_NAME}}")
expect(result[:stderr]).to include("APP_LOCATION -> {{APP_LOCATION}}")
expect(result[:stderr]).to include("APP_IMAGE -> {{APP_IMAGE}}")
expect(result[:stderr]).not_to include("APP_IMAGE -> {{APP_IMAGE}}")

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.

Flipping this from to include to not_to include is correct for the new app-with-deprecated-variables-no-image.yml fixture, but it removes the only test that verified the APP_IMAGE → {{APP_IMAGE}} deprecation warning fires at all. Consider either:

  • Adding a dedicated it "warns about deprecated APP_IMAGE variable" spec backed by a fixture that contains the legacy APP_IMAGE placeholder (the existing app-with-deprecated-variables.yml already has it, but that file triggers cp.latest_image — a small targeted stub in that one test would be enough), or
  • Verifying coverage via the template_parser_spec find_deprecated_variables path directly.

Without this, a regression that silently drops the APP_IMAGE deprecation path would go undetected.

@greptile-apps

greptile-apps Bot commented Jun 4, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes a false-positive duplicate-template validation error triggered when an app's setup_app_templates only selects a subset of all template files in the directory. Doctor and setup-app now validate only the configured templates instead of globbing every .yml file. A secondary optimization lazily fetches cp.latest_image only when a template actually contains an image placeholder.

  • DoctorService#template_filenames now resolves the active set from setup_app_templates (with a full-glob fallback for apps without any configured templates) and raises a ValidationError immediately if a configured template file is missing.
  • TemplateParser#replace_variables was refactored into replace_image_variables / replace_legacy_image_variable helpers that guard on include? before calling cp.latest_image, avoiding a network/API call for templates that carry no image references.
  • Test suite converted from Dir.glob stubs to dedicated app configs (duplicate-templates, alternate-app-template, deprecated-template, missing-template) with matching template fixtures, making each scenario self-contained and runnable against the real code path.

Confidence Score: 4/5

Safe to merge; the core validation logic is correct and the two-phase missing-template flow for apply-template is intentional.

The selected-template filtering, the lazy image fetch, and the ensure_templates_exist! error path all behave correctly. The only note is that existing_arg_template_filenames silently returns [] when files are absent — by design for the apply-template pre-flight — but this implicit contract is undocumented and could surprise future contributors adding a new command with positional-arg templates.

lib/core/doctor_service.rb — the existing_arg_template_filenames contract deserves a comment; all other files are straightforward.

Important Files Changed

Filename Overview
lib/core/doctor_service.rb Adds template_filenames, existing_arg_template_filenames, and ensure_templates_exist! to scope validation to selected templates; logic is correct and intentional for apply-template's two-phase missing-template handling.
lib/core/template_parser.rb Extracts image-variable replacement into guarded helpers to skip cp.latest_image calls when unneeded; refactored behavior is functionally equivalent to the original unconditional replacements.
spec/command/doctor_spec.rb Replaces fragile Dir.glob stubs with dedicated app configs; four new scenarios cover duplicates, missing templates, alternate templates, and deprecated-variable detection without image.
spec/dummy/.controlplane/controlplane.yml Adds four new named app configurations that back the new doctor spec scenarios; all use match_if_app_name_starts_with: true and *common alias consistently with existing entries.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[DoctorService#validate_templates] --> B[template_filenames]
    B --> C{config.args.any?}
    C -- Yes\napply-template path --> D[existing_arg_template_filenames]
    D --> E{All files exist?}
    E -- No --> F[return empty array\nlet apply-template handle error]
    E -- Yes --> G[return filenames]
    C -- No\ndoctor / setup-app path --> H{setup_app_templates\nconfigured?}
    H -- No --> I[Dir.glob all *.yml\nfull fallback]
    H -- Yes --> J[map names to filenames]
    J --> K[ensure_templates_exist!]
    K --> L{Any missing?}
    L -- Yes --> M[raise ValidationError\nMissing templates]
    L -- No --> G
    G --> N[TemplateParser#parse filenames]
    I --> N
    F --> N
    N --> O[replace_variables per file]
    O --> P{includes APP_IMAGE\nor APP_IMAGE_LINK?}
    P -- Yes --> Q[cp.latest_image\nreplace image vars]
    P -- No --> R[skip image fetch]
    Q --> S[check_for_duplicate_templates]
    R --> S
    S --> T{Duplicates?}
    T -- Yes --> U[raise ValidationError\nDuplicate templates]
    T -- No --> V[warn_deprecated_template_variables]
Loading

Reviews (1): Last reviewed commit: "Validate selected setup templates" | Re-trigger Greptile

Comment on lines +101 to +106
def existing_arg_template_filenames
filenames = config.args.map { |name| @template_parser.template_filename(name) }
return [] unless filenames.all? { |filename| File.exist?(filename) }

filenames
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 Silent pass when any arg-specified template is missing

When called in the apply-template pre-flight path, this returns [] if even one template file is absent, causing validate_templates to run duplicate/deprecation checks against zero templates and silently succeed. That's intentional because ApplyTemplate#ensure_templates! handles the error itself. However, if any future command gains positional-arg template names (REQUIRES_ARGS = true) and also uses VALIDATIONS = %w[templates] without its own missing-template guard, the validation will silently pass and the error will be swallowed. A brief comment documenting that this deliberately delegates to the caller's own error handling would prevent that footgun.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +101 to +106
def existing_arg_template_filenames
filenames = config.args.map { |name| @template_parser.template_filename(name) }
return [] unless filenames.all? { |filename| File.exist?(filename) }

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

Bug: silent pass when CLI-arg templates are missing

When any template file doesn't exist, this returns [] instead of raising a ValidationError. parse([]) then produces zero templates, check_for_duplicate_templates([]) trivially passes, and the user sees [PASS] templates — with no indication that their template arguments were never validated.

This is inconsistent with ensure_templates_exist! (used for setup_app_templates) and with apply-template's ensure_templates!, both of which raise an error on missing files.

Suggested change
def existing_arg_template_filenames
filenames = config.args.map { |name| @template_parser.template_filename(name) }
return [] unless filenames.all? { |filename| File.exist?(filename) }
filenames
end
def existing_arg_template_filenames
filenames = config.args.map { |name| @template_parser.template_filename(name) }
ensure_templates_exist!(config.args, filenames)
filenames
end

expect(result[:stderr]).to include("APP_GVC -> {{APP_NAME}}")
expect(result[:stderr]).to include("APP_LOCATION -> {{APP_LOCATION}}")
expect(result[:stderr]).to include("APP_IMAGE -> {{APP_IMAGE}}")
expect(result[:stderr]).not_to include("APP_IMAGE -> {{APP_IMAGE}}")

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.

Test coverage gap: APP_IMAGE deprecation warning

The old test verified that APP_IMAGE -> {{APP_IMAGE}} is reported as deprecated (using the app-with-deprecated-variables fixture which contained bare APP_IMAGE). Switching to the -no-image fixture and asserting not_to include removes the only test for that deprecation path.

new_variables in TemplateParser still maps "APP_IMAGE" => "{{APP_IMAGE}}", so the deprecation is still detected — it just goes untested. Consider keeping a separate test (or a second fixture) that covers a template with a bare APP_IMAGE to confirm the warning still fires when expected.

end

def replace_legacy_image_variable(yaml_file)
return yaml_file unless yaml_file.include?("APP_IMAGE")

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.

Guard check is correct but worth a note on ordering

yaml_file here has already had {{APP_IMAGE}} replaced by replace_image_variables, so any remaining "APP_IMAGE" substring is genuinely the bare legacy form. The guard is safe.

One subtle point: "APP_IMAGE" is a substring of "{{APP_IMAGE}}", so if replace_image_variables were ever skipped (e.g., the include? guard there was wrong), this guard could false-positive on a {{APP_IMAGE}} literal. The current design prevents that because both methods are called in strict order, but it's worth keeping in mind if the chain is ever refactored.

@claude

claude Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Code Review

Overview

This PR scopes template validation in doctor/setup-app to only the templates selected by setup_app_templates, avoiding false-positive duplicate-kind/name errors when a repo intentionally has alternate template files (e.g. app.yml vs app-review.yml). It also lazily defers cp.latest_image calls until a template actually references an image placeholder. The motivation and approach are sound.


Bug: existing_arg_template_filenames silently passes when files are missing

See inline comment on lib/core/doctor_service.rb:101–106.

When explicit CLI template args are passed to doctor and any of those files don't exist, the method returns []. parse([]) produces zero templates, all checks trivially pass, and the user sees [PASS] templates — without any indication that their template arguments were ignored. The fix is to call ensure_templates_exist! here exactly like the setup_app_templates branch does.

This code path also has no spec coverage (all the new tests drive doctor without positional template args, so config.args is always empty). A test like the apply_template_spec missing-template case would close that gap.


Test coverage gap: APP_IMAGE deprecation warning

See inline comment on spec/command/doctor_spec.rb:106.

The old test verified that APP_IMAGE -> {{APP_IMAGE}} is reported as deprecated. The new test switches to a no-image fixture and asserts it is not reported. The deprecation detection still exists in new_variables, but there is no longer a test that confirms it fires when a bare APP_IMAGE is present.


Everything else looks good

  • The lazy image fetch in replace_image_variables / replace_legacy_image_variable is correctly ordered relative to the existing find_deprecated_variables call — no false guards are possible.
  • New fixtures (app-review.yml, app-with-deprecated-variables-no-image.yml) and controlplane.yml app entries are well-structured and cover the intended scenarios.
  • The ensure_templates_exist! error format (name + path) is clearer than before.
  • Docs bump to v5.1.1 is appropriate.

@justin808 justin808 changed the title Validate selected setup templates Validate selected setup templates and review-app health Jun 4, 2026

def existing_arg_template_filenames
filenames = config.args.map { |name| @template_parser.template_filename(name) }
return [] unless filenames.all? { |filename| File.exist?(filename) }

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 empty return masks missing-template errors for explicit args

When doctor --validations templates -a myapp some-missing-template is invoked, this silently returns [], so check_for_duplicate_templates([]) and warn_deprecated_template_variables both no-op and [PASS] templates is printed. The user gets no indication that the specified template was not found.

This asymmetry — setup_app_templates raises ValidationError via ensure_templates_exist!, but explicit CLI args silently skip — could be surprising. If the intent is to preserve apply-template's upstream behavior (where the command itself handles missing args), a short comment would help future readers understand the deliberate choice.

Suggested change
return [] unless filenames.all? { |filename| File.exist?(filename) }
# apply-template handles missing explicit args itself; return empty so doctor
# skips validation rather than double-erroring.
return [] unless filenames.all? { |filename| File.exist?(filename) }

def replace_legacy_image_variable(yaml_file)
return yaml_file unless yaml_file.include?("APP_IMAGE")

yaml_file.gsub("APP_IMAGE", cp.latest_image)

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.

Two calls to cp.latest_image are possible

replace_image_variables fetches latest_image at line 68. If the same template file also contains the legacy bare APP_IMAGE token, replace_legacy_image_variable calls cp.latest_image again — two API calls for what should be one value. In theory they could even return different images if a new image is pushed between the two calls.

Consider caching the result at the replace_variables call-site and threading it through:

def replace_legacy_image_variable(yaml_file, latest_image = nil)
  return yaml_file unless yaml_file.include?("APP_IMAGE")

  latest_image ||= cp.latest_image
  yaml_file.gsub("APP_IMAGE", latest_image)
end

Or resolve once in replace_variables and pass it down to both helpers.

curl_max_time: ${{ env.REVIEW_APP_HEALTH_CHECK_CURL_MAX_TIME }}

- name: Retrieve app URL
if: steps.config.outputs.ready == 'true' && steps.source.outputs.allowed == 'true' && (steps.check-app.outputs.exists == 'true' || steps.setup-review-app.outcome == 'success')

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.

"Retrieve app URL" doesn't guard on health-check success

The if: condition here doesn't include steps.health-check.outputs.healthy == 'true'. This is safe today because the health-check step exits non-zero on failure, causing GitHub Actions to skip subsequent steps. However, if continue-on-error: true were ever added to the health-check step, this step would run after an unhealthy deploy and the finalize step would still mark the deployment as success (since JOB_STATUS would be "success").

Consider adding an explicit guard for resilience:

Suggested change
if: steps.config.outputs.ready == 'true' && steps.source.outputs.allowed == 'true' && (steps.check-app.outputs.exists == 'true' || steps.setup-review-app.outcome == 'success')
if: steps.config.outputs.ready == 'true' && steps.source.outputs.allowed == 'true' && (steps.check-app.outputs.exists == 'true' || steps.setup-review-app.outcome == 'success') && steps.health-check.outputs.healthy == 'true'

@claude

claude Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Code Review

Overview

This PR fixes a real-world bug (TanStack review apps failing cpflow 5.1.1 duplicate-template validation) and closes a CI gap (review-app deploys marking success before the workload was actually healthy). The two changes are independent and well-scoped.


What's Good

  • Targeted fix for the duplicate-validation bug. Scoping validation to setup_app_templates rather than all templates on disk is the right approach. The fallback to all-templates when setup_app_templates is unset preserves backward compatibility cleanly.
  • Lazy image fetching. The new guards in replace_image_variables and replace_legacy_image_variable avoid an unnecessary cp.latest_image API call for templates that don't reference the image. This is a good perf improvement.
  • Health check wiring. Re-using the existing cpflow-wait-for-health composite action is the right call. The new REVIEW_APP_HEALTH_CHECK_* env-var knobs allow teams to tune review-app checks independently from production promotion.
  • Test fixtures are realistic. Using distinct dummy app entries (duplicate-templates, missing-template, alternate-app-template, etc.) is cleaner than over-using stub_template_filenames and makes the fixture intent self-documenting.

Issues

Three inline comments were posted; summaries below:

  1. existing_arg_template_filenames silently returns [] (lib/core/doctor_service.rb:103): When doctor is invoked with explicit template args pointing to missing files, validation silently passes rather than reporting the missing files. This is asymmetric with the setup_app_templates path (which raises ValidationError). If the silent behavior is intentional (to preserve apply-template's own error handling), a one-line comment would prevent future confusion.

  2. Double cp.latest_image call possible (lib/core/template_parser.rb:77): A template containing both the new-style {{APP_IMAGE}} token and the legacy bare APP_IMAGE token would trigger two separate cp.latest_image calls — once in replace_image_variables and again in replace_legacy_image_variable. In a race, these could return different images. Low probability in practice, but worth caching the result once per file.

  3. "Retrieve app URL" step doesn't guard on health-check outcome (.github/workflows/cpflow-deploy-review-app.yml:462): Safe today because a failing health-check step causes GitHub Actions to skip subsequent steps by default. But if continue-on-error: true were ever added to the health-check step, the deploy would incorrectly be finalized as successful. An explicit && steps.health-check.outputs.healthy == 'true' guard would make the intent clear and future-proof the logic.


Minor Notes

  • Default values for the health-check knobs are defined in three places (workflow env:, action.yml defaults, and docs). This is an acceptable tradeoff for self-documenting workflows, just worth being aware of when changing defaults.
  • The deprecated-variable test now correctly asserts that APP_IMAGE does not trigger a deprecation warning for the app-with-deprecated-variables-no-image fixture — consistent with the lazy-fetch change meaning APP_IMAGE is no longer eagerly detected in templates that don't use it.

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