Skip to content

feat(feature-flags): reuse the basic-integration apps and grade the feature-flags outcome - #4189

Merged
johncwaters merged 4 commits into
mainfrom
johnw/feature-flags-e2e-checks
Sep 24, 2026
Merged

johncwaters merged 4 commits into
mainfrom
johnw/feature-flags-e2e-checks

Conversation

@johncwaters

@johncwaters johncwaters commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Feedback wanted: ready to merge. Approve or request changes. The direction is confirmed.

  • Is a separate .wizard-ci/source.json the right way to point an app at another app's tree, rather than growing expect.json?
  • Are these the right outcome checks for a feature-flags run, and is the "constants module used by another changed file" heuristic strict enough?

Follows up on gewenyu99's two answers on #4136: reuse the basic-integration apps instead of copying them, and have the feature-flags e2e assert its own outcome.

Changes

  • App pointer: apps/<app>/.wizard-ci/source.json ({"sourceApp": "..."}) names the app whose tree runE2e copies. It is read only for that, so reusing an app does not depend on expect.json, which would switch on the warehouse stub MCP and checks. source.json wins over the older sourceApp in expect.json.

  • Pointer folders: the 123 copied files become two folders, each with a README, source.json and feature-flags.json:

    • feature-flags/next-js/15-app-router-saas points at basic-integration/next-js/15-app-router-saas
    • feature-flags/django/django3-saas points at basic-integration/django/django3-saas
  • Feature-flag checks: services/wizard-ci/feature-flag-checks.ts grades a run against the keys in feature-flags.json (both example keys for Next.js, the backend key only for Django):

    • each flag exists, is inactive, and is at 0%, via the Flags API with POSTHOG_PERSONAL_API_KEY
    • each key string appears in exactly one changed file, the constants module
    • another changed file imports that module (a JS/TS or Python import naming it, or in Ruby a reference to the namespace that holds the key, since Rails autoloads constants; not just a mention)
    • no unexpected wizard-example-* key appears
    • the report file exists

    A changed file is one in the /tmp copy that differs from the source app. CI never deletes the flags, because parallel runs share the same keys.

  • Wizard layout: wizard-commands.ts finds e2e.json in both src/programs and src/lib/programs, so e2e-only commands resolve on either side of the wizard's layered refactor.

Follow-ups:

  • Move warehouse-seeded/next-stripe-declined's sourceApp from expect.json to source.json. Left as is here.
  • Add pnpm test:feature-flag-checks and pnpm test:source-app to .github/workflows/checks.yml. Not in this PR because the push token lacks the workflow scope.

How did you test this code?

  • pnpm test:feature-flag-checks (47 tests), pnpm test:source-app, pnpm test:warehouse-checks and tsc --noEmit pass.
  • Local e2e on a test project with the context-mill dev server, wizard at feat(feature-flags): add the wizard feature-flags command wizard#1322 rebased onto main:
    • pnpm wizard-ci feature-flags/next-js/15-app-router-saas --e2e: pass, all 12 feature-flag checks green.
    • pnpm wizard-ci feature-flags/django/django3-saas --e2e: pass, all 7 feature-flag checks green.
    • Rerun at this head, wizard at [CI] (48ac55f) tanstack-start/tanstack-start-saas #1322's head 878a5b7f: Next.js passes. Django fails no unexpected flag key, because that run added a frontend side (browser snippet in templates/base.html, wizard-example-frontend-flag evaluated in templates/dashboard/index.html) to an app whose templates have no scripts. The check is kept strict, and the side choice is fixed in the wizard: [CI] (48ac55f) tanstack-start/tanstack-start-saas #1322's seed prompt no longer counts templates without their own <script> as a frontend. With that prompt, Django passed twice (backend flag only, key in dashboard/feature_flags.py) and Next.js passed (both flags).
    • Rerun at 24de57a4e, wizard at [CI] (48ac55f) tanstack-start/tanstack-start-saas #1322 5e3c1c6e: Next.js, Django and Rails (basic-integration/rails/fizzy, through a temporary pointer that is not committed) all pass. Rails passes the constants-module check through the autoloaded PostHogFeatureFlags::BACKEND_EXAMPLE reference, with no import.
    • Django with feature-flags.json set to a wrong key (wizard-example-wrong-flag): fails as it should: the flag does not exist, the key is in no changed file, and wizard-example-backend-flag is reported as unexpected.

LLM context

Written with Claude Code.

The wizard's layered refactor moves programs from src/lib/programs to
src/programs. Look in both so e2e-only commands resolve on either side of it.
…on apps and grade its outcome

Replace the two copied apps with pointer folders. A new .wizard-ci/source.json
names the app a run copies, independent of expect.json, so reusing an app does
not switch on the warehouse stub and checks.

feature-flag-checks.ts grades a run against .wizard-ci/feature-flags.json:
each flag exists, inactive at 0%, via the Flags API; each key sits in exactly
one changed file, a constants module another changed file uses; no other
example key appears; the report is written. CI never deletes the flags,
because parallel legs share the keys.
The "constants module used by another changed file" check matched the
module's bare name anywhere, so a comment or string saying "flags" passed
it. It now requires an import form: a JS/TS specifier after from,
require( or import(, or a Python import or from-import naming the
module, including submodule, aliased, multi-name and commented
parenthesized forms.

The READMEs now list only what the checks grade, and say that existence
cannot prove this run created a flag, because CI never deletes the shared
keys.

Directive: flag checks stay existence-only; the wizard reuses an existing flag, so a created_at gate would fail real runs
Confidence: medium
Scope-risk: narrow
Not-tested: round-3 fix for `import os, posthog_flags` and commented parenthesized from-imports had no review after it (unit tests only)
…odule use

Rails loads constants by name, so a controller using
PostHogFeatureFlags::WIZARD_EXAMPLE_BACKEND has no import line, and the
"constants module used by another changed file" check false-failed a
correct Rails run. For a .rb constants file, a reference to the namespace
enclosing the flag key now counts: its full path or innermost name,
optionally with a leading ::.

The enclosing namespace comes from indentation, not from counting block
keywords: Ruby opens end-closed blocks in too many forms (mid-line begin,
private def, x = if) for a keyword counter to stay right, and an outer
wrapper namespace alone must never count.

Constraint: assumes conventionally indented Ruby, which Rails and generated code are
Confidence: medium
Scope-risk: narrow
Not-tested: round-3 switch to the indentation walk (memoized begin, private def cases) had no review after it; unit tests only
Not-tested: no e2e rerun on a Rails app; the saved /tmp Rails run output was cleaned up before re-grading
@johncwaters
johncwaters marked this pull request as ready for review September 24, 2026 17:05
@johncwaters
johncwaters requested a review from a team as a code owner September 24, 2026 17:05

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

Do it

@johncwaters
johncwaters merged commit 71442e0 into main Sep 24, 2026
11 checks passed
@johncwaters
johncwaters deleted the johnw/feature-flags-e2e-checks branch September 24, 2026 17:06
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.

2 participants