Skip to content

Commit 9860248

Browse files
committed
Fix post-migrate call. Replace with manual migrations
1 parent 2ef72a1 commit 9860248

File tree

5 files changed

+115
-6
lines changed

5 files changed

+115
-6
lines changed

ansible_base/feature_flags/apps.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,4 @@ class FeatureFlagsConfig(AppConfig):
1111
verbose_name = 'Feature Flags'
1212

1313
def ready(self):
14-
try:
15-
create_initial_data()
16-
except Exception:
17-
post_migrate.connect(create_initial_data, sender=self)
14+
post_migrate.connect(create_initial_data, sender=self)

ansible_base/feature_flags/migrations/0001_initial.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Generated by Django 4.2.21 on 2025-06-17 20:08
2+
# FileHash: df8a46833e96d189abbfb07f557975091a80adbe3637e365d93ff7acb7fb3343
23

34
import ansible_base.feature_flags.models.aap_flag
45
from django.conf import settings
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
### INSTRUCTIONS ###
2+
# If updating the feature_flags.yaml, create a new migration file by copying this one.
3+
# 1. Name the file XXXX_manual_YYYYMMDD.py. For example 0002_manual_20250808.py
4+
# 1. Uncomment the migration below, by uncommenting everything below the FileHash
5+
# 2. Update the dependency to point to the last dependency
6+
# 3. Set the FileHash
7+
###
8+
9+
# FileHash: <FileHash>
10+
11+
# from django.db import migrations
12+
13+
14+
# class Migration(migrations.Migration):
15+
16+
# dependencies = [
17+
# ('dab_feature_flags', '0001_initial'),
18+
# ]
19+
20+
# operations = [
21+
# ]

docs/apps/feature_flags.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,9 @@ urlpatterns = [
4848
]
4949
```
5050

51-
## Adding Feature Flags
51+
## Adding/updating/removing feature flags
5252

53-
To add a feature flag to the platform, specify it in the following [file](../../ansible_base/feature_flags/definitions/feature_flags.yaml)
53+
To add/update/remove a feature flag to the platform, ensure its configuration is specified correctly it in the following [file](../../ansible_base/feature_flags/definitions/feature_flags.yaml)
5454

5555
An example flag could resemble -
5656

@@ -73,3 +73,11 @@ Validate this file against the json schema by running `check-jsonschema` -
7373
pip install check-jsonschema
7474
check-jsonschema --schemafile ansible_base/feature_flags/definitions/schema.json ansible_base/feature_flags/definitions/feature_flags.yaml
7575
```
76+
77+
After adding/updating/removing a feature flag, make a manual migration. This can be done by -
78+
79+
1. Copying this [example-migration](../../ansible_base/feature_flags/migrations/example_migration).
80+
2. Name the file XXXX_manual_YYYYMMDD.py. For example 0002_manual_20250808.py
81+
3. Uncomment the migration, by uncommenting everything below the FileHash
82+
4. Update the dependency in the migration to point to the previous migration
83+
5. Set the **FileHash** in the migration file
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import hashlib
2+
import os
3+
4+
from django.conf import settings
5+
from django.test import TestCase
6+
7+
8+
class FileHashTest(TestCase):
9+
FILE_TO_CHECK_PATH = os.path.join(settings.BASE_DIR, 'ansible_base', 'feature_flags', 'definitions', 'feature_flags.yaml')
10+
HASH_ALGORITHM = 'sha256'
11+
HASH_COMMENT_PREFIX = '# FileHash:'
12+
13+
def _get_last_migration_file(self):
14+
"""
15+
Finds the path to the last migration file in the specified Django app.
16+
"""
17+
migrations_dir = os.path.join(settings.BASE_DIR, 'ansible_base', 'feature_flags', 'migrations')
18+
if not os.path.isdir(migrations_dir):
19+
raise FileNotFoundError(f"Migrations directory not found for app: {self.APP_NAME}")
20+
21+
migration_files = sorted([
22+
f for f in os.listdir(migrations_dir)
23+
if f.endswith('.py') and f != '__init__.py'
24+
])
25+
26+
if not migration_files:
27+
raise FileNotFoundError(f"No migration files found in {migrations_dir}")
28+
29+
return os.path.join(migrations_dir, migration_files[-1])
30+
31+
def _extract_hash_from_migration(self, migration_file_path):
32+
"""
33+
Extracts the expected hash from a comment in the migration file.
34+
Assumes the format: '# FileHash: <hash_value>'
35+
"""
36+
with open(migration_file_path, 'r') as f:
37+
for line in f:
38+
if line.strip().startswith(self.HASH_COMMENT_PREFIX):
39+
return line.strip().replace(self.HASH_COMMENT_PREFIX, '').strip()
40+
return None
41+
42+
def _calculate_file_hash(self, file_path):
43+
"""
44+
Calculates the hash of the given file.
45+
"""
46+
hash_func = getattr(hashlib, self.HASH_ALGORITHM, None)
47+
if not hash_func:
48+
raise ValueError(f"Unsupported hash algorithm: {self.HASH_ALGORITHM}")
49+
50+
hasher = hash_func()
51+
with open(file_path, 'rb') as f:
52+
for chunk in iter(lambda: f.read(4096), b""):
53+
hasher.update(chunk)
54+
return hasher.hexdigest()
55+
56+
def test_file_hash_matches_migration_comment(self):
57+
"""
58+
Checks if the hash of a specified file matches the hash commented
59+
in the last migration file.
60+
"""
61+
# 1. Get the last migration file
62+
try:
63+
last_migration_file = self._get_last_migration_file()
64+
except FileNotFoundError as e:
65+
self.fail(f"Could not find last migration file: {e}")
66+
67+
# 2. Extract the expected hash from the migration file
68+
expected_hash = self._extract_hash_from_migration(last_migration_file)
69+
self.assertIsNotNone(expected_hash,
70+
f"No hash comment '{self.HASH_COMMENT_PREFIX}' found in {last_migration_file}")
71+
self.assertTrue(expected_hash, "Extracted hash is empty.")
72+
73+
# 3. Calculate the hash of the target file
74+
self.assertTrue(os.path.exists(self.FILE_TO_CHECK_PATH),
75+
f"File to check does not exist: {self.FILE_TO_CHECK_PATH}")
76+
actual_hash = self._calculate_file_hash(self.FILE_TO_CHECK_PATH)
77+
78+
# 4. Compare the hashes
79+
self.assertEqual(expected_hash, actual_hash,
80+
f"Hash mismatch for '{os.path.basename(self.FILE_TO_CHECK_PATH)}'. "
81+
f"Expected: {expected_hash}, Got: {actual_hash} "
82+
f"If the feature_flags.yaml file changed, generate a new no-op migration file, and correctly set the FileHash.")

0 commit comments

Comments
 (0)