From 25b95b45423dcd595eb120ef6e41b42a01d03afe Mon Sep 17 00:00:00 2001 From: Karel Bemelmans Date: Wed, 26 Nov 2025 10:03:49 +0100 Subject: [PATCH 1/4] wip --- neo/neo.py | 64 ++++++++++++++++++++++++++++++++++++++++------------ neo/tests.py | 45 ++++++++++++++++++++++++++++++++---- 2 files changed, 91 insertions(+), 18 deletions(-) diff --git a/neo/neo.py b/neo/neo.py index f6828a8..599867f 100755 --- a/neo/neo.py +++ b/neo/neo.py @@ -10,10 +10,17 @@ import re from urllib.parse import quote_plus -from common import env_default, hdict, strtobool +try: + from .common import env_default, hdict, strtobool +except ImportError: + from common import env_default, hdict, strtobool -def update_matches(files, include_regex, old_matches=defaultdict(set), ): +def update_matches( + files, + include_regex, + old_matches=defaultdict(set), +): """ The update_matches function takes a list of files and their statuses, and returns a dictionary mapping the job matrix keys to sets of statuses. @@ -25,7 +32,7 @@ def update_matches(files, include_regex, old_matches=defaultdict(set), ): :return: A dictionary of dictionaries """ matches = defaultdict(set) - for (filename, status) in files: + for filename, status in files: match = include_regex.match(filename) if match: if match.groupdict(): @@ -90,10 +97,10 @@ def generate_matrix( for path, _, files in os.walk(default_dir) for f in files ] - matches = update_matches( default_files, include_regex, matches) + matches = update_matches(default_files, include_regex, matches) # mark matrix entries with a status if all its matches have the same status status_matrix = [] - for (groups, statuses) in matches.items(): + for groups, statuses in matches.items(): groups["reason"] = statuses.pop() if len(statuses) == 1 else "updated" status_matrix.append(groups) @@ -114,6 +121,18 @@ def main( if default_patterns is None: default_patterns = [] + + # Check if this is a workflow_dispatch event + github_event_name = os.getenv("GITHUB_EVENT_NAME", None) + + # For workflow_dispatch events, use default behavior (list all matched directories) + if github_event_name == "workflow_dispatch": + logging.info( + "workflow_dispatch event detected, using default behavior to list all matched directories" + ) + # Pass empty files list, but force defaults=True to trigger directory listing behavior + return generate_matrix([], include_regex, True, default_patterns) + with requests.session() as session: session.hooks = { "response": lambda resp, *resp_args, **kwargs: resp.raise_for_status() @@ -159,18 +178,33 @@ def github_webhook_ref(dest: str, option_strings: list): github_event = json.load(fp) if github_event_name == "pull_request": return argparse.Action( - default=github_event["pull_request"]["head"]["sha"] - if is_github_head_ref - else github_event["pull_request"]["base"]["sha"], + default=( + github_event["pull_request"]["head"]["sha"] + if is_github_head_ref + else github_event["pull_request"]["base"]["sha"] + ), required=False, dest=dest, option_strings=option_strings, ) elif github_event_name == "push": return argparse.Action( - default=github_event["after"] - if is_github_head_ref - else github_event["before"], + default=( + github_event["after"] + if is_github_head_ref + else github_event["before"] + ), + required=False, + dest=dest, + option_strings=option_strings, + ) + elif github_event_name == "workflow_dispatch": + # For workflow_dispatch, we use the default branch ref + # since this is a manual trigger without specific commit comparison + return argparse.Action( + default=github_event.get("ref", "refs/heads/main").replace( + "refs/heads/", "" + ), required=False, dest=dest, option_strings=option_strings, @@ -237,9 +271,11 @@ def set_github_actions_output(generated_matrix: list) -> None: args = vars(parser.parse_args()) logging.basicConfig( - level=logging.DEBUG - if os.getenv("NEO_LOG_LEVEL", "INFO") == "DEBUG" - else logging.INFO + level=( + logging.DEBUG + if os.getenv("NEO_LOG_LEVEL", "INFO") == "DEBUG" + else logging.INFO + ) ) matrix = main(**args) diff --git a/neo/tests.py b/neo/tests.py index c6600e6..51c5b80 100755 --- a/neo/tests.py +++ b/neo/tests.py @@ -37,8 +37,8 @@ def test_no_changes_with_default_pattern(self): ], ), [ - {'environment': 'live', 'reason': 'default'}, - {'environment': 'staging', 'reason': 'default'} + {"environment": "live", "reason": "default"}, + {"environment": "staging", "reason": "default"}, ], ) @@ -58,8 +58,8 @@ def test_changes_with_default_pattern(self): ], ), [ - {'environment': 'live', 'reason': 'default'}, - {'environment': 'staging', 'reason': 'modified'} + {"environment": "live", "reason": "default"}, + {"environment": "staging", "reason": "modified"}, ], ) @@ -203,6 +203,43 @@ def test_github_outputs(self): self.assertIn(f"matrix={expected_matrix_output}", output) self.assertIn(f"matrix-length=3", output) + def test_workflow_dispatch_behavior(self): + """Test that workflow_dispatch events use default behavior to list all matched directories""" + with tempfile.TemporaryDirectory() as d: + # Create test directory structure + staging_dir = os.path.join(d, "staging") + live_dir = os.path.join(d, "live") + os.makedirs(staging_dir) + os.makedirs(live_dir) + Path(os.path.join(staging_dir, "config.yaml")).touch() + Path(os.path.join(live_dir, "config.yaml")).touch() + + # Simulate workflow_dispatch event by setting environment variable + original_event = os.getenv("GITHUB_EVENT_NAME") + os.environ["GITHUB_EVENT_NAME"] = "workflow_dispatch" + + try: + # Test that workflow_dispatch triggers default behavior (listing all files) + result = neo.generate_matrix( + files=[], # Empty files list to simulate workflow_dispatch + include_regex="(?Pstaging|live)", + defaults=True, # This should be forced for workflow_dispatch + default_patterns=[], + default_dir=d, # Use our test directory + ) + + # Should return entries for both staging and live environments + environments = [entry.get("environment") for entry in result] + self.assertIn("staging", environments) + self.assertIn("live", environments) + + finally: + # Restore original environment + if original_event is None: + os.environ.pop("GITHUB_EVENT_NAME", None) + else: + os.environ["GITHUB_EVENT_NAME"] = original_event + class IntegrationTest(unittest.TestCase): empty_repo_commit_sha = "6b5794416e6750d16fb126a04eadb681349e6947" From 49e47b93532054e9ce54b2b08b8103b7cdefc474 Mon Sep 17 00:00:00 2001 From: Karel Bemelmans Date: Wed, 26 Nov 2025 10:16:47 +0100 Subject: [PATCH 2/4] Added .vscode folder to ensure same settings get applied --- .gitignore | 1 - .vscode/extensions.json | 8 ++++++++ .vscode/settings.json | 8 ++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json diff --git a/.gitignore b/.gitignore index 0c232bb..bee8a64 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ __pycache__ -.vscode/* diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..42022c0 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "ms-python.python", + "ms-python.black-formatter", + "tamasfe.even-better-toml", + "esbenp.prettier-vscode" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..85789c0 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter" + }, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.trimAutoWhitespace": true +} From 0bdaf222e32a8b7e1da832d42c5f4a1eab48006e Mon Sep 17 00:00:00 2001 From: Karel Bemelmans Date: Wed, 26 Nov 2025 10:18:10 +0100 Subject: [PATCH 3/4] Just stick to one way of doing imports --- neo/neo.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/neo/neo.py b/neo/neo.py index 599867f..e1f0329 100755 --- a/neo/neo.py +++ b/neo/neo.py @@ -10,10 +10,7 @@ import re from urllib.parse import quote_plus -try: - from .common import env_default, hdict, strtobool -except ImportError: - from common import env_default, hdict, strtobool +from common import env_default, hdict, strtobool def update_matches( From 839ae8bb56c5e685c728d010682b167fbd4f40a2 Mon Sep 17 00:00:00 2001 From: Karel Bemelmans Date: Thu, 27 Nov 2025 20:36:11 +0100 Subject: [PATCH 4/4] Add support for schedule actions --- neo/neo.py | 24 +++++++++++++++++++----- neo/tests.py | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/neo/neo.py b/neo/neo.py index e1f0329..6853713 100755 --- a/neo/neo.py +++ b/neo/neo.py @@ -10,7 +10,10 @@ import re from urllib.parse import quote_plus -from common import env_default, hdict, strtobool +try: + from .common import env_default, hdict, strtobool +except ImportError: + from common import env_default, hdict, strtobool def update_matches( @@ -119,13 +122,13 @@ def main( if default_patterns is None: default_patterns = [] - # Check if this is a workflow_dispatch event + # Check if this is a workflow_dispatch or schedule event github_event_name = os.getenv("GITHUB_EVENT_NAME", None) - # For workflow_dispatch events, use default behavior (list all matched directories) - if github_event_name == "workflow_dispatch": + # For workflow_dispatch and schedule events, use default behavior (list all matched directories) + if github_event_name in ["workflow_dispatch", "schedule"]: logging.info( - "workflow_dispatch event detected, using default behavior to list all matched directories" + f"{github_event_name} event detected, using default behavior to list all matched directories" ) # Pass empty files list, but force defaults=True to trigger directory listing behavior return generate_matrix([], include_regex, True, default_patterns) @@ -206,6 +209,17 @@ def github_webhook_ref(dest: str, option_strings: list): dest=dest, option_strings=option_strings, ) + elif github_event_name == "schedule": + # For scheduled events, we use the default branch ref + # since this is a time-based trigger without specific commit comparison + return argparse.Action( + default=github_event.get("ref", "refs/heads/main").replace( + "refs/heads/", "" + ), + required=False, + dest=dest, + option_strings=option_strings, + ) else: raise NotImplementedError( f"unsupported github event {github_event_name}" diff --git a/neo/tests.py b/neo/tests.py index 51c5b80..0a93255 100755 --- a/neo/tests.py +++ b/neo/tests.py @@ -240,6 +240,43 @@ def test_workflow_dispatch_behavior(self): else: os.environ["GITHUB_EVENT_NAME"] = original_event + def test_schedule_behavior(self): + """Test that schedule events use default behavior to list all matched directories""" + with tempfile.TemporaryDirectory() as d: + # Create test directory structure + staging_dir = os.path.join(d, "staging") + live_dir = os.path.join(d, "live") + os.makedirs(staging_dir) + os.makedirs(live_dir) + Path(os.path.join(staging_dir, "config.yaml")).touch() + Path(os.path.join(live_dir, "config.yaml")).touch() + + # Simulate schedule event by setting environment variable + original_event = os.getenv("GITHUB_EVENT_NAME") + os.environ["GITHUB_EVENT_NAME"] = "schedule" + + try: + # Test that schedule triggers default behavior (listing all files) + result = neo.generate_matrix( + files=[], # Empty files list to simulate schedule + include_regex="(?Pstaging|live)", + defaults=True, # This should be forced for schedule + default_patterns=[], + default_dir=d, # Use our test directory + ) + + # Should return entries for both staging and live environments + environments = [entry.get("environment") for entry in result] + self.assertIn("staging", environments) + self.assertIn("live", environments) + + finally: + # Restore original environment + if original_event is None: + os.environ.pop("GITHUB_EVENT_NAME", None) + else: + os.environ["GITHUB_EVENT_NAME"] = original_event + class IntegrationTest(unittest.TestCase): empty_repo_commit_sha = "6b5794416e6750d16fb126a04eadb681349e6947"