Skip to content

Commit 824f7dc

Browse files
committed
Experiment with finding feature flag references in the code
1 parent 41986fb commit 824f7dc

1 file changed

Lines changed: 114 additions & 0 deletions

File tree

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
name: 'PoC: GitHub Code References'
2+
permissions:
3+
contents: read
4+
5+
on:
6+
schedule:
7+
- cron: '0 0 * * *' # Runs daily at midnight UTC
8+
workflow_dispatch:
9+
pull_request: # DROPME
10+
11+
env:
12+
FLAGSMITH_PUBLIC_KEY: ENktaJnfLVbLifybz34JmX
13+
FLAGSMITH_EDGE_API_URL: https://edge.api.flagsmith.com
14+
PYTHON_VERSION: '3.13'
15+
PYTHON_REQUESTS_VERSION: '2.32.4'
16+
17+
jobs:
18+
collect-code-references:
19+
runs-on: depot-ubuntu-latest
20+
steps:
21+
- name: Checkout code
22+
uses: actions/checkout@v4
23+
24+
- uses: actions/setup-python@v5
25+
with:
26+
python-version: ${{ env.PYTHON_VERSION }}
27+
28+
- name: Install dependencies
29+
run: |
30+
pip install requests==${{ env.PYTHON_REQUESTS_VERSION }}
31+
32+
- name: Collect code references
33+
id: collect
34+
shell: python
35+
run: |
36+
import json
37+
import os
38+
import re
39+
from collections import deque
40+
from pathlib import Path
41+
from typing import Generator
42+
43+
import requests
44+
45+
EXCLUDE_PATTERNS = ["node_modules", "venv", ".git", "cache", "build", "htmlcov", "docs", ".json"]
46+
47+
def is_file_binary(file_path: Path) -> bool:
48+
"""Check if a file is binary."""
49+
with file_path.open("rb") as file:
50+
chunk = file.read(1024)
51+
return b'\0' in chunk
52+
53+
def find_references(feature_names: list[str]) -> Generator[tuple[str, str, int], None, None]:
54+
"""Search for references to a feature name in the codebase."""
55+
all_files = Path('.').glob("**/*")
56+
for path in all_files:
57+
if any(pattern in str(path).lower() for pattern in EXCLUDE_PATTERNS):
58+
continue
59+
if not path.is_file():
60+
continue
61+
if is_file_binary(path):
62+
continue
63+
context: deque[str] = deque(maxlen=2)
64+
with path.open("r", encoding="utf-8", errors="ignore") as file:
65+
for line_number, line in enumerate(file, start=1):
66+
context.append(line)
67+
for feature_name in feature_names:
68+
if feature_name not in line: # Match feature name
69+
continue
70+
if (
71+
re.search(fr"""[A-Z][A-Z0-9_]{3,}\s*=\s*(['"]){feature_name}\1""", line) or # Constant assignment
72+
re.search(fr"""(?ism:(?:feature|flag)[^\(]*\(.*(["']){feature_name})\1""", "".join(context)) # Function call
73+
):
74+
yield feature_name, str(path), line_number
75+
76+
# Fetch visible features
77+
all_flags = requests.get(f"${{ env.FLAGSMITH_EDGE_API_URL }}/api/v1/flags", headers={"X-Environment-Key": "${{ env.FLAGSMITH_PUBLIC_KEY }}"}).json()
78+
feature_names = sorted([flag["feature"]["name"] for flag in all_flags])
79+
print("Feature names:", feature_names)
80+
81+
# Find code references
82+
code_references = [
83+
(feature_name, file_path, line_number)
84+
for feature_name, file_path, line_number in find_references(feature_names)
85+
]
86+
87+
# Output to GHA
88+
json_references = json.dumps(code_references)
89+
with open(os.environ["GITHUB_OUTPUT"], "a") as gh_output:
90+
print(f"code_references='{json_references}'", file=gh_output)
91+
92+
- name: Display code references
93+
shell: python
94+
run: |
95+
import json
96+
from collections import defaultdict
97+
98+
code_references = json.loads('''${{ steps.collect.outputs.code_references }}''')
99+
if not code_references:
100+
print("No code references found.")
101+
exit(0)
102+
103+
references_by_feature = defaultdict(list)
104+
for feature_name, file_path, line_number in sorted(code_references):
105+
references_by_feature[feature_name].append((file_path, line_number))
106+
107+
print("Code References:")
108+
for feature_name, references in references_by_feature.items():
109+
print(f"\nFeature: {feature_name}")
110+
for file_path, line_number in references:
111+
print(f" - {file_path}:{line_number}")
112+
113+
# TODO
114+
# - name: Upload code references

0 commit comments

Comments
 (0)