Skip to content

feat: add feature flag awareness to release script#12021

Merged
0xApotheosis merged 2 commits intodevelopfrom
feat/release-script-feature-flag-awareness
Feb 24, 2026
Merged

feat: add feature flag awareness to release script#12021
0xApotheosis merged 2 commits intodevelopfrom
feat/release-script-feature-flag-awareness

Conversation

@0xApotheosis
Copy link
Member

@0xApotheosis 0xApotheosis commented Feb 24, 2026

Description

The release script (scripts/release.ts) generates AI-powered release summaries for the ops team. Currently, it has no awareness of which features are gated behind dev-only flags vs production-enabled flags. Rule 4 in the Claude prompt only catches commits whose titles explicitly say "behind feature flag" - it doesn't read the actual .env files. This means features like Celo, Agentic Chat, and ~25 other dev-only chains/swappers are not properly annotated, causing the ops team to over-test features that aren't even visible in production.

This PR adds env file parsing to the release script so it can automatically detect dev-only feature flags and produce two clearly separated output sections:

  1. Production changes - testing required - PRs affecting prod code paths, with testing notes
  2. Dev/local only - no production testing required - PRs gated behind dev-only flags, no testing needed

Example of updated output:

Screenshot 2026-02-24 at 2 44 14 pm

What changed

  • parseEnvFeatureFlags(filePath) - Reads a .env file and extracts VITE_FEATURE_* keys with boolean values
  • getDevOnlyFlags() - Computes effective prod flags (.env + .env.production) vs dev flags (.env + .env.development), returns flags where dev=true and prod!=true
  • buildReleasePrompt - Updated to accept dev-only flags and restructured prompt rules for two-section output
  • Call site in doRegularRelease updated to compute and pass dev-only flags

Currently detects 27 dev-only flags (e.g. CELO, AGENTIC_CHAT, MANTLE, FLOWEVM, ACROSS_SWAP) while correctly excluding prod-enabled flags (SOLANA, ARBITRUM, BASE, etc.).

Issue (if applicable)

N/A

Risk

Minimal. Changes are isolated to scripts/release.ts which is a CLI dev tool only - no runtime, user-facing, or on-chain impact. The script still falls back to raw commit list if Claude generation fails.

What protocols, transaction types, wallets or contract interactions might be affected by this PR?

None. This only affects the release notes generation tooling.

Testing

Engineering

  1. Run npx ts-node scripts/release.ts - it should still prompt for release type and generate summaries
  2. Verify yarn type-check passes (no new errors in scripts/release.ts)
  3. Verify yarn lint --fix passes (no new errors)
  4. Spot-check: flags like CELO, AGENTIC_CHAT, MANTLE appear in the dev-only list; flags like SOLANA, ARBITRUM, BASE do not

Operations

  • 🏁 My feature is behind a flag and doesn't require operations testing (yet)

This is a dev tooling change only - no user-facing impact. The output of the release script will now be more useful to operations by clearly separating what needs production testing from what doesn't.

Screenshots (if applicable)

Example output with new format (v1.1010.0 commits):

# Production changes - testing required

## Swap widget and RelaySwapper fixes
- fix: disable Tron swaps on RelaySwapper (#12014)
- fix: widget railway delivery (#11987)
- fix: cleanup swap widget (#11865)

Test that swaps execute normally, Tron is properly disabled from RelaySwapper, and the widget loads correctly.

## API authentication changes
- feat: replace API key auth with optional affiliate address tracking (#11959)

Test that the swap flow works normally and verify affiliate tracking is captured if configured.

---

# Dev/local only - no production testing required

## Second-class Relay chain integrations
- feat: integrate Celo (42220) as second-class Relay chain (#11939)
- feat: integrate Flow EVM (747) as second-class Relay chain (#11938)

Adds support for Celo and Flow EVM as second-class Relay chains behind dev-only feature flags.

Summary by CodeRabbit

  • Chores
    • Improved release note generation to automatically incorporate feature flag information. Release summaries now include details about features enabled only in development builds and features disabled in private builds, ensuring comprehensive documentation of feature availability across all build configurations.

The release script now parses .env, .env.development, and .env.production
to detect dev-only feature flags and produces two-section output separating
production changes from dev/local-only changes gated behind flags.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 24, 2026

📝 Walkthrough

Walkthrough

This PR adds parsing utilities for VITE_FEATURE_* environment variables from .env files and extends the AI-driven release summary generation flow to incorporate feature-flag information, including dev-only flags and private-build disabled flags, in the release prompt.

Changes

Cohort / File(s) Summary
Feature Flag Parsing & Release Integration
scripts/release.ts
Added three utility functions (parseEnvFeatureFlags, getDevOnlyFlags, getPrivateDisabledFlags) to parse and derive feature-flag differences from env files. Extended buildReleasePrompt, generateReleaseSummary, and doRegularRelease to accept and pass feature-flag arrays, injecting dev-only and private-disabled flag sections into release prompts.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~15 minutes

Possibly related PRs

Suggested reviewers

  • premiumjibles
  • gomesalexandre

Poem

🐰 The feature flags now speak and sing,
Dev-only and private, each their thing!
Release notes flutter with newfound sight,
Scripts parse the envs, everything's right! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly matches the main change: adding feature flag awareness to the release script through .env file parsing and prompt integration.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/release-script-feature-flag-awareness

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.

Annotates production commits related to features disabled in the private
build (Mixpanel, Chatwoot) so QA knows these don't apply to the private
deployment.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@0xApotheosis
Copy link
Member Author

I'm going to push this one through so that I can then check the new hotfix script update on this PR and confirm that it works.

@0xApotheosis 0xApotheosis marked this pull request as ready for review February 24, 2026 04:32
@0xApotheosis 0xApotheosis requested a review from a team as a code owner February 24, 2026 04:32
@0xApotheosis 0xApotheosis merged commit c16636c into develop Feb 24, 2026
3 of 4 checks passed
@0xApotheosis 0xApotheosis deleted the feat/release-script-feature-flag-awareness branch February 24, 2026 04:36
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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)
scripts/release.ts (1)

130-166: Extract shared flag-diff logic to avoid duplication

getDevOnlyFlags and getPrivateDisabledFlags are structurally identical — same rootDir resolution, same spread-merge pattern, same key-filtering loop. A single helper eliminates ~25 lines of duplication and makes the intent of each call site immediately clear.

♻️ Suggested refactor
+const ROOT_DIR = path.resolve(__dirname, '..')
+
+const computeFeatureFlagDiff = (enabledPaths: string[], disabledPaths: string[]): string[] => {
+  const loadEffective = (paths: string[]) =>
+    paths.reduce<Record<string, boolean>>(
+      (acc, filePath) => ({ ...acc, ...parseEnvFeatureFlags(filePath) }),
+      {},
+    )
+  const enabledFlags = loadEffective(enabledPaths)
+  const disabledFlags = loadEffective(disabledPaths)
+  const allKeys = new Set([...Object.keys(enabledFlags), ...Object.keys(disabledFlags)])
+  return Array.from(allKeys)
+    .filter(key => enabledFlags[key] === true && disabledFlags[key] !== true)
+    .map(key => key.replace('VITE_FEATURE_', ''))
+    .sort()
+}

-const getDevOnlyFlags = (): string[] => {
-  const rootDir = path.resolve(__dirname, '..')
-  const baseFlags = parseEnvFeatureFlags(path.join(rootDir, '.env'))
-  const prodOverrides = parseEnvFeatureFlags(path.join(rootDir, '.env.production'))
-  const devOverrides = parseEnvFeatureFlags(path.join(rootDir, '.env.development'))
-  const prodFlags: Record<string, boolean> = { ...baseFlags, ...prodOverrides }
-  const devFlags: Record<string, boolean> = { ...baseFlags, ...devOverrides }
-  const allKeys = new Set([...Object.keys(prodFlags), ...Object.keys(devFlags)])
-  const devOnly: string[] = []
-  for (const key of allKeys) {
-    if (devFlags[key] === true && prodFlags[key] !== true) {
-      devOnly.push(key.replace('VITE_FEATURE_', ''))
-    }
-  }
-  return devOnly.sort()
-}
+const getDevOnlyFlags = (): string[] =>
+  computeFeatureFlagDiff(
+    [path.join(ROOT_DIR, '.env'), path.join(ROOT_DIR, '.env.development')],
+    [path.join(ROOT_DIR, '.env'), path.join(ROOT_DIR, '.env.production')],
+  )

-const getPrivateDisabledFlags = (): string[] => {
-  const rootDir = path.resolve(__dirname, '..')
-  const baseFlags = parseEnvFeatureFlags(path.join(rootDir, '.env'))
-  const prodOverrides = parseEnvFeatureFlags(path.join(rootDir, '.env.production'))
-  const privateOverrides = parseEnvFeatureFlags(path.join(rootDir, '.env.private'))
-  const prodFlags: Record<string, boolean> = { ...baseFlags, ...prodOverrides }
-  const privateFlags: Record<string, boolean> = { ...baseFlags, ...privateOverrides }
-  const allKeys = new Set([...Object.keys(prodFlags), ...Object.keys(privateFlags)])
-  const privateDisabled: string[] = []
-  for (const key of allKeys) {
-    if (prodFlags[key] === true && privateFlags[key] !== true) {
-      privateDisabled.push(key.replace('VITE_FEATURE_', ''))
-    }
-  }
-  return privateDisabled.sort()
-}
+const getPrivateDisabledFlags = (): string[] => {
+  const privatePath = path.join(ROOT_DIR, '.env.private')
+  if (!fs.existsSync(privatePath)) {
+    console.log(chalk.yellow('No .env.private found; skipping private-build disabled flags.'))
+    return []
+  }
+  return computeFeatureFlagDiff(
+    [path.join(ROOT_DIR, '.env'), path.join(ROOT_DIR, '.env.production')],
+    [path.join(ROOT_DIR, '.env'), privatePath],
+  )
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/release.ts` around lines 130 - 166, Both getDevOnlyFlags and
getPrivateDisabledFlags duplicate the same logic; extract a reusable helper
(e.g., computeFlagDiff) that accepts rootDir, two override filenames or two flag
maps and a predicate describing which keys to keep, calls parseEnvFeatureFlags,
merges base + overrides into two records, computes the union of keys, filters
using the provided condition, strips the "VITE_FEATURE_" prefix, sorts and
returns the array; then implement getDevOnlyFlags to call computeFlagDiff with
'.env.development' and a predicate (dev true && prod not true) and
getPrivateDisabledFlags with '.env.private' and a predicate (prod true &&
private not true), reusing parseEnvFeatureFlags and keeping existing return
values.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/release.ts`:
- Around line 121-123: The split result leaves whitespace on the key and doesn't
strip quotes from values, causing mismatched keys and ignored quoted booleans;
in the parsing logic in scripts/release.ts (the const [key, ...rest] =
trimmed.split('=') block) trim the key (e.g. key = key.trim()) before the
VITE_FEATURE_ check and normalize the value by trimming, stripping surrounding
single or double quotes, then lowercasing (e.g. value =
unquote(rest.join('=').trim()).toLowerCase()) so quoted booleans like "true" are
recognized consistently across .env files.
- Around line 149-166: The function getPrivateDisabledFlags can produce false
positives when .env.private is missing because parseEnvFeatureFlags returns {} —
check for the presence of the private env file before using its parsed flags:
compute the privatePath (path.join(rootDir, '.env.private')), if
fs.existsSync(privatePath) is false then return an empty array early (or skip
comparing privateFlags) to avoid treating baseFlags as an intentional private
override; update the logic around privateOverrides/privateFlags in
getPrivateDisabledFlags to only parse and compare when the private file exists.

---

Nitpick comments:
In `@scripts/release.ts`:
- Around line 130-166: Both getDevOnlyFlags and getPrivateDisabledFlags
duplicate the same logic; extract a reusable helper (e.g., computeFlagDiff) that
accepts rootDir, two override filenames or two flag maps and a predicate
describing which keys to keep, calls parseEnvFeatureFlags, merges base +
overrides into two records, computes the union of keys, filters using the
provided condition, strips the "VITE_FEATURE_" prefix, sorts and returns the
array; then implement getDevOnlyFlags to call computeFlagDiff with
'.env.development' and a predicate (dev true && prod not true) and
getPrivateDisabledFlags with '.env.private' and a predicate (prod true &&
private not true), reusing parseEnvFeatureFlags and keeping existing return
values.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7cd378e and 1e3c32a.

📒 Files selected for processing (1)
  • scripts/release.ts

Comment on lines +121 to +123
const [key, ...rest] = trimmed.split('=')
if (!key.startsWith('VITE_FEATURE_')) continue
const value = rest.join('=').trim().toLowerCase()
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

key is not trimmed after splitting — potential whitespace mismatch across files

rest.join('=').trim() normalises the value but key retains any whitespace that surrounds = (e.g. VITE_FEATURE_FOO = truekey = "VITE_FEATURE_FOO "). When the same flag appears in two different .env files with inconsistent spacing, the spread merge treats them as distinct keys, silently producing wrong flag resolution. Additionally, quoted booleans (VITE_FEATURE_FOO="true") are silently ignored.

🛠 Proposed fix
-    const [key, ...rest] = trimmed.split('=')
-    if (!key.startsWith('VITE_FEATURE_')) continue
-    const value = rest.join('=').trim().toLowerCase()
+    const [rawKey, ...rest] = trimmed.split('=')
+    const key = rawKey.trim()
+    if (!key.startsWith('VITE_FEATURE_')) continue
+    const value = rest.join('=').trim().replace(/^["']|["']$/g, '').toLowerCase()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/release.ts` around lines 121 - 123, The split result leaves
whitespace on the key and doesn't strip quotes from values, causing mismatched
keys and ignored quoted booleans; in the parsing logic in scripts/release.ts
(the const [key, ...rest] = trimmed.split('=') block) trim the key (e.g. key =
key.trim()) before the VITE_FEATURE_ check and normalize the value by trimming,
stripping surrounding single or double quotes, then lowercasing (e.g. value =
unquote(rest.join('=').trim()).toLowerCase()) so quoted booleans like "true" are
recognized consistently across .env files.

Comment on lines +149 to +166
const getPrivateDisabledFlags = (): string[] => {
const rootDir = path.resolve(__dirname, '..')
const baseFlags = parseEnvFeatureFlags(path.join(rootDir, '.env'))
const prodOverrides = parseEnvFeatureFlags(path.join(rootDir, '.env.production'))
const privateOverrides = parseEnvFeatureFlags(path.join(rootDir, '.env.private'))

const prodFlags: Record<string, boolean> = { ...baseFlags, ...prodOverrides }
const privateFlags: Record<string, boolean> = { ...baseFlags, ...privateOverrides }

const allKeys = new Set([...Object.keys(prodFlags), ...Object.keys(privateFlags)])
const privateDisabled: string[] = []
for (const key of allKeys) {
if (prodFlags[key] === true && privateFlags[key] !== true) {
privateDisabled.push(key.replace('VITE_FEATURE_', ''))
}
}
return privateDisabled.sort()
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Silent incorrect output when .env.private is absent

parseEnvFeatureFlags returns {} for missing files, so if .env.private doesn't exist, privateFlags collapses to baseFlags. Any flag that is false in .env but true in .env.production (i.e. enabled only via the production override) then satisfies prodFlags[key] === true && privateFlags[key] !== true, and gets incorrectly injected into the Claude prompt as "disabled in private build". This silently causes incorrect QA-skip annotations on otherwise live production features.

🛠 Proposed fix — early-return when private env is absent
 const getPrivateDisabledFlags = (): string[] => {
   const rootDir = path.resolve(__dirname, '..')
-  const baseFlags = parseEnvFeatureFlags(path.join(rootDir, '.env'))
-  const prodOverrides = parseEnvFeatureFlags(path.join(rootDir, '.env.production'))
-  const privateOverrides = parseEnvFeatureFlags(path.join(rootDir, '.env.private'))
+  const privatePath = path.join(rootDir, '.env.private')
+  if (!fs.existsSync(privatePath)) {
+    console.log(chalk.yellow('No .env.private found; skipping private-build disabled flags.'))
+    return []
+  }
+  const baseFlags = parseEnvFeatureFlags(path.join(rootDir, '.env'))
+  const prodOverrides = parseEnvFeatureFlags(path.join(rootDir, '.env.production'))
+  const privateOverrides = parseEnvFeatureFlags(privatePath)
 
   const prodFlags: Record<string, boolean> = { ...baseFlags, ...prodOverrides }
   const privateFlags: Record<string, boolean> = { ...baseFlags, ...privateOverrides }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/release.ts` around lines 149 - 166, The function
getPrivateDisabledFlags can produce false positives when .env.private is missing
because parseEnvFeatureFlags returns {} — check for the presence of the private
env file before using its parsed flags: compute the privatePath
(path.join(rootDir, '.env.private')), if fs.existsSync(privatePath) is false
then return an empty array early (or skip comparing privateFlags) to avoid
treating baseFlags as an intentional private override; update the logic around
privateOverrides/privateFlags in getPrivateDisabledFlags to only parse and
compare when the private file exists.

0xApotheosis added a commit that referenced this pull request Feb 24, 2026
* feat: add feature flag awareness to release script

The release script now parses .env, .env.development, and .env.production
to detect dev-only feature flags and produces two-section output separating
production changes from dev/local-only changes gated behind flags.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: add private build environment awareness to release script

Annotates production commits related to features disabled in the private
build (Mixpanel, Chatwoot) so QA knows these don't apply to the private
deployment.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
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