Skip to content

[codex] Move generated GitHub Actions logic upstream#305

Merged
justin808 merged 1 commit into
mainfrom
jg-codex/reusable-github-actions
May 22, 2026
Merged

[codex] Move generated GitHub Actions logic upstream#305
justin808 merged 1 commit into
mainfrom
jg-codex/reusable-github-actions

Conversation

@justin808

@justin808 justin808 commented May 22, 2026

Copy link
Copy Markdown
Member

Summary

Moves the heavy generated GitHub Actions logic upstream into reusable workflows and shared composite actions.

Generated downstream repos now get thin wrappers that keep only:

  • event triggers and permissions
  • the small trusted-comment / same-repo safety gate
  • a reusable workflow call to shakacode/control-plane-flow
  • .github/cpflow-help.md

The deploy/delete/staging/promote/comment-formatting logic now lives in upstream .github/workflows/cpflow-*.yml, and shared composite actions live in upstream .github/actions/cpflow-*.

Also keeps the composite-action description fix from #297 in the upstream shared setup action so vars.* examples are not embedded in action metadata descriptions.

Testing

  • CPLN_ORG=dummy-test-org bundle exec rspec spec/command/generate_github_actions_spec.rb
  • RUBOCOP_CACHE_ROOT=/private/tmp/rubocop-cache bundle exec rubocop
  • actionlint -ignore 'SC2129' .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-help-command.yml .github/workflows/cpflow-promote-staging-to-production.yml .github/workflows/cpflow-review-app-help.yml lib/github_flow_templates/.github/workflows/cpflow-cleanup-stale-review-apps.yml lib/github_flow_templates/.github/workflows/cpflow-delete-review-app.yml lib/github_flow_templates/.github/workflows/cpflow-deploy-review-app.yml lib/github_flow_templates/.github/workflows/cpflow-deploy-staging.yml lib/github_flow_templates/.github/workflows/cpflow-help-command.yml lib/github_flow_templates/.github/workflows/cpflow-promote-staging-to-production.yml lib/github_flow_templates/.github/workflows/cpflow-review-app-help.yml
  • generated a smoke-test scaffold with CPFLOW_GITHUB_ACTIONS_REF=jg-codex/reusable-github-actions and verified only wrapper workflows plus .github/cpflow-help.md are emitted

I also started the full RSpec suite, but stopped it after it hung without output beyond the initial examples. The focused generator coverage above is green.


Note

Medium Risk
Medium risk because it rewires deployment/cleanup GitHub Actions to call upstream reusable workflows (with secrets: inherit) and changes Control Plane API auth/error handling, which could affect CI deployments and API access behavior.

Overview
Shifts the GitHub Actions generation model to upstream reusable workflows. Generated downstream repos now emit thin cpflow-* workflow wrappers that uses shakacode/control-plane-flow/.github/workflows/*@<ref> with secrets: inherit, instead of embedding the full deploy/delete/staging/promote logic locally.

Adds a new shared setup composite action and updates the generator. The new .github/actions/cpflow-setup-environment installs cpln plus cpflow (either from RubyGems by version or built from the checked-out source), supports a working_directory, and persists CPLN_TOKEN via GITHUB_ENV; generate-github-actions now substitutes __CPFLOW_GITHUB_ACTIONS_REF__ (configurable via CPFLOW_GITHUB_ACTIONS_REF) and a staging_app_branch_default instead of embedding expressions.

Hardens Control Plane API interactions for scoped tokens. API calls now normalize Authorization to Bearer ..., relax token-format validation (rejecting only newlines), introduce ForbiddenError without leaking response bodies, and make org_exists? tolerate nil/403 from list_orgs so scoped tokens can proceed.

Docs and specs are updated accordingly (new wrapper behavior, SHA-pinned GitHub-owned actions, and composite-action metadata descriptions avoiding ${{ ... }} parsing pitfalls).

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

Summary by CodeRabbit

  • New Features

    • Added reusable workflows for deploy, delete, promote, staging deploy, cleanup, and help; generated templates now delegate to upstream reusable workflows.
    • Installer gained a working_directory input; can install a pinned cpflow version or build/install from source; Control Plane token is persisted for later steps.
  • Bug Fixes

    • Removed embedded template snippets from action descriptions to avoid accidental syntax exposure.
  • Documentation

    • Docs and generator output updated to describe thin workflow wrappers and include a help file.
  • Tests

    • Specs expanded to validate reusable-workflow wiring and new template variables.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 22, 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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c735144c-4b0d-4063-9c54-1faba40cbff5

📥 Commits

Reviewing files that changed from the base of the PR and between 547060f and 2db67ab.

📒 Files selected for processing (32)
  • .github/actions/cpflow-build-docker-image/action.yml
  • .github/actions/cpflow-delete-control-plane-app/action.yml
  • .github/actions/cpflow-delete-control-plane-app/delete-app.sh
  • .github/actions/cpflow-detect-release-phase/action.yml
  • .github/actions/cpflow-setup-environment/action.yml
  • .github/actions/cpflow-validate-config/action.yml
  • .github/actions/cpflow-wait-for-health/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-help-command.yml
  • .github/workflows/cpflow-promote-staging-to-production.yml
  • .github/workflows/cpflow-review-app-help.yml
  • docs/ai-github-flow-prompt.md
  • docs/ci-automation.md
  • docs/commands.md
  • lib/command/generate_github_actions.rb
  • lib/core/controlplane.rb
  • lib/core/controlplane_api.rb
  • lib/core/controlplane_api_direct.rb
  • lib/github_flow_templates/.github/actions/cpflow-setup-environment/action.yml
  • lib/github_flow_templates/.github/workflows/cpflow-cleanup-stale-review-apps.yml
  • lib/github_flow_templates/.github/workflows/cpflow-delete-review-app.yml
  • lib/github_flow_templates/.github/workflows/cpflow-deploy-review-app.yml
  • lib/github_flow_templates/.github/workflows/cpflow-deploy-staging.yml
  • lib/github_flow_templates/.github/workflows/cpflow-help-command.yml
  • lib/github_flow_templates/.github/workflows/cpflow-promote-staging-to-production.yml
  • lib/github_flow_templates/.github/workflows/cpflow-review-app-help.yml
  • spec/command/generate_github_actions_spec.rb
  • spec/core/controlplane_api_direct_spec.rb
  • spec/core/controlplane_spec.rb
💤 Files with no reviewable changes (1)
  • lib/github_flow_templates/.github/actions/cpflow-setup-environment/action.yml
✅ Files skipped from review due to trivial changes (2)
  • docs/commands.md
  • lib/core/controlplane_api.rb

<review_stack_artifact>

</review_stack_artifact>

Walkthrough

Refactors generated GitHub Actions into thin wrapper workflows that delegate to upstream reusable workflows, fixes cpflow-setup-environment metadata and install behavior, adds reusable workflows for deploy/cleanup/help/promote flows, updates generator/template variables, adapts wrapper templates, extends specs, updates docs, and tightens token/org handling.

Changes

Reusable Workflows Architecture Refactoring

Layer / File(s) Summary
Setup action metadata & install
.github/actions/cpflow-setup-environment/action.yml
Action description/input text rewritten to avoid ${{ ... }} snippets, working_directory input added, ruby/setup-ruby invoked with working-directory, CPFLOW_SOURCE_DIR handled, default CPLN CLI version set, and cpflow install now chooses exact-version install or build-from-source; CPLN_TOKEN is persisted to GITHUB_ENV using a random delimiter.
New reusable workflows
.github/workflows/cpflow-*.yml
Added reusable workflow_call workflows: cleanup-stale-review-apps, delete-review-app, deploy-review-app, deploy-staging, help-command, promote-staging-to-production, and review-app-help — each encapsulates validation, cpflow orchestration, GitHub reactions/comments/deployments, health checks, and rollback logic.
Generated wrapper templates delegating to reusable workflows
lib/github_flow_templates/.github/workflows/cpflow-*.yml
Previously-inline workflow implementations replaced by thin wrapper jobs that invoke upstream reusable workflows via uses: shakacode/control-plane-flow/...@__CPFLOW_GITHUB_ACTIONS_REF__, passing inputs and inheriting secrets.
Generator template variables & helpers
lib/command/generate_github_actions.rb
Add __CPFLOW_GITHUB_ACTIONS_REF__ and __STAGING_BRANCH_DEFAULT__ template variables and helper methods cpflow_github_actions_ref and staging_branch_default; update example docs to describe thin workflow wrappers.
Core token/org handling
lib/core/controlplane.rb, lib/core/controlplane_api_direct.rb
org_exists? now tolerates nil from api.list_orgs; ControlplaneApiDirect removes regex token validation, requires tokens be non-empty and newline-free, preserves Bearer prefixes, raises ForbiddenError for 403s with suppressed response body, and guards JWT decode payload when deciding token refresh.
Specs and tests
spec/*
Specs refactored to validate wrapper delegation to reusable workflows, ensure composite action descriptions contain no ${{ expressions, parse both generated and shared YAML, assert cpflow install/build branching, and add token/org tests.
Docs
docs/*
Docs updated to present generator output as thin .github/workflows wrappers delegating to upstream reusable workflows; rollout checklist now requires .github/cpflow-help.md and removes local composite-action file list.

Sequence Diagram(s)

sequenceDiagram
  participant GitHubEvent as GitHub Event
  participant Wrapper as Generated Wrapper Workflow
  participant ReusableFlow as shakacode/control-plane-flow Reusable Workflow
  participant CPFlowCLI as cpflow CLI
  participant GitHubAPI as GitHub API
  GitHubEvent->>Wrapper: trigger (issue_comment/pull_request/workflow_dispatch)
  Wrapper->>ReusableFlow: workflow_call (control_plane_flow_ref, inputs, secrets: inherit)
  ReusableFlow->>CPFlowCLI: setup + run cpflow commands (exists/deploy/delete/cleanup)
  ReusableFlow->>GitHubAPI: create/update comments, reactions, Deployment Status
  CPFlowCLI-->>ReusableFlow: command results / workload URLs
  ReusableFlow-->>Wrapper: outputs (status, environment_url)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • dzirtusss
  • borela
  • rafaelgomesxyz

"A rabbit hops with workflows grand,
Thin wrappers call the upstream land,
Descriptions fixed and versions pinned,
Tests updated, docs aligned.
Hop, deploy, and sing: all systems grinned." 🐰✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title '[codex] Move generated GitHub Actions logic upstream' accurately summarizes the main objective: moving GitHub Actions logic from generated workflows to upstream reusable workflows.
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.

✏️ 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/reusable-github-actions

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.

@justin808 justin808 force-pushed the jg-codex/reusable-github-actions branch from 2b7e8cf to fae07ad Compare May 22, 2026 03:33

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

🧹 Nitpick comments (2)
.github/workflows/cpflow-deploy-staging.yml (1)

63-63: ⚡ Quick win

actions/checkout uses @v6 tags—only pin to commit SHAs if this repo enforces it.

.github/workflows/cpflow-deploy-staging.yml uses uses: actions/checkout@v6 (mutable tags) at lines 63, 90, 95, 127, and 132. This repo’s own workflow template code explicitly allows floating major tags for generated templates and suggests downstream repos with stricter supply-chain requirements should replace uses: actions/...@vN with immutable commit SHAs—so if “unpinned-uses” is an active policy for this repo, update these steps; otherwise treat SHA pinning as an optional hardening improvement.

🤖 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 @.github/workflows/cpflow-deploy-staging.yml at line 63, The workflow
currently uses mutable tags for the checkout action via the string "uses:
actions/checkout@v6"; if your repo enforces the "unpinned-uses" policy, replace
each "uses: actions/checkout@v6" occurrence with the corresponding immutable
commit SHA (e.g., "uses: actions/checkout@<commit-sha>") by looking up the exact
commit for the v6 release on the actions/checkout GitHub repo, or otherwise
document that floating major tags are acceptable; update all occurrences of the
"uses: actions/checkout@v6" token in the workflow to use the chosen SHA-pinned
ref or leave as-is if floating tags are allowed.
.github/workflows/cpflow-delete-review-app.yml (1)

44-44: ⚡ Quick win

Action SHA pinning isn’t required here (repo intentionally uses floating major tags).

These uses: actions/github-script@v8 (lines 44/95/108/137) and uses: actions/checkout@v6 (line 63) align with the repo’s documented approach in .github/actions/cpflow-setup-environment/action.yml, which intentionally pins third-party actions to floating major tags rather than immutable SHAs; SHA pinning is positioned as an option for downstream repos with stricter requirements. Consider immutable SHA pinning only as optional supply-chain hardening.

🤖 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 @.github/workflows/cpflow-delete-review-app.yml at line 44, Keep the existing
floating major tags instead of pinning to immutable SHAs: leave the uses entries
as actions/github-script@v8 (all occurrences) and actions/checkout@v6 rather
than replacing them with specific SHAs; ensure the workflow matches the repo
convention described in .github/actions/cpflow-setup-environment/action.yml so
only downstream repos opt into SHA pinning if desired.
🤖 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/workflows/cpflow-cleanup-stale-review-apps.yml:
- Line 29: The workflow currently uses the mutable tag "uses:
actions/checkout@v6"; replace that with a pinned immutable commit SHA (full
40-character commit) for actions/checkout to avoid supply-chain risks. Locate
the "uses: actions/checkout@v6" entry in the CPFlow cleanup workflow and update
it to "uses: actions/checkout@<COMMIT_SHA>" where <COMMIT_SHA> is the exact
commit hash from the actions/checkout repository corresponding to the v6 release
you want to lock to.

In @.github/workflows/cpflow-deploy-review-app.yml:
- Line 54: The workflow uses mutable action tags (actions/github-script@v8 and
actions/checkout@v6) which should be SHA-pinned for the privileged deploy path;
locate every occurrence of "uses: actions/github-script@v8" and "uses:
actions/checkout@v6" in the YAML and replace the tag with the corresponding full
commit SHA (e.g., actions/github-script@<commit-sha> and
actions/checkout@<commit-sha>) by looking up the desired release commit on the
action's GitHub repo and substituting that SHA so each "uses:" entry is pinned.

In @.github/workflows/cpflow-help-command.yml:
- Line 31: Replace mutable action tags with immutable commit SHAs: update every
"uses: actions/github-script@v8" occurrence (both spots) to the corresponding
full commit SHA for that release and update "uses: actions/checkout@v6" to its
full commit SHA; ensure you commit the exact SHA strings (not tags) so the
workflow references the pinned commits instead of v-tags.

In @.github/workflows/cpflow-promote-staging-to-production.yml:
- Line 56: Replace the loose version tag for actions/checkout (currently
"actions/checkout@v6") in the two checkout steps ("Checkout repository" and
"Checkout control-plane-flow actions") with the corresponding immutable commit
SHAs; locate the uses: "actions/checkout@v6" entries in the workflow and change
them to "actions/checkout@<commit-sha>" using the official checkout repository
commit SHA you want to pin to so both checkout steps reference the exact same
pinned SHA for supply-chain integrity.

In `@lib/github_flow_templates/.github/workflows/cpflow-delete-review-app.yml`:
- Around line 29-32: The reusable-workflow wrapper forwards a mutable branch ref
via `@__CPFLOW_GITHUB_ACTIONS_REF__` and the generator uses
ENV.fetch("CPFLOW_GITHUB_ACTIONS_REF", "main"); change the generator and CI to
only accept immutable refs (commit SHAs or release tags) by validating
cpflow_github_actions_ref against a SHA/tag regex and rejecting branch names
(remove the "main" default), and update usages in
lib/github_flow_templates/.github/workflows/* (e.g.,
cpflow-delete-review-app.yml referencing `@__CPFLOW_GITHUB_ACTIONS_REF__`) to
require a pinned ref; also add a CI check that fails if
CPFLOW_GITHUB_ACTIONS_REF is a branch name so generation/consumers cannot supply
mutable refs.

---

Nitpick comments:
In @.github/workflows/cpflow-delete-review-app.yml:
- Line 44: Keep the existing floating major tags instead of pinning to immutable
SHAs: leave the uses entries as actions/github-script@v8 (all occurrences) and
actions/checkout@v6 rather than replacing them with specific SHAs; ensure the
workflow matches the repo convention described in
.github/actions/cpflow-setup-environment/action.yml so only downstream repos opt
into SHA pinning if desired.

In @.github/workflows/cpflow-deploy-staging.yml:
- Line 63: The workflow currently uses mutable tags for the checkout action via
the string "uses: actions/checkout@v6"; if your repo enforces the
"unpinned-uses" policy, replace each "uses: actions/checkout@v6" occurrence with
the corresponding immutable commit SHA (e.g., "uses:
actions/checkout@<commit-sha>") by looking up the exact commit for the v6
release on the actions/checkout GitHub repo, or otherwise document that floating
major tags are acceptable; update all occurrences of the "uses:
actions/checkout@v6" token in the workflow to use the chosen SHA-pinned ref or
leave as-is if floating tags are allowed.
🪄 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: 470e4dcf-4211-482c-b5cf-2207cfcde67e

📥 Commits

Reviewing files that changed from the base of the PR and between 02d92ad and 2b7e8cf.

📒 Files selected for processing (26)
  • .github/actions/cpflow-build-docker-image/action.yml
  • .github/actions/cpflow-delete-control-plane-app/action.yml
  • .github/actions/cpflow-delete-control-plane-app/delete-app.sh
  • .github/actions/cpflow-detect-release-phase/action.yml
  • .github/actions/cpflow-setup-environment/action.yml
  • .github/actions/cpflow-validate-config/action.yml
  • .github/actions/cpflow-wait-for-health/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-help-command.yml
  • .github/workflows/cpflow-promote-staging-to-production.yml
  • .github/workflows/cpflow-review-app-help.yml
  • docs/ai-github-flow-prompt.md
  • docs/ci-automation.md
  • docs/commands.md
  • lib/command/generate_github_actions.rb
  • lib/github_flow_templates/.github/workflows/cpflow-cleanup-stale-review-apps.yml
  • lib/github_flow_templates/.github/workflows/cpflow-delete-review-app.yml
  • lib/github_flow_templates/.github/workflows/cpflow-deploy-review-app.yml
  • lib/github_flow_templates/.github/workflows/cpflow-deploy-staging.yml
  • lib/github_flow_templates/.github/workflows/cpflow-help-command.yml
  • lib/github_flow_templates/.github/workflows/cpflow-promote-staging-to-production.yml
  • lib/github_flow_templates/.github/workflows/cpflow-review-app-help.yml
  • spec/command/generate_github_actions_spec.rb

Comment thread .github/workflows/cpflow-cleanup-stale-review-apps.yml Outdated
Comment thread .github/workflows/cpflow-deploy-review-app.yml Outdated
Comment thread .github/workflows/cpflow-help-command.yml Outdated
Comment thread .github/workflows/cpflow-promote-staging-to-production.yml Outdated

@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: 2

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

589-592: ⚡ Quick win

Add coverage for a non-default CPFLOW_GITHUB_ACTIONS_REF.

Current assertions lock in the default "main" path, but they don’t verify that a custom env ref is actually propagated into generated wrappers. A small env-scoped example would protect the new feature from regressions.

🤖 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/generate_github_actions_spec.rb` around lines 589 - 592, The
spec currently only asserts the default replacement for
__CPFLOW_GITHUB_ACTIONS_REF__ ("main"); add a focused example in
generate_github_actions_spec.rb that sets a non-default ENV value for
CPFLOW_GITHUB_ACTIONS_REF (e.g. "refs/heads/custom"), runs the same generation
flow that produces actual = playground.join(relative_path).read, and asserts the
generated wrapper contains that custom ref instead of "main"; ensure you set and
restore the ENV within the example (or use with_modified_env) so other tests are
unaffected and reference the same relative_path/playground read code path used
in the existing examples.
🤖 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 `@lib/command/generate_github_actions.rb`:
- Around line 66-67: The cpflow_github_actions_ref method currently returns
ENV.fetch("CPFLOW_GITHUB_ACTIONS_REF", "main") which still yields an empty
string if the env var is set to "", so update cpflow_github_actions_ref to guard
against empty/whitespace-only values by reading the env var
(ENV["CPFLOW_GITHUB_ACTIONS_REF"] or ENV.fetch), trimming it (to_s.strip) and
returning the trimmed value if non-empty, otherwise falling back to "main";
ensure you reference the cpflow_github_actions_ref method when applying this
change.

In `@lib/github_flow_templates/.github/workflows/cpflow-review-app-help.yml`:
- Around line 16-18: The show-help step currently uses the upstream workflow via
"uses:
shakacode/control-plane-flow/.github/workflows/cpflow-review-app-help.yml@__CPFLOW_GITHUB_ACTIONS_REF__"
but omits the usual with: and secrets: blocks; update the show-help job to
include a with: block passing control_plane_flow_ref:
__CPFLOW_GITHUB_ACTIONS_REF__ (and any other required inputs the upstream
workflow expects) and add a secrets: block to forward necessary secrets (e.g.,
GITHUB_TOKEN or other repo secrets) while preserving the existing if:
vars.REVIEW_APP_PREFIX != '' condition so the wrapper matches other templates
like cpflow-help-command.yml.

---

Nitpick comments:
In `@spec/command/generate_github_actions_spec.rb`:
- Around line 589-592: The spec currently only asserts the default replacement
for __CPFLOW_GITHUB_ACTIONS_REF__ ("main"); add a focused example in
generate_github_actions_spec.rb that sets a non-default ENV value for
CPFLOW_GITHUB_ACTIONS_REF (e.g. "refs/heads/custom"), runs the same generation
flow that produces actual = playground.join(relative_path).read, and asserts the
generated wrapper contains that custom ref instead of "main"; ensure you set and
restore the ENV within the example (or use with_modified_env) so other tests are
unaffected and reference the same relative_path/playground read code path used
in the existing examples.
🪄 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: d912a0ef-4c09-435b-840a-ef3adfcb87c9

📥 Commits

Reviewing files that changed from the base of the PR and between 2b7e8cf and fae07ad.

📒 Files selected for processing (26)
  • .github/actions/cpflow-build-docker-image/action.yml
  • .github/actions/cpflow-delete-control-plane-app/action.yml
  • .github/actions/cpflow-delete-control-plane-app/delete-app.sh
  • .github/actions/cpflow-detect-release-phase/action.yml
  • .github/actions/cpflow-setup-environment/action.yml
  • .github/actions/cpflow-validate-config/action.yml
  • .github/actions/cpflow-wait-for-health/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-help-command.yml
  • .github/workflows/cpflow-promote-staging-to-production.yml
  • .github/workflows/cpflow-review-app-help.yml
  • docs/ai-github-flow-prompt.md
  • docs/ci-automation.md
  • docs/commands.md
  • lib/command/generate_github_actions.rb
  • lib/github_flow_templates/.github/workflows/cpflow-cleanup-stale-review-apps.yml
  • lib/github_flow_templates/.github/workflows/cpflow-delete-review-app.yml
  • lib/github_flow_templates/.github/workflows/cpflow-deploy-review-app.yml
  • lib/github_flow_templates/.github/workflows/cpflow-deploy-staging.yml
  • lib/github_flow_templates/.github/workflows/cpflow-help-command.yml
  • lib/github_flow_templates/.github/workflows/cpflow-promote-staging-to-production.yml
  • lib/github_flow_templates/.github/workflows/cpflow-review-app-help.yml
  • spec/command/generate_github_actions_spec.rb
✅ Files skipped from review due to trivial changes (2)
  • docs/commands.md
  • docs/ci-automation.md

Comment thread lib/command/generate_github_actions.rb Outdated
@greptile-apps

greptile-apps Bot commented May 22, 2026

Copy link
Copy Markdown

Greptile Summary

This PR migrates the heavy GitHub Actions logic from generated downstream repositories into reusable upstream workflows (shakacode/control-plane-flow) and shared composite actions. Generated downstream repos now receive thin wrappers with only event triggers, permission declarations, a uses: call to the upstream workflow, and .github/cpflow-help.md.

  • The template generator now emits only 7 thin workflow files plus cpflow-help.md; all action files (previously duplicated into every downstream repo) are gone from the generator output and are instead referenced directly from shakacode/control-plane-flow at a configurable CPFLOW_GITHUB_ACTIONS_REF git ref.
  • The __STAGING_APP_BRANCH_EXPRESSION__ template variable is replaced by __STAGING_BRANCH_DEFAULT__, which passes a bare branch name to the upstream reusable workflow's staging_app_branch_default input; the vars.STAGING_APP_BRANCH || inputs.staging_app_branch_default fallback expression now lives in the upstream workflow.
  • The spec is updated throughout to assert against the shared upstream file paths (Cpflow.root_path.join(...)) rather than the playground, correctly reflecting that composite actions are no longer copied downstream.

Confidence Score: 4/5

Safe to merge; no functional regressions found. The architecture shift is clean and the key security properties (same-repo gate, author_association checks, no PR code on trusted runners) are preserved in the upstream workflows.

The upstream workflows correctly inherit github.event_name and the full event context through workflow_call, so all if: guards, pull_request_target handling, and workflow_dispatch input access work as intended. The only findings are a dead __CPFLOW_VERSION__ entry left in template_variables and the spec drift test, and a help-doc section that describes a testing pattern now superseded by CPFLOW_GITHUB_ACTIONS_REF.

The two cleanups are self-contained: lib/command/generate_github_actions.rb (dead template variable) and spec/command/generate_github_actions_spec.rb (matching dead substitution in the drift test). The cpflow-help.md testing section warrants a documentation update.

Important Files Changed

Filename Overview
lib/command/generate_github_actions.rb Adds __CPFLOW_GITHUB_ACTIONS_REF__ and __STAGING_BRANCH_DEFAULT__ variables; __CPFLOW_VERSION__ is still present in template_variables but no longer used by any template file.
spec/command/generate_github_actions_spec.rb Tests correctly redirect shared-path assertions to Cpflow.root_path; drift test still substitutes __CPFLOW_VERSION__ even though no template contains that placeholder.
.github/workflows/cpflow-deploy-review-app.yml New upstream reusable workflow with full deploy logic, defense-in-depth if: conditions, same-repo safety gate, and control_plane_flow_ref input for composite action checkout.
.github/workflows/cpflow-deploy-staging.yml New upstream reusable staging workflow with three-job structure (validate-branch, build, deploy); accepts staging_app_branch_default as fallback when vars.STAGING_APP_BRANCH is unset.
.github/workflows/cpflow-promote-staging-to-production.yml New upstream promote workflow; create-github-release job uses job-level permissions: contents: write, which is within the calling template's permissions: contents: write grant.
lib/github_flow_templates/.github/workflows/cpflow-deploy-staging.yml Thin wrapper now passes staging_app_branch_default to the upstream workflow; branch filter comment explains the GitHub limitation on repo vars in push triggers.
lib/github_flow_templates/.github/cpflow-help.md Still contains an 'Advanced: testing changes to generated workflows' section that describes gh workflow run --ref <your-pr-branch>, which only tests the thin wrapper, not the upstream logic.
.github/actions/cpflow-setup-environment/action.yml Descriptions now use plain prose (no ${{ vars.* }} syntax), fixing the issue #293 action-load failure. Version defaults are hardcoded and kept in sync via the spec.

Sequence Diagram

sequenceDiagram
    participant DW as Downstream Wrapper<br/>(generated .github/workflows/)
    participant UW as Upstream Reusable Workflow<br/>(shakacode/control-plane-flow)
    participant CA as Composite Actions<br/>(.cpflow/.github/actions/)

    Note over DW: Event triggers, permissions,<br/>safety-gate if condition
    DW->>UW: "uses: cpflow-deploy-review-app.yml@ref"
    DW->>UW: with: control_plane_flow_ref / secrets: inherit

    UW->>UW: React to comment command
    UW->>CA: actions/checkout (shakacode/control-plane-flow → .cpflow)
    UW->>CA: cpflow-validate-config
    UW->>UW: Resolve PR ref / same-repo gate
    UW->>UW: Checkout PR commit → app/
    UW->>CA: cpflow-setup-environment
    UW->>CA: cpflow-detect-release-phase
    UW->>CA: cpflow-build-docker-image
    UW->>UW: cpflow deploy-image
    UW->>CA: cpflow-wait-for-health
    UW-->>DW: job result
Loading

Reviews (1): Last reviewed commit: "Move generated GitHub Actions logic upst..." | Re-trigger Greptile

Comment on lines 40 to 47
def template_variables
{
"__CPFLOW_VERSION__" => ::Cpflow::VERSION,
"__CPFLOW_GITHUB_ACTIONS_REF__" => cpflow_github_actions_ref,
"__STAGING_BRANCH_FILTER__" => staging_branch_filter,
"__STAGING_APP_BRANCH_EXPRESSION__" => staging_app_branch_expression
"__STAGING_BRANCH_DEFAULT__" => staging_branch_default
}
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 The __CPFLOW_VERSION__ key is no longer used by any template file — no file under lib/github_flow_templates/ contains this placeholder. The version is now hardcoded directly in .github/actions/cpflow-setup-environment/action.yml and kept in sync via the spec. Leaving the entry in template_variables is dead code that the substitute_template_variables helper iterates over on every generation run.

Suggested change
def template_variables
{
"__CPFLOW_VERSION__" => ::Cpflow::VERSION,
"__CPFLOW_GITHUB_ACTIONS_REF__" => cpflow_github_actions_ref,
"__STAGING_BRANCH_FILTER__" => staging_branch_filter,
"__STAGING_APP_BRANCH_EXPRESSION__" => staging_app_branch_expression
"__STAGING_BRANCH_DEFAULT__" => staging_branch_default
}
end
def template_variables
{
"__CPFLOW_GITHUB_ACTIONS_REF__" => cpflow_github_actions_ref,
"__STAGING_BRANCH_FILTER__" => staging_branch_filter,
"__STAGING_BRANCH_DEFAULT__" => staging_branch_default
}
end

Comment on lines 586 to 590
template = template_root.join(relative_path).read
expected = template
.gsub("__CPFLOW_VERSION__", Cpflow::VERSION)
.gsub("__CPFLOW_GITHUB_ACTIONS_REF__", "main")
.gsub("__STAGING_BRANCH_FILTER__", %("main", "master"))

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 __CPFLOW_VERSION__ substitution in the drift test is now a no-op — no template file contains that placeholder. It should be removed in lockstep with the template_variables cleanup to keep the test an accurate mirror of what the generator actually does.

Suggested change
template = template_root.join(relative_path).read
expected = template
.gsub("__CPFLOW_VERSION__", Cpflow::VERSION)
.gsub("__CPFLOW_GITHUB_ACTIONS_REF__", "main")
.gsub("__STAGING_BRANCH_FILTER__", %("main", "master"))
expected = template
.gsub("__CPFLOW_GITHUB_ACTIONS_REF__", "main")
.gsub("__STAGING_BRANCH_FILTER__", %("main", "master"))
.gsub("__STAGING_BRANCH_DEFAULT__", "")

@justin808 justin808 force-pushed the jg-codex/reusable-github-actions branch 6 times, most recently from ee25ceb to 6188949 Compare May 22, 2026 03:54

@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: 618894963c

ℹ️ 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 +86 to +90
uses: ./.cpflow/.github/actions/cpflow-setup-environment
with:
token: ${{ secrets.CPLN_TOKEN_STAGING }}
org: ${{ vars.CPLN_ORG_STAGING }}
cpln_cli_version: ${{ vars.CPLN_CLI_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.

P1 Badge Pass working_directory when invoking setup in delete/cleanup jobs

These jobs only check out shakacode/control-plane-flow into .cpflow, then call cpflow-setup-environment without a working_directory. In that action, ruby/setup-ruby auto-detects Ruby from the working directory, which defaults to .; here . is not the checked-out .cpflow tree, so Ruby resolution can fail or drift to the runner’s system Ruby and break review-app deletion/cleanup runs. Set working_directory: .cpflow (or check out the caller repo before setup) to make runtime deterministic; the same omission also appears in .github/workflows/cpflow-cleanup-stale-review-apps.yml.

Useful? React with 👍 / 👎.

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Missing user repo checkout breaks cleanup and delete workflows
    • Added caller repository checkout steps before loading cpflow actions in both cleanup and delete reusable workflows so Ruby setup and cpflow run from the app repository root.

Create PR

Or push these changes by commenting:

@cursor push 796f90efe2
Preview (796f90efe2)
diff --git a/.github/workflows/cpflow-cleanup-stale-review-apps.yml b/.github/workflows/cpflow-cleanup-stale-review-apps.yml
--- a/.github/workflows/cpflow-cleanup-stale-review-apps.yml
+++ b/.github/workflows/cpflow-cleanup-stale-review-apps.yml
@@ -25,6 +25,11 @@
     runs-on: ubuntu-latest
     timeout-minutes: 30
     steps:
+      - name: Checkout repository
+        uses: actions/checkout@v6
+        with:
+          persist-credentials: false
+
       - name: Checkout control-plane-flow actions
         uses: actions/checkout@v6
         with:

diff --git a/.github/workflows/cpflow-delete-review-app.yml b/.github/workflows/cpflow-delete-review-app.yml
--- a/.github/workflows/cpflow-delete-review-app.yml
+++ b/.github/workflows/cpflow-delete-review-app.yml
@@ -59,6 +59,11 @@
               }
             }
 
+      - name: Checkout repository
+        uses: actions/checkout@v6
+        with:
+          persist-credentials: false
+
       - name: Checkout control-plane-flow actions
         uses: actions/checkout@v6
         with:

You can send follow-ups to the cloud agent here.

Comment thread .github/workflows/cpflow-cleanup-stale-review-apps.yml
@justin808 justin808 force-pushed the jg-codex/reusable-github-actions branch from 6188949 to 25e80af Compare May 22, 2026 04:32
env:
CPLN_CLI_VERSION: ${{ inputs.cpln_cli_version }}
CPFLOW_VERSION: ${{ inputs.cpflow_version }}
CPFLOW_SOURCE_DIR: ${{ github.action_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.

Fragile relative path from github.action_path

github.action_path resolves to .cpflow/.github/actions/cpflow-setup-environment, so ../../.. correctly lands at .cpflow. However, this silently breaks if the action is ever nested differently (e.g., moved one level deeper).

Consider making this path more explicit and robust:

Suggested change
CPFLOW_SOURCE_DIR: ${{ github.action_path }}/../../..
CPFLOW_SOURCE_DIR: ${{ github.action_path }}/../../../..

Wait — actually the current path is intentional (action is exactly 3 levels under the repo root: .cpflow/.github/actions/<action-name>). A self-documenting alternative:

Suggested change
CPFLOW_SOURCE_DIR: ${{ github.action_path }}/../../..
# github.action_path = .cpflow/.github/actions/cpflow-setup-environment → 3 levels up = .cpflow
CPFLOW_SOURCE_DIR: ${{ github.action_path }}/../../..

At minimum, adding a runtime sanity-check would catch misuse early:

if [[ ! -f "${CPFLOW_SOURCE_DIR}/cpflow.gemspec" ]]; then
  echo "::error::cpflow.gemspec not found at ${CPFLOW_SOURCE_DIR}; check the action's path depth." >&2
  exit 1
fi

end
return @@api_token if @@api_token[:token].match?(API_TOKEN_REGEX)
token = @@api_token[:token]
return @@api_token if token && !token.empty? && !token.match?(/[\r\n]/)

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.

Token validation now accepts almost any non-empty string

The old API_TOKEN_REGEX = /^[\w\-._]+$/ was too strict for scoped service-account tokens (which contain /, +, :, =). The new check correctly allows those, but the trade-off is that malformed tokens now surface at the first actual API call rather than upfront — potentially with a less obvious error message.

This is the right design decision for scoped tokens, but worth a brief comment explaining why the regex was dropped rather than relaxed, so future readers don't see it as accidentally too-permissive:

Suggested change
return @@api_token if token && !token.empty? && !token.match?(/[\r\n]/)
# Allow any token that doesn't contain line-breaks. Scoped service-account tokens
# include characters like '/', '+', ':', '=' that the old word-char regex rejected;
# format enforcement is deferred to the API, which returns a clear auth error on
# bad tokens.
return @@api_token if token && !token.empty? && !token.match?(/[\r\n]/)

Comment thread lib/core/controlplane.rb

# Scoped service-account tokens may not be allowed to list every org. Let
# the command's target API call report any org/resource problem instead.
return true unless result

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.

nil from list_orgs silently bypasses org validation

Returning true when list_orgs returns nil is the right call for scoped tokens that can't enumerate orgs. The concern is that a transient API error that happens to return nil (rather than raising) would also bypass the check.

If ControlplaneApiDirect can return nil for network/HTTP failures (not just permission-denied responses), it may be worth distinguishing the two:

# nil means the token is scoped and can't list orgs — assume it's correct and let
# the subsequent API call surface any org/auth problem with a real error message.
return true unless result

The comment is good. Just confirm that api.list_orgs only returns nil for 403/permission responses and raises (not returns nil) for genuine network errors — otherwise this broadens the bypass surface beyond intent.

# Don't cancel an in-flight promotion: a half-finished `cpflow deploy-image` plus a
# rollback can leave production in a worse state than letting the first run finish.
cancel-in-progress: false
contents: write

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.

Permissions elevated to contents: write at the workflow level

The upstream reusable workflow scopes contents: write narrowly to the create-github-release job. By granting contents: write here at the wrapper's workflow level, the promote-to-production job call also runs with write access — even though it only needs contents: read.

GitHub's reusable-workflow permission model requires the caller to grant any permission the callee uses, so this is technically necessary. Consider adding a comment to explain the elevation:

Suggested change
contents: write
permissions:
# contents: write is required for the create-github-release job in the upstream reusable
# workflow. The promote-to-production job only needs read, but callers must grant the union.
contents: write


jobs:
delete-review-app:
if: |

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.

pull_request_target security rationale was removed from the wrapper

The original template had a clear inline comment explaining why pull_request_target is used (fork PRs need write permission to delete the review app and update PR comments, and the checkout doesn't use the PR ref: so it's safe). That comment was intentionally moved to the upstream reusable workflow — but the wrapper no longer carries any explanation.

Downstream adopters reading this generated file will see pull_request_target without the security context. Consider keeping a brief note here:

jobs:
  delete-review-app:
    # pull_request_target is intentional: fork PR-close events need access to staging
    # secrets to delete review apps and update PR comments. The upstream reusable workflow
    # checks out trusted base-branch action code (not the fork head), keeping secrets safe.
    if: |

});
uses: shakacode/control-plane-flow/.github/workflows/cpflow-deploy-review-app.yml@__CPFLOW_GITHUB_ACTIONS_REF__
with:
control_plane_flow_ref: __CPFLOW_GITHUB_ACTIONS_REF__

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.

secrets: inherit passes all repository secrets upstream

This is the correct approach for reusable workflows that need CPLN_TOKEN_STAGING, DOCKER_BUILD_SSH_KEY, etc. However, secrets: inherit passes every secret in the calling repository — including any secrets unrelated to cpflow — to the upstream shakacode/control-plane-flow reusable workflow.

GitHub's reusable workflow trust model means the callee receives the secrets but can only access the ones it explicitly references. Still, it's worth documenting this in the generated file so downstream teams are aware:

    secrets: inherit
    # Note: `secrets: inherit` passes all repository secrets to the upstream reusable
    # workflow. The upstream workflow only uses the secrets it explicitly references
    # (CPLN_TOKEN_STAGING, DOCKER_BUILD_SSH_KEY, etc.), but downstream repos should
    # be aware of this trust delegation when adding new secrets.

@justin808 justin808 force-pushed the jg-codex/reusable-github-actions branch from 25e80af to 27a862e Compare May 22, 2026 04:36

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/github_flow_templates/.github/workflows/cpflow-help-command.yml (1)

26-27: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing with: and secrets: blocks for consistency with other wrappers.

All other wrapper templates in this PR pass control_plane_flow_ref via with: and forward secrets via secrets: inherit. This template is missing both, which may cause issues if the upstream reusable workflow expects these inputs or needs access to secrets.

Proposed fix
     uses: shakacode/control-plane-flow/.github/workflows/cpflow-help-command.yml@__CPFLOW_GITHUB_ACTIONS_REF__
+    with:
+      control_plane_flow_ref: __CPFLOW_GITHUB_ACTIONS_REF__
+    secrets: inherit
🤖 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 `@lib/github_flow_templates/.github/workflows/cpflow-help-command.yml` around
lines 26 - 27, The reusable-workflow invocation that currently only has the
uses:
shakacode/control-plane-flow/.github/workflows/cpflow-help-command.yml@__CPFLOW_GITHUB_ACTIONS_REF__
needs to be updated to include a with: block passing control_plane_flow_ref
(same input used by other wrappers) and a secrets: inherit block so secrets are
forwarded to the upstream workflow; locate the line with the uses: value and add
the with key supplying control_plane_flow_ref (using the same var/expr pattern
as other templates) and add secrets: inherit beneath it.
🧹 Nitpick comments (1)
spec/core/controlplane_api_direct_spec.rb (1)

60-68: ⚡ Quick win

Add explicit carriage-return rejection coverage.

The implementation rejects both \n and \r, but this spec only locks in \n. Please add a \r case so the full guard remains protected.

Proposed spec addition
     it "rejects tokens containing newlines" do
       stub_env("CPLN_TOKEN", "token\nsecond-line")

       message = "Unknown API token format. " \
                 "Please re-run 'cpln profile login' or set the correct CPLN_TOKEN env variable."
       expect do
         described_instance.api_token
       end.to raise_error(RuntimeError, message)
     end
+
+    it "rejects tokens containing carriage returns" do
+      stub_env("CPLN_TOKEN", "token\rsecond-line")
+
+      message = "Unknown API token format. " \
+                "Please re-run 'cpln profile login' or set the correct CPLN_TOKEN env variable."
+      expect do
+        described_instance.api_token
+      end.to raise_error(RuntimeError, message)
+    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/core/controlplane_api_direct_spec.rb` around lines 60 - 68, Add a second
test case that mirrors the existing newline rejection spec but uses a
carriage-return character: call stub_env("CPLN_TOKEN", "token\rsecond-line") and
assert that described_instance.api_token raises the same RuntimeError with the
same message; you can either duplicate the existing it "rejects tokens
containing newlines" as a new it "rejects tokens containing carriage returns" or
extend the current spec to include both "\n" and "\r" cases while still
referencing described_instance.api_token and the same expected message.
🤖 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.

Outside diff comments:
In `@lib/github_flow_templates/.github/workflows/cpflow-help-command.yml`:
- Around line 26-27: The reusable-workflow invocation that currently only has
the uses:
shakacode/control-plane-flow/.github/workflows/cpflow-help-command.yml@__CPFLOW_GITHUB_ACTIONS_REF__
needs to be updated to include a with: block passing control_plane_flow_ref
(same input used by other wrappers) and a secrets: inherit block so secrets are
forwarded to the upstream workflow; locate the line with the uses: value and add
the with key supplying control_plane_flow_ref (using the same var/expr pattern
as other templates) and add secrets: inherit beneath it.

---

Nitpick comments:
In `@spec/core/controlplane_api_direct_spec.rb`:
- Around line 60-68: Add a second test case that mirrors the existing newline
rejection spec but uses a carriage-return character: call stub_env("CPLN_TOKEN",
"token\rsecond-line") and assert that described_instance.api_token raises the
same RuntimeError with the same message; you can either duplicate the existing
it "rejects tokens containing newlines" as a new it "rejects tokens containing
carriage returns" or extend the current spec to include both "\n" and "\r" cases
while still referencing described_instance.api_token and the same expected
message.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 430468af-bf96-4ff0-8919-e2addda8d064

📥 Commits

Reviewing files that changed from the base of the PR and between fae07ad and 25e80af.

📒 Files selected for processing (30)
  • .github/actions/cpflow-build-docker-image/action.yml
  • .github/actions/cpflow-delete-control-plane-app/action.yml
  • .github/actions/cpflow-delete-control-plane-app/delete-app.sh
  • .github/actions/cpflow-detect-release-phase/action.yml
  • .github/actions/cpflow-setup-environment/action.yml
  • .github/actions/cpflow-validate-config/action.yml
  • .github/actions/cpflow-wait-for-health/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-help-command.yml
  • .github/workflows/cpflow-promote-staging-to-production.yml
  • .github/workflows/cpflow-review-app-help.yml
  • docs/ai-github-flow-prompt.md
  • docs/ci-automation.md
  • docs/commands.md
  • lib/command/generate_github_actions.rb
  • lib/core/controlplane.rb
  • lib/core/controlplane_api_direct.rb
  • lib/github_flow_templates/.github/workflows/cpflow-cleanup-stale-review-apps.yml
  • lib/github_flow_templates/.github/workflows/cpflow-delete-review-app.yml
  • lib/github_flow_templates/.github/workflows/cpflow-deploy-review-app.yml
  • lib/github_flow_templates/.github/workflows/cpflow-deploy-staging.yml
  • lib/github_flow_templates/.github/workflows/cpflow-help-command.yml
  • lib/github_flow_templates/.github/workflows/cpflow-promote-staging-to-production.yml
  • lib/github_flow_templates/.github/workflows/cpflow-review-app-help.yml
  • spec/command/generate_github_actions_spec.rb
  • spec/core/controlplane_api_direct_spec.rb
  • spec/core/controlplane_spec.rb
✅ Files skipped from review due to trivial changes (1)
  • docs/ci-automation.md

@claude

claude Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

Code Review: PR 305

Overview

Well-designed architectural shift. Moving deploy/delete/promote/cleanup logic into upstream reusable workflows eliminates 1,400+ lines of duplicated code from every downstream repo and centralizes future fixes. The structural decisions (thin wrappers, workflow_call, secrets: inherit, control_plane_flow_ref input) are all sound.


Strengths

  • Security-conscious checkout strategy: deploy-review-app correctly separates the cpflow actions checkout (.cpflow) from PR app code (app), with rm -rf app/.git. The fork PR trust boundary is maintained.
  • Defensive shell scripting: Consistent set -euo pipefail, env-routed expressions, randomized heredoc delimiters for rollback state, openssl rand for safe multi-line GITHUB_OUTPUT.
  • Token/API hardening: The should_refresh_api_token? guard against non-JWT payloads and org_exists? nil-tolerance for scoped tokens are correct fixes, with new specs covering both.
  • Description field fix: GitHub expression syntax removed from composite action description: fields (issue 293), enforced by a new spec.
  • CPFLOW_GITHUB_ACTIONS_REF: Clean escape hatch for testing unreleased upstream changes without publishing a gem.

Issues and Suggestions

Medium: verify list_orgs nil semantics (inline comment on controlplane.rb:479)

Returning true when list_orgs returns nil is correct for scoped tokens that cannot enumerate orgs. But if nil is also returned on network/HTTP errors rather than raising, the org check would be silently bypassed for any transient API failure. Worth confirming ControlplaneApiDirect raises on network errors.

Low: CPFLOW_SOURCE_DIR path relies on implicit depth (inline comment on action.yml:61)

The ../../.. from github.action_path only works if the action stays exactly 3 levels deep. A runtime check for cpflow.gemspec at the resolved path would catch any future relocation immediately.

Low: secrets: inherit scope (inline comment on deploy-review-app wrapper:35)

secrets: inherit passes all repository secrets upstream. The callee only reads what it explicitly references so this is safe, but a brief comment in each generated wrapper helps downstream teams understand the trust delegation.

Nit: pull_request_target security rationale removed from wrapper (inline comment on delete-review-app:22)

The explanation for why pull_request_target is used was moved upstream. Downstream adopters reading the generated file will not find that context.

Nit: contents: write elevation in promote wrapper (inline comment on promote wrapper:12)

The wrapper grants contents: write at the workflow level even though the promote-to-production job only needs read. This is required by the GitHub permission model for workflow_call, but a comment explaining the elevation prevents future confusion.


Testing Gap

The PR notes the full RSpec suite was aborted. The generate_github_actions_spec.rb coverage is thorough for the generator output. The API changes (org_exists? nil path, should_refresh_api_token? non-JWT guard) touch the token/org validation path used across all commands - running the full suite before merge is recommended.


Summary

Architecturally sound, security posture well-maintained. The five items above are mostly documentation/defensive nits. The one substantive question before merge is confirming list_orgs nil semantics to ensure the org check bypass is strictly scoped to permission-denied responses and not broader API failures.

@justin808 justin808 force-pushed the jg-codex/reusable-github-actions branch 2 times, most recently from 432eb65 to 7575e7b Compare May 22, 2026 04:42
@claude

claude Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

Code Review

Overview

This PR is a well-structured architectural refactor. Moving all deploy/delete/staging/promotion logic upstream into shakacode/control-plane-flow reusable workflows and letting downstream repos carry only thin wrapper files is sound and significantly reduces the maintenance burden in generated repos.

The Ruby API hardening (ForbiddenError, Bearer prefix, token validation, safe parse_org) is all correct and adds meaningful robustness.


Security

Token validation loosening (controlplane_api_direct.rb:122): Replacing the old character-class regex with a newline-only rejection is the right boundary — it prevents HTTP header injection while allowing service-account token formats with :, +, /, =. Well-reasoned tradeoff.

ForbiddenError in org_exists?: The rescue is tight and correct. list_orgs calls exactly /org, so e.url == "/org" is not fragile. Any ForbiddenError on any other URL re-raises rather than being silently swallowed. Good.

pull_request_target trust boundary: The old template had an important inline comment explaining why pull_request_target is safe (doesn't checkout PR code; loads base-branch-trusted workflow code). That comment has moved into the upstream reusable workflow. Downstream maintainers reading the generated delete wrapper no longer see it. Worth adding a brief note in the template pointing to the upstream workflow and explaining the trust model.

secrets: inherit: All wrapper workflows pass every secret to the upstream reusable. This is necessary for the architecture but means any new downstream secret is implicitly available to the upstream workflow. Acceptable here, but worth documenting for adopters.


Architecture

Redundant if conditions: Template wrappers duplicate the triggering if condition that the upstream reusable workflow already guards internally. This is intentional (avoids unnecessary workflow_call invocations) but creates a maintenance surface where both copies must stay in sync. A comment calling this out would help future editors.

__CPFLOW_GITHUB_ACTIONS_REF__ passed twice: The uses: ref pins which version of the upstream workflow runs; control_plane_flow_ref tells that workflow which ref to use for checking out composite actions. They should always match. A drift between them (e.g. uses: ...@v1.2 but control_plane_flow_ref: main) would run v1 logic while loading main's composite actions. Consider adding a code comment in the templates explaining this invariant.

staging_app_branch_default as a quoted string: Clean improvement. The upstream workflow now owns the vars.STAGING_APP_BRANCH || inputs.staging_app_branch_default expression, keeping expression syntax out of both the template and composite action descriptions.


Bugs / Minor Issues

Temp gem file not cleaned up (cpflow-setup-environment/action.yml:83-88): mktemp creates a file that persists after the step on self-hosted runners. See inline comment.

contents: write in promote template: Correct — GitHub caps called-workflow permissions at what the caller grants, so the wrapper must grant contents: write for the upstream's create-github-release job. Worth a comment in the template that the elevated permission exists for release creation, not promotion itself.


Tests

Spec updates are thorough. The new shared_yaml_paths helper, the YAML-expression-in-description guard (issue #293), and the token validation tests are all good additions. The focused coverage is reasonable given the scope.


Summary

Solid refactor with correct security handling throughout. Three items worth addressing before merge:

  1. Clean up the temp gem file in cpflow-setup-environment (minor, self-hosted runner concern)
  2. Add a comment in wrapper templates explaining the duplicated if and the uses:/control_plane_flow_ref invariant
  3. Consider restoring a brief pull_request_target security note in the generated delete-review-app wrapper

cd "${cpflow_source_dir}"
gem build cpflow.gemspec --output "${cpflow_gem}"
)
gem install "${cpflow_gem}" --no-document

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 temp file created by mktemp is never removed. On GitHub-hosted runners this is harmless (the VM is discarded), but on self-hosted runners it leaks a gem file per workflow run. Add a trap to clean up on exit:

Suggested change
gem install "${cpflow_gem}" --no-document
trap 'rm -f "${cpflow_gem}"' EXIT
gem install "${cpflow_gem}" --no-document

comment_id: commentId,
body
});
uses: shakacode/control-plane-flow/.github/workflows/cpflow-delete-review-app.yml@__CPFLOW_GITHUB_ACTIONS_REF__

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 if: guard duplicates the condition from the upstream reusable workflow. This is intentional — it prevents a billable workflow_call invocation when the event doesn't match — but it means both copies must stay in sync when trigger conditions change. A short comment here would help future editors understand the pattern:

Suggested change
uses: shakacode/control-plane-flow/.github/workflows/cpflow-delete-review-app.yml@__CPFLOW_GITHUB_ACTIONS_REF__
# This `if:` mirrors the upstream workflow's own job guard.
# Its purpose is to short-circuit the workflow_call and avoid
# burning billable minutes when the event doesn't match.
# Keep this condition in sync with cpflow-delete-review-app.yml@upstream.
uses: shakacode/control-plane-flow/.github/workflows/cpflow-delete-review-app.yml@__CPFLOW_GITHUB_ACTIONS_REF__

Comment thread lib/core/controlplane_api_direct.rb Outdated
org = ControlplaneApiDirect.parse_org(url)
message =
if org
"Double check your org #{org}. #{response} #{response.body}"

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 error message includes the raw response.body, which may contain sensitive CP API error payloads (internal identifiers, partial token context, etc.). Consider omitting or truncating the body, or at minimum stripping it from the message surfaced to users:

Suggested change
"Double check your org #{org}. #{response} #{response.body}"
message =
if org
"Double check your org #{org}. #{response}"
else
"Control Plane API request to #{url} was forbidden. #{response}"
end

Comment thread lib/command/generate_github_actions.rb Outdated
# expression literal cannot be broken by the generated branch name.
"${{ vars.STAGING_APP_BRANCH || '#{staging_branch}' }}"
def cpflow_github_actions_ref
ENV.fetch("CPFLOW_GITHUB_ACTIONS_REF", "main")

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.

Defaulting to main means every downstream repo that omits CPFLOW_GITHUB_ACTIONS_REF always fetches the tip of main at CI time. A breaking upstream change silently breaks all callers. Consider using "v#{Cpflow::VERSION}" as the default so generated wrappers pin to the released version they were generated with, and let users opt in to tracking main.

cd "${cpflow_source_dir}"
gem build cpflow.gemspec --output "${cpflow_gem}"
)
gem install "${cpflow_gem}" --no-document

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 temp .gem file is installed but never deleted. Since runners are ephemeral this does not leak, but adding rm -f "${cpflow_gem}" here keeps /tmp tidy and avoids confusion when inspecting failed runs.

Comment thread lib/core/controlplane.rb Outdated
items = result.fetch("items", [])
items.any? { |item| item["name"] == org }
rescue ControlplaneApiDirect::ForbiddenError => e
raise unless e.url == "/org"

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 string "/org" is tightly coupled to ControlplaneApi#list_orgs which calls api_json("/org", ...). If that endpoint path ever changes, this rescue clause silently stops matching and callers see the raw 403 message instead. Consider extracting LIST_ORGS_PATH = "/org" as a constant shared between the API layer and this check, or at minimum add an inline comment linking the two.

# Don't cancel an in-flight promotion: a half-finished `cpflow deploy-image` plus a
# rollback can leave production in a worse state than letting the first run finish.
cancel-in-progress: false
contents: write

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 contents: write grant is at the workflow level (not the job level) because GitHub's reusable-workflow permission model requires the caller to grant at least as much permission as the called workflow's most permissive job. The create-github-release job inside the upstream reusable workflow needs contents: write, so this wrapper cannot scope it to a single job. A comment here would help future maintainers avoid accidentally narrowing it back to read.

@claude

claude Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

Code Review: Move generated GitHub Actions logic upstream

Overview

This is a well-structured refactor that moves the heavy workflow logic from generated downstream files into upstream reusable workflows and composite actions in this repository. The thin-wrapper pattern is sound: downstream repos now contain only event triggers, permissions, and a single uses: call, which dramatically reduces generated code churn and makes future improvements available to all users by updating upstream.


Positives

  • Typed ForbiddenError — replacing the raw string raise with a structured exception makes it possible to catch and act on 403s selectively (as in org_exists?).
  • parse_org nil safetyurl.match(...)&.[](1) prevents a NoMethodError when the URL does not contain an org path segment (e.g., /org).
  • Token validation relaxation — replacing the strict regex with a newline-only rejection correctly handles service-account token formats without false positives.
  • working_directory input — fixes Ruby version detection when the downstream app is checked out into a subdirectory (e.g., app/).
  • Build from source by default — a smart approach for testing upstream PRs without publishing a release; the CPFLOW_VERSION escape hatch for pinning a RubyGems version is clean.
  • Randomised heredoc delimiter in the rollback-state capture step prevents injection when rollback JSON contains a literal EOF line.
  • Env-routing pattern — consistently routing ${{ ... }} values through env vars before using them in shell scripts is good practice and quiets security scanners.

Issues to address

1. Default main ref is a mutable moving target (medium risk)

cpflow_github_actions_ref defaults to main. Every downstream repo that does not set CPFLOW_GITHUB_ACTIONS_REF will silently pull whichever commit happens to be at main when their CI runs. A breaking change pushed upstream immediately affects all users. Consider defaulting to the current gem version tag (e.g., v#{Cpflow::VERSION}) rather than main, with main available as an opt-in.

2. contents: write at workflow level in the promote template

lib/github_flow_templates/.github/workflows/cpflow-promote-staging-to-production.yml now declares permissions: contents: write at the workflow level. This is required for create-github-release inside the upstream reusable workflow, but it is broader than the previous per-job grant. Worth adding a comment so future maintainers understand it cannot be narrowed back to the job level.

3. Tempfile not cleaned up after gem install

In cpflow-setup-environment/action.yml, the gem built via mktemp is installed but the .gem file is left in /tmp. Runners are ephemeral so this does not leak, but an explicit rm -f "${cpflow_gem}" after the install call would be cleaner.

4. Duplicate if: conditions create a maintenance burden

The downstream templates (cpflow-delete-review-app.yml, cpflow-deploy-review-app.yml, cpflow-help-command.yml) each duplicate the same if: conditions that also appear on the corresponding job in the upstream reusable workflow. When called via workflow_call, GitHub surfaces the caller event name inside the reusable workflow, so the upstream guard is live. Any future change to supported event types must be updated in both places. A comment in each template noting 'keep in sync with upstream job if:' would reduce future confusion.

5. ForbiddenError URL comparison hardcodes the list-orgs endpoint

raise unless e.url == "/org" works because list_orgs calls api_json("/org", ...) today. If the endpoint ever changes, the rescue silently stops matching. Extracting a constant or adding a comment tying this string to the API call would help.

6. Full test suite not confirmed green

The PR description notes the full RSpec suite was stopped after it hung. Before merging, it would be worth confirming no regressions in spec/core/controlplane* tests, particularly around org_exists? and the token-validation changes.


Summary

The architecture is solid and the security posture (env-routing, persist-credentials: false, pull_request_target trust model, fork-PR gating) is well-handled. Items 1 and 6 are the most important to resolve before merge; the rest are low-risk quality improvements.

@justin808 justin808 force-pushed the jg-codex/reusable-github-actions branch from 7575e7b to 547060f Compare May 22, 2026 05:52
@claude

claude Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

Code Review

Overview

This PR moves the heavy GitHub Actions deployment/cleanup/promote logic from generated downstream files into reusable upstream workflows in this repo, producing thin wrapper workflows that delegate to shakacode/control-plane-flow. The architecture is sound and the reduction in generated code is significant. There are also useful Ruby API hardening changes covering ForbiddenError, token validation, and JWT safety.


Bug: cleanup-stale-apps is missing the consumer repo checkout

cpflow cleanup-stale-apps reads stale_app_image_deployed_days and match_if_app_name_starts_with from the consumer's .controlplane/controlplane.yml. The upstream cleanup workflow only checks out shakacode/control-plane-flow into .cpflow; the consumer's application repository (containing .controlplane/) is never checked out. Running cpflow cleanup-stale-apps without that config file will fail at config load time.

The staging and promote workflows correctly check out the caller's repo with a bare actions/checkout (in a workflow_call context this checks out the calling repository). The cleanup workflow needs the same step before running cpflow.


Concern: secrets: inherit with mutable version tags

All generated wrapper templates use secrets: inherit, which passes all downstream repository secrets to the upstream reusable workflow. The version tag (e.g. v1.2.3) is mutable and can be force-moved. If the tag is ever pointed at a commit that references additional secret names, those secrets would be silently exfiltrated. The risk requires compromising this repo or its tag infrastructure, but is worth documenting in the generated workflow comment so downstream users understand the trust model. Consider recommending that security-sensitive users pin to an immutable commit SHA via CPFLOW_GITHUB_ACTIONS_REF.


Minor: ruby/setup-ruby@v1 is a floating tag

In .github/actions/cpflow-setup-environment/action.yml, all other third-party actions are pinned to immutable SHAs, but ruby/setup-ruby@v1 remains a floating tag. The inline comment explains this is intentional. That is reasonable, but since the old SHA-pinning rationale comment was reworded in this PR, a brief note explaining why ruby/setup-ruby is exempt (it is not GitHub-owned, and major-tag tracking is valuable for Ruby/toolcache updates) would help reviewers understand the policy.


Positive: Ruby API hardening

  • ForbiddenError without body leakage: the old raise included response.body which could expose internal Control Plane IDs. The new ForbiddenError uses only response.to_s. Good fix.
  • Token validation relaxed correctly: replacing the strict regex with a simple no-newline check correctly supports service-account tokens that include /, +, :, =.
  • JWT expiry guard: return false unless payload.is_a?(Hash) && payload["exp"] prevents a TypeError for JWTs without an exp claim. The .to_i coercion is also safer.
  • parse_org nil-safety: safe-navigation prevents a NoMethodError for non-matching URLs. New spec coverage confirms this.
  • org_exists? scoped-token tolerance: the ForbiddenError rescue keyed on LIST_ORGS_PATH correctly lets scoped tokens proceed to their target API call. Two new specs validate both the nil-return and 403 paths.

Testing

Test coverage for the generator is thorough and specs correctly validate both the generated wrappers and the upstream shared workflows. Note that the full RSpec suite was not completed per the PR description; a full run before merge would be prudent.

cpln_cli_version: ${{ vars.CPLN_CLI_VERSION }}
cpflow_version: ${{ vars.CPFLOW_VERSION }}

- name: Remove stale review apps

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: consumer repo is never checked out.

cpflow cleanup-stale-apps calls into lib/command/cleanup_stale_apps.rb, which reads config[:stale_app_image_deployed_days] and match_if_app_name_starts_with from .controlplane/controlplane.yml. Without that file on disk the command fails at config load time.

The staging and promote reusable workflows both have a bare actions/checkout step (no repository:) that, in a workflow_call context, checks out the caller's repository. The cleanup workflow should do the same. Suggested addition before the "Remove stale review apps" step:

      - name: Checkout caller repository
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
        with:
          persist-credentials: false

Without this, every scheduled cleanup run will fail silently with a config-not-found error.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Confirmed. cleanup-stale-apps loads .controlplane/controlplane.yml, so the cleanup reusable workflow needs a caller-repo checkout before Remove stale review apps. I’ll add a pinned caller checkout there while keeping the .cpflow checkout for shared actions.

# reusable workflows pin GitHub-owned actions to immutable SHAs.
steps:
- name: Set up Ruby
uses: ruby/setup-ruby@v1

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.

ruby/setup-ruby@v1 is a floating major tag, unlike every other third-party action in the privileged workflows which are pinned to immutable SHAs. The comment on line 42 notes this is intentional, but the previous comment that explained the full SHA-pinning rationale was removed in this PR. Consider adding a one-liner here to explain the exemption, e.g.:

      # ruby/setup-ruby is exempt from SHA pinning: major-tag tracking is
      # intentional so Ruby and toolcache updates roll out automatically.
      # It is not a GitHub-owned action, so Dependabot/Renovate can manage it.
      uses: ruby/setup-ruby@v1

This makes the policy legible for future reviewers without relying on the git history.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Confirmed. The workflow-level comment captures the policy, but the exemption is clearer if it sits directly above ruby/setup-ruby@v1. I’ll add an inline note that major-tag tracking is intentional for Ruby/toolcache updates.

control_plane_flow_ref: __CPFLOW_GITHUB_ACTIONS_REF__
# Delegates repository secrets to the trusted upstream cpflow workflow; it
# only references the named secrets it needs.
secrets: inherit

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.

secrets: inherit delegates all of the downstream repo's secrets to the upstream reusable workflow. Combined with a mutable version tag (@v1.2.3 can be force-moved), this creates a supply-chain trust dependency: if this repo's tag infrastructure is ever compromised, every downstream secret is exposed.

This is an accepted tradeoff for usability (many popular Actions do the same), but the comment above only describes the functional purpose. Consider adding an explicit note about the trust boundary, e.g.:

    # secrets: inherit passes all downstream repository secrets to the upstream
    # reusable workflow. The upstream workflow only reads the named secrets it
    # declares, but `inherit` cannot enforce that boundary at the GitHub level.
    # Users with strict supply-chain requirements should set CPFLOW_GITHUB_ACTIONS_REF
    # to an immutable commit SHA instead of a version tag.
    secrets: inherit

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Confirmed. The current generated-wrapper comment explains which secrets the upstream workflow reads, but it does not state the GitHub-level trust boundary. I’ll expand it to call out that secrets: inherit grants all caller secrets to the trusted reusable workflow and that strict consumers can pin CPFLOW_GITHUB_ACTIONS_REF to an immutable SHA.

@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: 3

🤖 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/workflows/cpflow-deploy-staging.yml:
- Around line 102-109: The Setup environment step uses the local action
./.cpflow/.github/actions/cpflow-setup-environment but doesn't set
working_directory, so Ruby/toolchain discovery may pick the repo root; add
working_directory: .cpflow to the step that has name: Setup environment and
uses: ./.cpflow/.github/actions/cpflow-setup-environment (apply the same change
to the other identical job near the other occurrence mentioned) so the action
runs from the checked-out .cpflow directory.

In @.github/workflows/cpflow-promote-staging-to-production.yml:
- Around line 98-126: The workflow resolves app_workloads into the variable
workloads but does not enforce a single effective primary workload or validate a
provided PRIMARY_WORKLOAD; update the Ruby block (where PRODUCTION_APP_NAME,
app_config, and app_workloads are used and the workloads variable is produced)
to: 1) parse app_config["app_workloads"] into an ordered list, 2) if the env
PRIMARY_WORKLOAD is set, validate it is present in that list and fail fast with
a clear error if not, 3) if PRIMARY_WORKLOAD is blank, select a single primary
workload deterministically (e.g., the only entry or fail if multiple to force
explicit selection), and 4) emit that single primary workload to GITHUB_OUTPUT
(e.g., names or primary_workload) so subsequent steps use the resolved output
instead of falling back to `${{ env.PRIMARY_WORKLOAD || 'rails' }}`; reference
the workloads-producing code and the echo "names=${workloads}" >>
"$GITHUB_OUTPUT" line when making the change.

In `@lib/command/generate_github_actions.rb`:
- Around line 65-68: The cpflow_github_actions_ref method currently only strips
edges allowing internal whitespace like "feature branch" which later breaks the
generated uses: ...@ref; update cpflow_github_actions_ref to validate the
resolved ref (ENV.fetch("CPFLOW_GITHUB_ACTIONS_REF",
default_cpflow_github_actions_ref).to_s) and if it is blank or contains any
whitespace characters (e.g. ref.match?(/\s/)) raise a clear ArgumentError or
logging+raise indicating an invalid CPFLOW_GITHUB_ACTIONS_REF, otherwise return
the validated ref (falling back to default_cpflow_github_actions_ref when
appropriate).
🪄 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: e5f1e2cc-d42b-4556-9ad5-0f51b83bafa8

📥 Commits

Reviewing files that changed from the base of the PR and between 25e80af and 547060f.

📒 Files selected for processing (31)
  • .github/actions/cpflow-build-docker-image/action.yml
  • .github/actions/cpflow-delete-control-plane-app/action.yml
  • .github/actions/cpflow-delete-control-plane-app/delete-app.sh
  • .github/actions/cpflow-detect-release-phase/action.yml
  • .github/actions/cpflow-setup-environment/action.yml
  • .github/actions/cpflow-validate-config/action.yml
  • .github/actions/cpflow-wait-for-health/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-help-command.yml
  • .github/workflows/cpflow-promote-staging-to-production.yml
  • .github/workflows/cpflow-review-app-help.yml
  • docs/ai-github-flow-prompt.md
  • docs/ci-automation.md
  • docs/commands.md
  • lib/command/generate_github_actions.rb
  • lib/core/controlplane.rb
  • lib/core/controlplane_api.rb
  • lib/core/controlplane_api_direct.rb
  • lib/github_flow_templates/.github/workflows/cpflow-cleanup-stale-review-apps.yml
  • lib/github_flow_templates/.github/workflows/cpflow-delete-review-app.yml
  • lib/github_flow_templates/.github/workflows/cpflow-deploy-review-app.yml
  • lib/github_flow_templates/.github/workflows/cpflow-deploy-staging.yml
  • lib/github_flow_templates/.github/workflows/cpflow-help-command.yml
  • lib/github_flow_templates/.github/workflows/cpflow-promote-staging-to-production.yml
  • lib/github_flow_templates/.github/workflows/cpflow-review-app-help.yml
  • spec/command/generate_github_actions_spec.rb
  • spec/core/controlplane_api_direct_spec.rb
  • spec/core/controlplane_spec.rb
✅ Files skipped from review due to trivial changes (2)
  • docs/commands.md
  • docs/ci-automation.md

Comment thread .github/workflows/cpflow-deploy-staging.yml
Comment thread .github/workflows/cpflow-promote-staging-to-production.yml Outdated
Comment thread lib/command/generate_github_actions.rb
@justin808 justin808 force-pushed the jg-codex/reusable-github-actions branch from 547060f to 2db67ab Compare May 22, 2026 08:31
@claude

claude Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

Code Review: PR 305 - Move Generated GitHub Actions Logic Upstream

Overview: This PR moves heavy GitHub Actions deployment logic from generated downstream repos into upstream reusable workflows. Generated repos get thin wrappers delegating to reusable workflows via secrets: inherit. The architecture eliminates duplicated workflow code across all consumer repos. The Ruby API-hardening changes (scoped token support, ForbiddenError, Bearer normalization) are solid and well-tested.

Security - secrets: inherit trust boundary (medium risk, acknowledged)

All six wrapper templates pass secrets: inherit to upstream reusable workflows. GitHub does not enforce that only referenced secrets are forwarded; the entire caller secret store is passed. Two concerns: (1) The default ref is v which is a mutable tag; republishing that tag could redirect all consumers to different code. SHA-pinning guidance in the generated comment is the right advice. (2) The promote-to-production template grants contents: write to the caller workflow. Combined with secrets: inherit, a compromised upstream ref gets write access to the caller repo AND all its secrets. The generated comment should explicitly note that contents: write is required specifically for the create-github-release job.

Security - ForbiddenError no longer leaks response body (good)

Previously response.body was included in the raised message. The new ForbiddenError only interpolates response.to_s (the HTTP status line), never response.body. CPln 403 bodies sometimes contain internal tracking IDs, so this is the right call.

Security - Token validation relaxed correctly

Removing API_TOKEN_REGEX and accepting any non-empty token without line breaks correctly supports scoped service account tokens with special characters. Format validation is correctly deferred to the CP API.

Potential Issue 1 - Manual ref-sync in generated wrappers

Every generated wrapper has both the uses: ref and control_plane_flow_ref set to the same value, and both must stay in sync manually. If a downstream user updates one but not the other, they silently run the wrong composite action version. The comment warns about this but does not enforce it. Consider having the upstream workflow echo both refs so a mismatch is immediately visible in the run log.

Potential Issue 2 - cpflow cleanup-stale-apps has no explicit working-directory

In .github/workflows/cpflow-cleanup-stale-review-apps.yml, the caller repo is checked out with no path:, landing at GITHUB_WORKSPACE. The cpflow step finds .controlplane/ correctly. However, without an explicit working-directory: directive, if a future commit adds path: app to that checkout step (mirroring the review-app workflow), the cpflow step would silently break.

Potential Issue 3 - staging_branch_default relies on empty-string falsy behaviour

When both vars.STAGING_APP_BRANCH and inputs.staging_app_branch_default are empty, the || expression evaluates to empty string. The downstream shell check correctly falls through to main/master. This works but a brief comment would make the intent clearer for future maintainers.

Code Quality

parse_org nil-safety is a good defensive fix -- the old code raised NoMethodError on /org (the list-orgs path itself), exactly the URL now handled in org_exists?. The is_a?(Hash) and payload exp guard with .to_i handles non-JWT service account tokens that decode without an exp claim. The authorization_header case-insensitive Bearer prefix check is correct. The cpflow_github_actions_ref whitespace guard aborts before writing invalid YAML refs. The ForbiddenError URL-equality check in org_exists? avoids fragile message-text matching. The new primary workload resolution in the promote workflow (explicit error when ambiguous rather than silently falling back to first_image) is a meaningful correctness improvement.

Tests

Generator specs are well-updated to cover the wrapper/reusable split. ForbiddenError, authorization_header, and token-validation tests are complete and correctly scoped. The acknowledgment that the full RSpec suite was not run to completion is notable -- the promote and cleanup workflows have non-trivial shell logic that benefits from integration coverage.

when Net::HTTPForbidden
org = self.class.parse_org(url)
raise("Double check your org #{org}. #{response} #{response.body}")
raise ForbiddenError.new(url: url, response: response)

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: response here is the Net::HTTPForbidden object, so string interpolation calls response.to_s which produces the status line (e.g. "403 Forbidden"), not response.body. This deliberately prevents raw 403 bodies -- which can contain internal CPln tracking IDs -- from appearing in logs or error messages. The test in controlplane_api_direct_spec.rb verifies this behaviour explicitly.

Comment thread lib/core/controlplane.rb
items = result.fetch("items", [])
items.any? { |item| item["name"] == org }
rescue ControlplaneApiDirect::ForbiddenError => e
raise unless e.url == ControlplaneApi::LIST_ORGS_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.

This URL-equality check (e.url == LIST_ORGS_PATH) is the right approach -- it re-raises any ForbiddenError from other endpoints (e.g. a GVC or workload API call) while silently treating a 403 on the list-orgs endpoint as "token is scoped, proceed". Using the URL rather than matching on message text means this won't silently swallow unrelated 403s if error messages ever change.

gem install cpflow -v "${CPFLOW_VERSION}" --no-document
else
cpflow_source_dir="$(cd "${CPFLOW_SOURCE_DIR}" && pwd)"
if [[ ! -f "${cpflow_source_dir}/cpflow.gemspec" ]]; 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.

This path traversal is correct given the action's location at .github/actions/cpflow-setup-environment/ -- navigating up three levels reaches the repo root, where cpflow.gemspec lives. The cpflow.gemspec existence check on line 102 provides the only validation of this path. Worth noting: this path is evaluated at runner time from github.action_path, so it's stable across runner OS versions. If this action were ever moved to a different nesting depth, this ../../.. would need to be updated.

shell: bash
run: |
set -euo pipefail
cpflow cleanup-stale-apps -a "${REVIEW_APP_PREFIX}" --org "${CPLN_ORG_STAGING}" --yes

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 caller repo checkout above (line 58) has no path:, so it lands at $GITHUB_WORKSPACE and cpflow correctly finds .controlplane/controlplane.yml. However, this step has no working-directory: directive. If a future change adds path: app to the checkout step (to mirror the review-app workflow), this step would silently run from the wrong directory and fail to find the app config. Suggest adding working-directory: ${{ github.workspace }} to make the intent explicit.

env:
APP_NAME: ${{ vars.STAGING_APP_NAME }}
CPLN_ORG: ${{ vars.CPLN_ORG_STAGING }}
STAGING_APP_BRANCH: ${{ vars.STAGING_APP_BRANCH || inputs.staging_app_branch_default }}

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 both vars.STAGING_APP_BRANCH and inputs.staging_app_branch_default are empty/unset, this expression evaluates to "". The downstream if [[ -n "${STAGING_APP_BRANCH}" ]] shell check correctly falls through to the main/master branch logic. This works because GitHub Actions treats empty strings as falsy in || expressions -- a brief inline comment on this line would make the intent clearer for future maintainers.

cancel-in-progress: false
# The upstream reusable workflow's create-github-release job needs
# contents: write, and callers must grant the union of callee permissions.
contents: write

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 comment is correct but could be more explicit about the risk. This is the only generated wrapper that requires contents: write. Combined with secrets: inherit, it means a compromised or misconfigured upstream ref (e.g. if v<VERSION> tag is republished) would have write access to both the caller repo's git contents and its entire secret store. Consider adding a note that this elevated permission scope is solely required for gh release create in the upstream create-github-release job, so downstream operators know exactly which upstream job drives this requirement.

@justin808 justin808 merged commit 8e9c0c5 into main May 22, 2026
21 checks passed
@justin808 justin808 deleted the jg-codex/reusable-github-actions branch May 22, 2026 08:41
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