Skip to content

Add changelog-driven application deployments to reflex-release - #7146

Open
masenf wants to merge 1 commit into
mainfrom
codex/changelog-app-deployments
Open

masenf wants to merge 1 commit into
mainfrom
codex/changelog-app-deployments

Conversation

@masenf

@masenf masenf commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

Application deployments need the same reviewed changelog and post-publish tagging flow as package releases. Add an opt-in [tool.reflex-release.app] mode that generates the orchestration while repository-owned reusable workflows handle builds and deployments. Existing package publishing keeps its current templates.

Dispatch accepts an optional source revision and reason, pins a configured Git submodule, allocates an ISO-year.week.sequence version, and opens a release PR. After merge, the generated workflow builds the captured commit, queues staging through production approval, and creates the tag and GitHub release only after the production hook succeeds. Existing tags make detection a no-op. Optional scheduled dev builds resolve upstream independently without committing release metadata.

The app mode documents and validates hook inputs, checks generated workflow drift, reserves versions across pending release branches, and rejects stale releases. Includes user documentation and a news fragment.

Type of change

  • New feature (non-breaking change which adds functionality)
  • Documentation and tests included

Validation

  • All 483 reflex-release tests pass.
  • Commit hooks pass: Ruff format/check, codespell, pyi generation, Pyright and ty.
  • Generated workflows pass sync --check for both package and app configurations.
  • Docs consumer hook execution tests pass with local Git fixtures and fake services.

Workflow validation from implementation passed except for actionlint 1.7.12's unsupported concurrency.queue key; queue: max is documented by GitHub and is intentionally retained. No live deployment was run.

Companion consumer migration: https://github.com/reflex-dev/reflex-docs/pull/27. That PR stays draft until this support is published in the pinned reflex-release version.

@masenf
masenf requested a review from a team as a code owner September 14, 2026 23:20
@greptile-apps

greptile-apps Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 3/5

The PR is not safe to merge until app release headings are protected from ordinary pull requests and private cross-repository submodules have a functional credential path.

Findings

  1. P1 Manual headings trigger deployments
  2. P1 Private submodule checkout fails
  3. P2 Hook types remain unchecked

Summary

This PR adds an opt-in application deployment mode to reflex-release, including app-specific configuration, ISO week-based release allocation, source-submodule pinning, generated build/deployment orchestration, and documentation and tests.

  • Generates dispatch, changelog validation, queued promotion, publication, and optional development deployment workflows.
  • Reuses existing tag and GitHub release helpers after successful production deployment.
  • Adds validation for repository-owned build/deploy hooks and app-specific configuration.
  • The current implementation needs heading-provenance protection and a usable credential path for private cross-repository submodules.

Reviews (1) · Last reviewed commit: "Add changelog-driven application deploym..."

persist-credentials: false
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
@@UV_SETUP_WITH@@
- run: @@CLI@@ sync --check

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.

P1 Manual headings trigger deployments

This app-mode pull request workflow only runs sync --check. An ordinary pull request can therefore add a valid ## YYYY.W.S heading without using the release dispatch flow. After that pull request is merged, release_from_changelog.yml treats the heading as an unpublished release and starts the staging-to-production deployment pipeline. Add the existing heading-provenance check, adapted for app release branches, so hand-written headings cannot initiate deployments.

Knowledge Base Used:

Comment on lines +35 to +40
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: "@@MAIN_BRANCH@@"
fetch-depth: 0
persist-credentials: false
submodules: recursive

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.

P1 Private submodule checkout fails

If the source is a private cross-repository submodule, recursive checkout uses the action's default repository token before app-materialize can install the GitHub CLI credential helper. The checkout then fails before the release PR can be created. The generated configuration also has no token or secret setting through which consumers can provide the appropriately scoped credential described in the README. The same issue affects the recursive checkout in deploy_dev.yml.

Comment on lines +807 to +811
missing = inputs - workflow_call_inputs(target.read_text(encoding="utf-8"))
if missing:
fail(
f"app workflow {workflow} must declare workflow_call inputs: {', '.join(sorted(missing))}"
)

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.

P2 Hook types remain unchecked

App hook validation checks only whether the expected input names exist, even though the documented contract requires string inputs. A hook can declare version, ref, or environment as an incompatible type and still pass sync --check, leaving the generated reusable-workflow call to fail when GitHub validates or executes it. Parse and verify each input's type: string contract instead of comparing names alone.

Knowledge Base Used: Release command and workflow automation

@codspeed

codspeed Bot commented Sep 14, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 40 untouched benchmarks
⏩ 8 skipped benchmarks1


Comparing codex/changelog-app-deployments (a0a1a9e) with main (de63f5d)

Open in CodSpeed

Footnotes

  1. 8 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@cubic-dev-ai cubic-dev-ai Bot left a comment

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.

6 issues found across 16 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/reflex-release/src/reflex_release/scaffold.py">

<violation number="1" location="packages/reflex-release/src/reflex_release/scaffold.py:807">
P2: When an app hook declares a required input with a non-string or missing `type`, `sync --check` still passes because it checks names only, and the release then fails when GitHub validates the reusable-workflow call. Validate each hook's input type (and required declaration) against the documented contract before accepting it.</violation>
</file>

<file name="packages/reflex-release/src/reflex_release/config.py">

<violation number="1" location="packages/reflex-release/src/reflex_release/config.py:948">
P2: When an app repository retains package-only settings such as `changelog-exempt-packages`, `_load_app` accepts them even though app workflows ignore them. Reject all package-only keys consistently to enforce the documented app-versus-package configuration boundary.</violation>

<violation number="2" location="packages/reflex-release/src/reflex_release/config.py:1077">
P2: When app configuration contains an invalid `release-timezone`, `app-materialize` raises an uncaught `ZoneInfoNotFoundError` instead of the CLI’s normal configuration error. Validate the timezone while loading app configuration so the workflow fails clearly before allocation.

(Based on your team's feedback about validating release configuration early.)</violation>
</file>

<file name="packages/reflex-release/src/reflex_release/templates/workflows/app/changelog.yml">

<violation number="1" location="packages/reflex-release/src/reflex_release/templates/workflows/app/changelog.yml:4">
P2: The app changelog check only runs `sync --check`; unlike the package `changelog.yml`, it has no guard rejecting hand-written CHANGELOG version headings. A PR that hand-adds an untagged `YYYY.W.sequence` heading passes this check and, once merged to `main`, `release_from_changelog.yml` → `app-detect` treats it as a release and automatically deploys to staging (production still requires approval). Add the same heading rejection (and the release-branch/author exemption) to the app workflow if the 'same reviewed changelog flow' claim is to hold.</violation>
</file>

<file name="tests/units/reflex_release/test_app.py">

<violation number="1" location="tests/units/reflex_release/test_app.py:94">
P2: These capsys assertions are fragile: `write_outputs`/`write_summary` (actions._append_lines) write to stdout only when `$GITHUB_OUTPUT`/`$GITHUB_STEP_SUMMARY` are unset, and otherwise append to those files leaving stdout empty. GitHub Actions sets GITHUB_OUTPUT for the whole job, so in the project's CI every `capsys.readouterr().out` here (this detect test, `test_detect_empty_and_stale`, `test_dev_never_changes_changelog_or_tags`, `test_app_cli_materialize_and_detect`) would assert against empty output and fail. The rest of the suite avoids this via the `outputs`/`summary` fixtures that monkeypatch GITHUB_OUTPUT to a temp file; these app tests should do the same instead of depending on the variable being absent.</violation>
</file>

<file name="packages/reflex-release/README.md">

<violation number="1" location="packages/reflex-release/README.md:1073">
P2: Private cross-repository submodules cannot use the documented app configuration because generated checkouts expose no scoped token or SSH credential. Add a configurable checkout credential to the generated workflows, or document private cross-repository submodules as unsupported.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

fail(
f"app workflow {workflow} must be an existing, repository-owned workflow"
)
missing = inputs - workflow_call_inputs(target.read_text(encoding="utf-8"))

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.

P2: When an app hook declares a required input with a non-string or missing type, sync --check still passes because it checks names only, and the release then fails when GitHub validates the reusable-workflow call. Validate each hook's input type (and required declaration) against the documented contract before accepting it.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/reflex-release/src/reflex_release/scaffold.py, line 807:

<comment>When an app hook declares a required input with a non-string or missing `type`, `sync --check` still passes because it checks names only, and the release then fails when GitHub validates the reusable-workflow call. Validate each hook's input type (and required declaration) against the documented contract before accepting it.</comment>

<file context>
@@ -768,8 +794,24 @@ def sync(config: Config, check: bool = False, force: bool = False) -> None:
+                fail(
+                    f"app workflow {workflow} must be an existing, repository-owned workflow"
+                )
+            missing = inputs - workflow_call_inputs(target.read_text(encoding="utf-8"))
+            if missing:
+                fail(
</file context>

"never-publish-packages",
"latest-release-package",
"post-release-workflow",
"tag-prefix",

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.

P2: When an app repository retains package-only settings such as changelog-exempt-packages, _load_app accepts them even though app workflows ignore them. Reject all package-only keys consistently to enforce the documented app-versus-package configuration boundary.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/reflex-release/src/reflex_release/config.py, line 948:

<comment>When an app repository retains package-only settings such as `changelog-exempt-packages`, `_load_app` accepts them even though app workflows ignore them. Reject all package-only keys consistently to enforce the documented app-versus-package configuration boundary.</comment>

<file context>
@@ -849,6 +877,85 @@ def _default_root_source_dirs(root: Path, root_package: str | None) -> tuple[str
+                "never-publish-packages",
+                "latest-release-package",
+                "post-release-workflow",
+                "tag-prefix",
+            )
+        )
</file context>

)

if "app" in table:
return dataclasses.replace(

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.

P2: When app configuration contains an invalid release-timezone, app-materialize raises an uncaught ZoneInfoNotFoundError instead of the CLI’s normal configuration error. Validate the timezone while loading app configuration so the workflow fails clearly before allocation.

(Based on your team's feedback about validating release configuration early.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/reflex-release/src/reflex_release/config.py, line 1077:

<comment>When app configuration contains an invalid `release-timezone`, `app-materialize` raises an uncaught `ZoneInfoNotFoundError` instead of the CLI’s normal configuration error. Validate the timezone while loading app configuration so the workflow fails clearly before allocation.

(Based on your team's feedback about validating release configuration early.) </comment>

<file context>
@@ -966,6 +1073,11 @@ def load_config(root: Path) -> Config:
         )
 
+    if "app" in table:
+        return dataclasses.replace(
+            config, app=_load_app(table, towncrier), packages_dir=None
+        )
</file context>

@@ -0,0 +1,20 @@
@@HEADER@@

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.

P2: The app changelog check only runs sync --check; unlike the package changelog.yml, it has no guard rejecting hand-written CHANGELOG version headings. A PR that hand-adds an untagged YYYY.W.sequence heading passes this check and, once merged to main, release_from_changelog.ymlapp-detect treats it as a release and automatically deploys to staging (production still requires approval). Add the same heading rejection (and the release-branch/author exemption) to the app workflow if the 'same reviewed changelog flow' claim is to hold.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/reflex-release/src/reflex_release/templates/workflows/app/changelog.yml, line 4:

<comment>The app changelog check only runs `sync --check`; unlike the package `changelog.yml`, it has no guard rejecting hand-written CHANGELOG version headings. A PR that hand-adds an untagged `YYYY.W.sequence` heading passes this check and, once merged to `main`, `release_from_changelog.yml` → `app-detect` treats it as a release and automatically deploys to staging (production still requires approval). Add the same heading rejection (and the release-branch/author exemption) to the app workflow if the 'same reviewed changelog flow' claim is to hold.</comment>

<file context>
@@ -0,0 +1,20 @@
+@@HEADER@@
+name: Check release workflows
+
+on:
+  pull_request:
+  workflow_dispatch:
</file context>

version = app.materialize(app_repo, "", "Release")
capsys.readouterr()
app.detect(app_repo, "main")
assert "any=true" in capsys.readouterr().out

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.

P2: These capsys assertions are fragile: write_outputs/write_summary (actions._append_lines) write to stdout only when $GITHUB_OUTPUT/$GITHUB_STEP_SUMMARY are unset, and otherwise append to those files leaving stdout empty. GitHub Actions sets GITHUB_OUTPUT for the whole job, so in the project's CI every capsys.readouterr().out here (this detect test, test_detect_empty_and_stale, test_dev_never_changes_changelog_or_tags, test_app_cli_materialize_and_detect) would assert against empty output and fail. The rest of the suite avoids this via the outputs/summary fixtures that monkeypatch GITHUB_OUTPUT to a temp file; these app tests should do the same instead of depending on the variable being absent.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tests/units/reflex_release/test_app.py, line 94:

<comment>These capsys assertions are fragile: `write_outputs`/`write_summary` (actions._append_lines) write to stdout only when `$GITHUB_OUTPUT`/`$GITHUB_STEP_SUMMARY` are unset, and otherwise append to those files leaving stdout empty. GitHub Actions sets GITHUB_OUTPUT for the whole job, so in the project's CI every `capsys.readouterr().out` here (this detect test, `test_detect_empty_and_stale`, `test_dev_never_changes_changelog_or_tags`, `test_app_cli_materialize_and_detect`) would assert against empty output and fail. The rest of the suite avoids this via the `outputs`/`summary` fixtures that monkeypatch GITHUB_OUTPUT to a temp file; these app tests should do the same instead of depending on the variable being absent.</comment>

<file context>
@@ -0,0 +1,271 @@
+    version = app.materialize(app_repo, "", "Release")
+    capsys.readouterr()
+    app.detect(app_repo, "main")
+    assert "any=true" in capsys.readouterr().out
+    git(app_repo.root, "tag", version)
+    app.detect(app_repo, "main")
</file context>

section, and opens a PR against main. Omitting `source-submodule` supports apps
whose source lives entirely in the deployment repository; in that case leave
`revision` empty. The source repository URL comes from `.gitmodules`. The default
checkout/token must be able to read it; private cross-repository submodules need

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.

P2: Private cross-repository submodules cannot use the documented app configuration because generated checkouts expose no scoped token or SSH credential. Add a configurable checkout credential to the generated workflows, or document private cross-repository submodules as unsupported.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/reflex-release/README.md, line 1073:

<comment>Private cross-repository submodules cannot use the documented app configuration because generated checkouts expose no scoped token or SSH credential. Add a configurable checkout credential to the generated workflows, or document private cross-repository submodules as unsupported.</comment>

<file context>
@@ -1029,3 +1029,105 @@ The same shape recovers a final version: seed `## 1.2.3 (<date>)` and dispatch
+section, and opens a PR against main. Omitting `source-submodule` supports apps
+whose source lives entirely in the deployment repository; in that case leave
+`revision` empty. The source repository URL comes from `.gitmodules`. The default
+checkout/token must be able to read it; private cross-repository submodules need
+an appropriately scoped checkout credential configured by the consumer.
+
</file context>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant