mongo/buildscripts/idl/generate_ifr_registry.py
Charlie Swanson e39da77904 SERVER-125794: Add IFR flag registry for code ownership (#52504)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
GitOrigin-RevId: 17d6ac52f78f00e3bf8a1ae5a32d029308c5824d
2026-05-20 19:40:25 +00:00

160 lines
5.7 KiB
Python
Executable File

#!/usr/bin/env python3
"""Generate src/mongo/db/ifr_flag_registry.yml from the feature flags in IDL files.
The IFR flag registry lists every feature flag declared with
incremental_rollout_phase 'rollout' or 'released' across all IDL files under src/.
It is fully derived from the IDL.
The registry's purpose is to give GitHub's CODEOWNERS mechanism a concrete
path to gate: OWNERS.yml lists the registry under @10gen/rollout-flag-managers,
so every regenerated change triggers that team's review when committed. See the
generated file's header for the full rationale.
"""
import argparse
import sys
import textwrap
from pathlib import Path
import yaml
DEFAULT_REGISTRY_PATH = "src/mongo/db/ifr_flag_registry.yml"
DEFAULT_SOURCE_ROOT = "src"
_HEADER = textwrap.dedent("""\
# AUTO-GENERATED — do not edit by hand.
#
# This file is regenerated by buildscripts/idl/generate_ifr_registry.py.
# It lists every feature flag declared with incremental_rollout_phase
# 'rollout' or 'released' across the IDL sources under src/. Just commit
# whatever your script invocation produces — there is no manual step.
#
# Why this file exists
# --------------------
# The registry is a hook for GitHub's CODEOWNERS mechanism. OWNERS.yml
# lists this file under @10gen/rollout-flag-managers, so every regenerated
# change requires that team's review — which is how the team stays informed
# about each phase transition.
#
# This ownership and heavy review process is expected to be a temporary
# measure while the organization continues to improve the IFR tooling, and
# while the broader server org ramps up its familiarity with IFR. The
# long-term goal is to have the phase transition process be as lightweight
# as possible for server developers, while still giving rollout flag
# managers confidence that they won't miss any important changes. This file
# and its associated CODEOWNERS gate are part of the current solution to
# that problem, but may not be needed in the future as the ecosystem
# matures.
#
# What IFR is
# -----------
# Incremental Feature Rollout is MongoDB's mechanism for shipping a feature
# as on-by-default while still allowing it to be toggled at runtime, so
# operators can disable it without a server restart if something goes wrong.
# - 'rollout': actively being rolled out; on by default, kill-switchable.
# - 'released': rollout complete; retained here as a historical record.
""")
def _iter_idl_files(source_root):
"""Yield absolute paths for every .idl file under source_root, sorted."""
root = Path(source_root)
if not root.is_dir():
return
yield from sorted(root.rglob("*.idl"))
def _extract_phase_flags(idl_path):
"""Return (rollout_flags, released_flags) declared in one IDL file.
IDL files are YAML, so we can read them directly with yaml.safe_load — no need
to bring in the full IDL parser just to look at feature_flags entries.
"""
with open(idl_path, encoding="utf-8") as f:
doc = yaml.safe_load(f)
if not isinstance(doc, dict):
return set(), set()
feature_flags = doc.get("feature_flags") or {}
rollout = set()
released = set()
for flag_name, flag_body in feature_flags.items():
if not isinstance(flag_body, dict):
continue
phase = flag_body.get("incremental_rollout_phase")
if phase == "rollout":
rollout.add(flag_name)
elif phase == "released":
released.add(flag_name)
return rollout, released
def collect_flags(source_root):
"""Walk source_root and return (rollout_flags, released_flags) across all IDLs."""
rollout = set()
released = set()
for idl_path in _iter_idl_files(source_root):
r, rel = _extract_phase_flags(idl_path)
rollout |= r
released |= rel
overlap = rollout & released
if overlap:
# TODO SERVER-126893 This shouldn't be possible, and we should delete
# this code and the corresponding test case.
raise ValueError(
f"Feature flag(s) declared in both 'rollout' and 'released' phases "
f"across IDL files: {sorted(overlap)}. A flag can only be in one phase."
)
return rollout, released
def render_registry(rollout_flags, released_flags):
"""Render the registry file contents. Deterministic given the inputs."""
def section(name, flags):
if not flags:
return f"{name}: {{}}\n"
lines = [f"{name}:"]
# Empty-mapping values keep the shape stable so future metadata
# (e.g. since_version) can be added without a format change.
lines.extend(f" {flag}: {{}}" for flag in sorted(flags))
return "\n".join(lines) + "\n"
return (
_HEADER
+ "\n"
+ section("rollout", rollout_flags)
+ "\n"
+ section("released", released_flags)
)
def main(argv=None):
parser = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
)
parser.add_argument(
"--source-root",
default=DEFAULT_SOURCE_ROOT,
help="Source tree to scan for .idl files (default: %(default)s).",
)
parser.add_argument(
"--output",
default=DEFAULT_REGISTRY_PATH,
help="Path to the registry file (default: %(default)s).",
)
args = parser.parse_args(argv)
try:
rollout, released = collect_flags(args.source_root)
except ValueError as exc:
sys.stderr.write(f"{exc}\n")
return 2
with open(args.output, "w", encoding="utf-8") as f:
f.write(render_registry(rollout, released))
return 0
if __name__ == "__main__":
sys.exit(main())