Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> GitOrigin-RevId: 17d6ac52f78f00e3bf8a1ae5a32d029308c5824d
160 lines
5.7 KiB
Python
Executable File
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())
|