Skip to content

PoC: GitHub Code References #101

PoC: GitHub Code References

PoC: GitHub Code References #101

name: 'PoC: GitHub Code References'
permissions:
contents: read
on:
schedule:
- cron: '0 0 * * *' # Runs daily at midnight UTC
workflow_dispatch:
env:
EXCLUDE_PATTERNS: node_modules,venv,.git,cache,build,htmlcov,docs,.json,tests
FLAGSMITH_ADMIN_API_URL: https://api.flagsmith.com
FLAGSMITH_ADMIN_API_KEY: ${{ secrets.FLAGSMITH_CODE_REFERENCES_API_KEY }}
FLAGSMITH_PROJECT_ID: 12
PYTHON_VERSION: '3.13'
jobs:
collect-code-references:
runs-on: depot-ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: astral-sh/setup-uv@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
enable-cache: true
- name: Collect code references
id: collect
run: |
uv run - <<EOF
# /// script
# requires-python = ">=${{ env.PYTHON_VERSION }}"
# dependencies = ["requests"]
# ///
import json
import os
import re
from collections import defaultdict, deque
from pathlib import Path
from typing import Generator
import requests
EXCLUDE_PATTERNS = os.environ["EXCLUDE_PATTERNS"].replace(" ", "").split(",")
def should_skip_file(file_path: Path) -> bool:
"""Whether to skip a file based on its size or content"""
file_size = file_path.stat().st_size
if file_size == 0: # Empty files are irrelevant
return True
if file_size > 1024 * 1024: # Large files are likely binary
return True
with file_path.open("rb") as file:
chunk = file.read(4096) # A text file rarely contains null bytes
if b'\0' in chunk:
return True
try:
chunk.decode('utf-8')
except UnicodeDecodeError: # Decoding likely fails for binary files
return True
return False
def find_references(feature_names: list[str]) -> Generator[tuple[str, str, int], None, None]:
"""Search for references to a feature name in the codebase."""
all_files = Path('.').glob("**/*")
for path in all_files:
if any(pattern in str(path).lower() for pattern in EXCLUDE_PATTERNS):
continue
if not path.is_file():
continue
if should_skip_file(path):
continue
context: deque[str] = deque(maxlen=2)
with path.open("r", encoding="utf-8", errors="ignore") as file:
for line_number, line in enumerate(file, start=1):
context.append(line)
for feature_name in feature_names:
if feature_name not in line: # Match feature name
continue
re_function_calls = rf"""(?i:(?:feature|flag)\w*\(\s*(["']){re.escape(feature_name)})\1"""
if re.search(re_function_calls, "".join(context)):
yield feature_name, str(path), line_number
# TODO: Add more sophisticated matching, e.g. feature names defined as constants
def retrieve_feature_names() -> list[str]:
"""Fetch feature names from the Flagsmith API."""
response = requests.get( # TODO: Make better use of pagination
f"${{ env.FLAGSMITH_ADMIN_API_URL }}/api/v1/projects/${{ env.FLAGSMITH_PROJECT_ID }}/features/?page_size=1000",
headers={"Authorization": f"Api-Key ${{ env.FLAGSMITH_ADMIN_API_KEY }}"},
)
response.raise_for_status()
return [feature["name"] for feature in response.json()["results"]]
# Fetch visible features
feature_names = retrieve_feature_names()
# Find code references
code_references = [
{"feature_name": feature_name, "file_path": file_path, "line_number": line_number}
for feature_name, file_path, line_number in find_references(feature_names)
]
# Output to GHA
json_references = json.dumps(code_references)
with open(os.environ["GITHUB_OUTPUT"], "a") as gh_output:
print(f"code_references={json_references}", file=gh_output)
if not code_references:
print("No code references found.")
exit(0)
references_by_feature = defaultdict(list)
sorted_code_references = sorted(code_references, key=lambda x: (x["feature_name"], x["file_path"], x["line_number"]))
for reference in sorted_code_references:
references_by_feature[reference["feature_name"]].append((reference["file_path"], reference["line_number"]))
print("Code References:")
for feature_name, references in references_by_feature.items():
print(f"\nFeature: {feature_name}")
for file_path, line_number in references:
print(f" - {file_path}:{line_number}")
EOF
- name: Upload code references
run: |
uv run - <<EOF
# /// script
# requires-python = ">=${{ env.PYTHON_VERSION }}"
# dependencies = ["requests"]
# ///
import json
import requests
code_references = json.loads("""${{ steps.collect.outputs.code_references }}""")
if not code_references:
print("No code references to upload.")
exit(0)
response = requests.post(
f"${{ env.FLAGSMITH_ADMIN_API_URL }}/api/v1/projects/${{ env.FLAGSMITH_PROJECT_ID }}/code-references/",
headers={"Authorization": f"Api-Key ${{ env.FLAGSMITH_ADMIN_API_KEY }}"},
json={
"repository_url": "${{ github.server_url }}/${{ github.repository }}",
"revision": "${{ github.sha }}",
"code_references": code_references,
},
)
response.raise_for_status()
print(f"Uploaded {len(code_references)} code references.")
EOF