-
Notifications
You must be signed in to change notification settings - Fork 201
feat: add feature flag awareness to release script #12021
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -111,10 +111,66 @@ const extractDescription = (prBody: string): string | undefined => { | |
| return desc.length > MAX_DESC_LENGTH ? `${desc.slice(0, MAX_DESC_LENGTH)}...` : desc | ||
| } | ||
|
|
||
| const parseEnvFeatureFlags = (filePath: string): Record<string, boolean> => { | ||
| const flags: Record<string, boolean> = {} | ||
| if (!fs.existsSync(filePath)) return flags | ||
| const content = fs.readFileSync(filePath, 'utf-8') | ||
| for (const line of content.split('\n')) { | ||
| const trimmed = line.trim() | ||
| if (trimmed.startsWith('#') || !trimmed.includes('=')) continue | ||
| const [key, ...rest] = trimmed.split('=') | ||
| if (!key.startsWith('VITE_FEATURE_')) continue | ||
| const value = rest.join('=').trim().toLowerCase() | ||
| if (value === 'true') flags[key] = true | ||
| else if (value === 'false') flags[key] = false | ||
| } | ||
| return flags | ||
| } | ||
|
|
||
| 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 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() | ||
| } | ||
|
Comment on lines
+149
to
+166
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Silent incorrect output when
🛠 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 |
||
|
|
||
| const buildReleasePrompt = ( | ||
| version: string, | ||
| messages: string[], | ||
| prBodies: Map<number, string>, | ||
| devOnlyFlags: string[], | ||
| privateDisabledFlags: string[], | ||
| ): string => { | ||
| const commitList = messages | ||
| .map(msg => { | ||
|
|
@@ -128,26 +184,50 @@ const buildReleasePrompt = ( | |
| }) | ||
| .join('\n') | ||
|
|
||
| const devOnlySection = | ||
| devOnlyFlags.length > 0 | ||
| ? `\n\n## Dev-only feature flags (enabled in dev, disabled in production)\n\n${devOnlyFlags | ||
| .map(f => `- ${f}`) | ||
| .join('\n')}` | ||
| : '' | ||
|
|
||
| const privateDisabledSection = | ||
| privateDisabledFlags.length > 0 | ||
| ? `\n\n## Private-build disabled flags (enabled in production, disabled in private build)\n\n${privateDisabledFlags | ||
| .map(f => `- ${f}`) | ||
| .join('\n')}` | ||
| : '' | ||
|
|
||
| return `You are a release notes generator for ShapeShift Web, a decentralized crypto exchange. | ||
|
|
||
| Given the commit list below for ${version}, produce grouped release notes in markdown. | ||
| Given the commit list below for ${version}, produce grouped release notes in markdown with two clearly separated top-level sections. | ||
|
|
||
| ## Rules | ||
|
|
||
| 1. Group related commits under descriptive ## headings by feature domain (e.g. "TON chain + Stonfi swapper", "Yield improvements", "BigAmount migration") | ||
| 2. List each commit as a bullet under its group, preserving the original text and PR number | ||
| 3. After the bullet list, write 1-2 sentences summarizing what changed and what to test | ||
| 4. For commits that are clearly behind a feature flag (title contains "behind feature flag" or "under flag"), note **under flag, no testing required** | ||
| ### Section 1: "# Production changes - testing required" | ||
| 1. Contains all PRs/commits that affect production code paths | ||
| 2. Group related commits under descriptive ## headings by feature domain (e.g. "TON chain + Stonfi swapper", "Yield improvements", "BigAmount migration") | ||
| 3. List each commit as a bullet under its group, preserving the original text and PR number | ||
| 4. After the bullet list, write 1-2 sentences summarizing what changed and what to test | ||
| 5. For internal refactors with no user-facing changes (e.g. migrations, type changes, selector renames), note **regression testing only** and what to sanity-check | ||
| 6. For dependency bumps, CI fixes, infra, docker, CSP, and asset data regeneration, group under "## Fixes, deps, and infra" with **no testing required** | ||
| 7. Merge/backmerge commits (e.g. "Merge branch 'main' into develop") should be silently dropped | ||
| 8. Keep testing notes brief and actionable - what a QA person should click on, not implementation details | ||
| 9. Use present tense for summaries ("Enables TON chain" not "Enabled TON chain") | ||
| 10. Do NOT use emdashes. Use regular hyphens. | ||
| 7. If a commit relates to a feature listed in the private-build disabled flags below, append "**Note: disabled in private build.**" to its testing notes within the production section | ||
|
|
||
| ### Section 2: "# Dev/local only - no production testing required" | ||
| 8. Contains all PRs/commits that are gated behind dev-only feature flags listed below | ||
| 9. Match commits to dev-only flags by feature name (e.g. "Celo" matches CELO, "agentic chat" matches AGENTIC_CHAT, "Mantle" matches MANTLE, "Across" matches ACROSS_SWAP, etc.) | ||
| 10. Commits whose title explicitly says "behind feature flag" or "under flag" also belong here | ||
| 11. Group by feature domain with brief description only - no testing notes needed since these are not visible in production | ||
|
|
||
| ### General rules | ||
| 12. Merge/backmerge commits (e.g. "Merge branch 'main' into develop") should be silently dropped | ||
| 13. Keep testing notes brief and actionable - what a QA person should click on, not implementation details | ||
| 14. Use present tense for summaries ("Enables TON chain" not "Enabled TON chain") | ||
| 15. Do NOT use emdashes. Use regular hyphens. | ||
|
|
||
| ## Commits | ||
|
|
||
| ${commitList}` | ||
| ${commitList}${devOnlySection}${privateDisabledSection}` | ||
| } | ||
|
|
||
| const runClaude = (promptPath: string): Promise<string> => { | ||
|
|
@@ -203,8 +283,10 @@ const generateReleaseSummary = async ( | |
| version: string, | ||
| messages: string[], | ||
| prBodies: Map<number, string>, | ||
| devOnlyFlags: string[], | ||
| privateDisabledFlags: string[], | ||
| ): Promise<string | null> => { | ||
| const prompt = buildReleasePrompt(version, messages, prBodies) | ||
| const prompt = buildReleasePrompt(version, messages, prBodies, devOnlyFlags, privateDisabledFlags) | ||
| const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'shapeshift-release-')) | ||
| const promptPath = path.join(tmpDir, 'prompt.txt') | ||
|
|
||
|
|
@@ -310,7 +392,19 @@ const doRegularRelease = async () => { | |
| const prBodies = prNumbers.length > 0 ? await fetchPrBodies(prNumbers) : new Map<number, string>() | ||
| console.log(chalk.green(`Fetched ${prBodies.size}/${prNumbers.length} PR descriptions.`)) | ||
|
|
||
| const summary = await generateReleaseSummary(nextVersion, messages, prBodies) | ||
| const devOnlyFlags = getDevOnlyFlags() | ||
| console.log(chalk.green(`Detected ${devOnlyFlags.length} dev-only feature flags.`)) | ||
|
|
||
| const privateDisabledFlags = getPrivateDisabledFlags() | ||
| console.log(chalk.green(`Detected ${privateDisabledFlags.length} private-build disabled flags.`)) | ||
|
|
||
| const summary = await generateReleaseSummary( | ||
| nextVersion, | ||
| messages, | ||
| prBodies, | ||
| devOnlyFlags, | ||
| privateDisabledFlags, | ||
| ) | ||
| const prBody = summary ?? messages.join('\n') | ||
|
|
||
| if (summary) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
keyis not trimmed after splitting — potential whitespace mismatch across filesrest.join('=').trim()normalises the value butkeyretains any whitespace that surrounds=(e.g.VITE_FEATURE_FOO = true→key = "VITE_FEATURE_FOO "). When the same flag appears in two different.envfiles 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
🤖 Prompt for AI Agents