From c9102c1c6b65a53b390220855d794ff729028f73 Mon Sep 17 00:00:00 2001 From: Elliot Jordan Date: Sun, 12 Apr 2026 14:38:00 -0700 Subject: [PATCH 1/4] Bump version to 1.24.1 for development --- README.md | 8 ++++---- setup.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3ba55d6..aea8007 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ For any hook in this repo you wish to use, add the following to your pre-commit ```yaml - repo: https://github.com/homebysix/pre-commit-macadmin - rev: v1.24.0 + rev: v1.24.1 hooks: - id: check-plists # - id: ... @@ -147,7 +147,7 @@ When combining arguments that take lists (for example: `--required-keys`, `--cat ```yaml - repo: https://github.com/homebysix/pre-commit-macadmin - rev: v1.24.0 + rev: v1.24.1 hooks: - id: check-munki-pkgsinfo args: ['--catalogs', 'testing', 'stable', '--'] @@ -157,7 +157,7 @@ But if you also use the `--categories` argument, you would move the trailing `-- ```yaml - repo: https://github.com/homebysix/pre-commit-macadmin - rev: v1.24.0 + rev: v1.24.1 hooks: - id: check-munki-pkgsinfo args: ['--catalogs', 'testing', 'stable', '--categories', 'Design', 'Engineering', 'Web Browsers', '--'] @@ -169,7 +169,7 @@ If it looks better to your eye, feel free to use a multi-line list for long argu ```yaml - repo: https://github.com/homebysix/pre-commit-macadmin - rev: v1.24.0 + rev: v1.24.1 hooks: - id: check-munki-pkgsinfo args: [ diff --git a/setup.py b/setup.py index 319d263..38fd3c8 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ name="pre-commit-macadmin", description="Pre-commit hooks for Mac admins, client engineers, and IT consultants.", url="https://github.com/homebysix/pre-commit-macadmin", - version="1.24.0", + version="1.24.1", author="Elliot Jordan", author_email="elliot@elliotjordan.com", packages=["pre_commit_macadmin_hooks"], From 71ff7a78a5ede130742cbafb850f17a259f7f6d5 Mon Sep 17 00:00:00 2001 From: Elliot Jordan Date: Sun, 12 Apr 2026 14:39:51 -0700 Subject: [PATCH 2/4] Skip AutoPkg recipe type convention checks when type is unknown Closes #55 --- CHANGELOG.md | 4 ++- .../check_autopkg_recipes.py | 13 +++++++++ tests/test_check_autopkg_recipes.py | 28 +++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d50f6d1..51f8f43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,9 @@ All notable changes to this project will be documented in this file. This projec ## [Unreleased] -Nothing yet. +### Changed + +- Skipped AutoPkg recipe type convention checks when type is unknown. (#55) ## [1.24.0] - 2026-04-12 diff --git a/pre_commit_macadmin_hooks/check_autopkg_recipes.py b/pre_commit_macadmin_hooks/check_autopkg_recipes.py index 64604ab..89dc51d 100755 --- a/pre_commit_macadmin_hooks/check_autopkg_recipes.py +++ b/pre_commit_macadmin_hooks/check_autopkg_recipes.py @@ -451,6 +451,19 @@ def validate_proc_type_conventions(process, filename): ], } + # Extract all known recipe types from conventions + all_known_types = [] + for recipe_group in proc_type_conventions: + for recipe_type in recipe_group: + all_known_types.append(f".{recipe_type}.") + + # Skip validation if filename doesn't contain any known recipe type + if not any(known_type in filename for known_type in all_known_types): + print( + f"{filename}: WARNING: Unknown recipe type. Skipping processor convention checks." + ) + return True + passed = True processors = [x.get("Processor") for x in process] for recipe_group in proc_type_conventions: diff --git a/tests/test_check_autopkg_recipes.py b/tests/test_check_autopkg_recipes.py index d29224a..0b6da73 100644 --- a/tests/test_check_autopkg_recipes.py +++ b/tests/test_check_autopkg_recipes.py @@ -201,6 +201,34 @@ def test_validate_no_var_in_app_path_passes(self): result = target.validate_no_var_in_app_path(process, "file.recipe") self.assertTrue(result) + def test_validate_proc_type_conventions_unknown_type_passes(self): + # Unknown recipe type should skip validation and pass with warning + process = [{"Processor": "MunkiImporter"}] + with mock.patch("builtins.print") as mock_print: + result = target.validate_proc_type_conventions(process, "App.custom.recipe") + self.assertTrue(result) + mock_print.assert_called_with( + "App.custom.recipe: WARNING: Unknown recipe type. Skipping processor convention checks." + ) + + def test_validate_proc_type_conventions_known_type_wrong_processor_fails(self): + # Munki processor in a download recipe should fail + process = [{"Processor": "MunkiImporter"}] + with mock.patch("builtins.print") as mock_print: + result = target.validate_proc_type_conventions( + process, "App.download.recipe" + ) + self.assertFalse(result) + mock_print.assert_called_with( + "App.download.recipe: Processor MunkiImporter is not conventional for this recipe type." + ) + + def test_validate_proc_type_conventions_known_type_correct_processor_passes(self): + # Munki processor in a munki recipe should pass + process = [{"Processor": "MunkiImporter"}] + result = target.validate_proc_type_conventions(process, "App.munki.recipe") + self.assertTrue(result) + if __name__ == "__main__": unittest.main() From 9d562da5f6adb3878484fe65bf1ca67bcff84612 Mon Sep 17 00:00:00 2001 From: Elliot Jordan Date: Sun, 12 Apr 2026 15:22:52 -0700 Subject: [PATCH 3/4] Updated release docs and fixed version-skipping bug in workflow --- .github/workflows/release.yml | 90 ++++++++++++++++++++--------------- CHANGELOG.md | 9 +++- RELEASING.md | 55 ++++++++++++++++++--- 3 files changed, 108 insertions(+), 46 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a45ee2d..aaf3c7b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,6 +29,42 @@ jobs: echo "version=$(python ./setup.py --version)" >> $GITHUB_OUTPUT echo "Version detected: $(python ./setup.py --version)" + - name: Validate version alignment + run: | + VERSION="${{ steps.getversion.outputs.version }}" + ERRORS=0 + + # Check CHANGELOG.md has the version section + if ! grep -q "^## \[$VERSION\]" CHANGELOG.md; then + echo "::error file=CHANGELOG.md::CHANGELOG.md is missing a section for version [$VERSION]. The workflow extracts release notes from the CHANGELOG, so this section must exist before releasing. Expected format: '## [$VERSION] - YYYY-MM-DD'" + ERRORS=1 + fi + + # Check README.md has the version + if ! grep -q "rev: v$VERSION" README.md; then + CURRENT_README_VERSION=$(grep -m 1 "rev: v" README.md | sed -n 's/.*rev: v\([0-9.]*\).*/\1/p') + echo "::error file=README.md::README.md version (v$CURRENT_README_VERSION) does not match setup.py version ($VERSION). Update all 'rev: v' references in README.md to 'rev: v$VERSION' before releasing." + ERRORS=1 + fi + + # Check version link exists in CHANGELOG.md + if ! grep -q "^\[$VERSION\]:" CHANGELOG.md; then + echo "::error file=CHANGELOG.md::CHANGELOG.md is missing a version comparison link for [$VERSION]. Add '[$VERSION]: https://github.com/homebysix/pre-commit-macadmin/compare/vPREVIOUS...v$VERSION' at the bottom of CHANGELOG.md." + ERRORS=1 + fi + + if [ $ERRORS -gt 0 ]; then + echo "" + echo "❌ Version alignment check failed. Please ensure:" + echo " 1. CHANGELOG.md has a '## [$VERSION]' section with release notes" + echo " 2. README.md has 'rev: v$VERSION' in all examples" + echo " 3. CHANGELOG.md has a '[$VERSION]:' comparison link at the bottom" + echo "" + exit 1 + fi + + echo "✅ Version $VERSION is properly aligned across all files" + - name: Fetch tags run: git fetch --tags origin @@ -84,56 +120,32 @@ jobs: echo "next=$NEXT_VERSION" >> $GITHUB_OUTPUT echo "Next version will be: $NEXT_VERSION" - - name: Update setup.py version + - name: Merge main to dev if: steps.tagcheck.outputs.should_release == 'true' run: | - sed -i 's/version="${{ steps.getversion.outputs.version }}"/version="${{ steps.nextversion.outputs.next }}"/' setup.py + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git fetch origin dev + git checkout dev + git merge origin/main --no-edit - - name: Update README.md version + - name: Update setup.py version on dev if: steps.tagcheck.outputs.should_release == 'true' run: | - sed -i 's/rev: v${{ steps.getversion.outputs.version }}/rev: v${{ steps.nextversion.outputs.next }}/g' README.md + sed -i 's/version="${{ steps.getversion.outputs.version }}"/version="${{ steps.nextversion.outputs.next }}"/' setup.py - - name: Update CHANGELOG.md + - name: Update README.md version on dev if: steps.tagcheck.outputs.should_release == 'true' run: | - CURRENT="${{ steps.getversion.outputs.version }}" - NEXT="${{ steps.nextversion.outputs.next }}" - TODAY=$(date +%Y-%m-%d) - - # Create temp file with new changelog section - cat > /tmp/changelog_update.txt << 'ENDOFCHANGELOG' - ## [Unreleased] - - Nothing yet. - - ## [NEXT_VERSION] - TODAY_PLACEHOLDER - ENDOFCHANGELOG - - # Replace placeholders - sed -i "s/NEXT_VERSION/$NEXT/g" /tmp/changelog_update.txt - sed -i "s/TODAY_PLACEHOLDER/$TODAY/g" /tmp/changelog_update.txt - - # Insert new section after the first "## [Unreleased]" line - sed -i "/## \[Unreleased\]/r /tmp/changelog_update.txt" CHANGELOG.md - # Remove the old "## [Unreleased]" and "Nothing yet." lines - sed -i '1,/Nothing yet\./ { /## \[Unreleased\]/d; /Nothing yet\./d }' CHANGELOG.md - - # Update version comparison links at bottom - # Update [Unreleased] link - sed -i "s|\[Unreleased\]:.*|[Unreleased]: https://github.com/homebysix/pre-commit-macadmin/compare/v$NEXT...HEAD|" CHANGELOG.md - # Add new version link after [Unreleased] - sed -i "/\[Unreleased\]:/a [$NEXT]: https://github.com/homebysix/pre-commit-macadmin/compare/v$CURRENT...v$NEXT" CHANGELOG.md + sed -i 's/rev: v${{ steps.getversion.outputs.version }}/rev: v${{ steps.nextversion.outputs.next }}/g' README.md - - name: Commit version bump + - name: Commit version bump on dev if: steps.tagcheck.outputs.should_release == 'true' run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add setup.py README.md CHANGELOG.md - git commit -m "Bump to ${{ steps.nextversion.outputs.next }} [skip ci]" + git add setup.py README.md + git commit -m "Bump to ${{ steps.nextversion.outputs.next }}" - - name: Push changes + - name: Push dev branch if: steps.tagcheck.outputs.should_release == 'true' run: | - git push origin main + git push origin dev diff --git a/CHANGELOG.md b/CHANGELOG.md index 51f8f43..bf2c6f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,9 +12,15 @@ All notable changes to this project will be documented in this file. This projec ## [Unreleased] +Nothing yet. + +## [1.24.1] - 2026-04-12 + ### Changed - Skipped AutoPkg recipe type convention checks when type is unknown. (#55) +- Updated release documentation to reflect new automated workflow. +- Built error handling into release automation workflow. ## [1.24.0] - 2026-04-12 @@ -470,7 +476,8 @@ All notable changes to this project will be documented in this file. This projec - Initial release -[Unreleased]: https://github.com/homebysix/pre-commit-macadmin/compare/v1.24.0...HEAD +[Unreleased]: https://github.com/homebysix/pre-commit-macadmin/compare/v1.24.1...HEAD +[1.24.1]: https://github.com/homebysix/pre-commit-macadmin/compare/v1.24.0...v1.24.1 [1.24.0]: https://github.com/homebysix/pre-commit-macadmin/compare/v1.23.0...v1.24.0 [1.23.0]: https://github.com/homebysix/pre-commit-macadmin/compare/v1.22.0...v1.23.0 [1.22.0]: https://github.com/homebysix/pre-commit-macadmin/compare/v1.21.0...v1.22.0 diff --git a/RELEASING.md b/RELEASING.md index 9e33c53..9e04be2 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,15 +1,58 @@ # Releasing new versions of pre-commit-macadmin -1. Update the versions in __README.md__ and __setup.py__. +Releases are largely automated via GitHub Actions. The workflow triggers when `setup.py` is pushed to `main`, creating a GitHub release and automatically preparing the `dev` branch for the next release. -1. Check unit tests: +## Release Process + +1. On the `dev` branch, check unit tests: .venv/bin/python -m coverage run -m unittest discover -vs tests -1. Update the change log. +1. Prepare CHANGELOG.md for release by moving `[Unreleased]` changes to a new version section: + + ## [Unreleased] + + Nothing yet. + + ## [X.Y.Z] - YYYY-MM-DD + + ### Added/Changed/Fixed/Removed (separate sections as applicable) + - New features, changes, fixes, or removals + + The release workflow will extract this section for the GitHub release notes. + +1. Add the version comparison link at the bottom of CHANGELOG.md: + + [X.Y.Z]: https://github.com/homebysix/pre-commit-macadmin/compare/vPREVIOUS...vX.Y.Z + +1. Update the version in `setup.py` to match the CHANGELOG version (e.g., `2.3.6`). + +1. Update the version in `README.md` examples to match (e.g., `rev: v2.3.6`). + +1. Commit these changes to `dev` and push. + +1. Merge `dev` branch to `main`. + +1. The release workflow will automatically: + - Detect the version from `setup.py` (e.g., `2.3.6`) + - Create a GitHub release with tag `v2.3.6` + - Extract release notes from CHANGELOG.md + - Merge `main` back to `dev` + - Bump `dev` versions to the next patch version (e.g., `2.3.7`) in `setup.py` and `README.md` + - Commit and push the updated version to `dev` + + Note: CHANGELOG.md is NOT automatically updated on `dev`. Add entries to the `[Unreleased]` section as you make changes. + +1. Pull the updated `dev` branch to continue development at the new version. + +1. As you make changes, add entries to the `[Unreleased]` section of CHANGELOG.md. When ready for the next release, simply promote those changes to a new version section (repeat from step 2) - no need to manually bump versions unless you want to change to a minor or major version. + +1. After each release, verify on GitHub and run `pre-commit autoupdate` on a test repo to confirm it updates correctly. + +## Version Numbering -1. Merge development branch to main. +The workflow automatically bumps the **patch** version (X.Y.Z → X.Y.Z+1). If you need to bump the **minor** or **major** version, manually update the version numbers in `setup.py`, `README.md`, and CHANGELOG.md before merging to `main`. -1. Create a GitHub release with version tag, prefixed with `v`. (For example: `v2.3.4`) +## Pre-releases -1. Run `pre-commit autoupdate` on a test repo and confirm it updates to the new version. +If a pre-release is desired for testing purposes, it must be done manually. From 23000c31f1d224916d5041de1cb1a76baeee4f11 Mon Sep 17 00:00:00 2001 From: Elliot Jordan Date: Sun, 12 Apr 2026 15:24:16 -0700 Subject: [PATCH 4/4] Add tests for validate_required_proc_for_types --- tests/test_check_autopkg_recipes.py | 168 ++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) diff --git a/tests/test_check_autopkg_recipes.py b/tests/test_check_autopkg_recipes.py index 0b6da73..ed83944 100644 --- a/tests/test_check_autopkg_recipes.py +++ b/tests/test_check_autopkg_recipes.py @@ -229,6 +229,174 @@ def test_validate_proc_type_conventions_known_type_correct_processor_passes(self result = target.validate_proc_type_conventions(process, "App.munki.recipe") self.assertTrue(result) + def test_validate_required_proc_for_types_munki_with_importer_passes(self): + # Munki recipe with MunkiImporter should pass + process = [{"Processor": "MunkiImporter"}] + result = target.validate_required_proc_for_types(process, "App.munki.recipe") + self.assertTrue(result) + + def test_validate_required_proc_for_types_munki_without_importer_fails(self): + # Munki recipe without MunkiImporter should fail + process = [{"Processor": "URLDownloader"}] + with mock.patch("builtins.print") as mock_print: + result = target.validate_required_proc_for_types( + process, "App.munki.recipe" + ) + self.assertFalse(result) + mock_print.assert_called_with( + "App.munki.recipe: Recipe type munki should contain processor MunkiImporter." + ) + + def test_validate_required_proc_for_types_pkg_with_creator_passes(self): + # Pkg recipe with PkgCreator should pass + process = [{"Processor": "PkgCreator"}] + result = target.validate_required_proc_for_types(process, "App.pkg.recipe") + self.assertTrue(result) + + def test_validate_required_proc_for_types_pkg_without_creator_fails(self): + # Pkg recipe without required processor should fail + process = [{"Processor": "URLDownloader"}] + with mock.patch("builtins.print") as mock_print: + result = target.validate_required_proc_for_types(process, "App.pkg.recipe") + self.assertFalse(result) + mock_print.assert_called_with( + "App.pkg.recipe: Recipe type pkg should contain one of these processors: ['AppPkgCreator', 'PkgCreator', 'PkgCopier']." + ) + + def test_validate_required_proc_for_types_pkg_empty_process_passes(self): + # Pkg recipe with empty process list should pass (special case) + process = [] + result = target.validate_required_proc_for_types(process, "App.pkg.recipe") + self.assertTrue(result) + + def test_validate_required_proc_for_types_jss_with_importer_passes(self): + # JSS recipe with JSSImporter should pass + process = [{"Processor": "JSSImporter"}] + result = target.validate_required_proc_for_types(process, "App.jss.recipe") + self.assertTrue(result) + + def test_validate_required_proc_for_types_unknown_type_passes(self): + # Unknown recipe type should pass (no checks) + process = [{"Processor": "SomeProcessor"}] + result = target.validate_required_proc_for_types(process, "App.unknown.recipe") + self.assertTrue(result) + + def test_validate_proc_args_valid_arguments_passes(self): + # Valid arguments for a core processor should pass + # Skip if autopkglib is not available + if not target.HAS_AUTOPKGLIB: + self.skipTest("AutoPkg library not available") + + # Mock the AutoPkg library functions + mock_proc = mock.Mock() + mock_proc.input_variables = {"url": {}, "filename": {}} + + with mock.patch.object( + target, "processor_names", return_value=["URLDownloader"] + ), mock.patch.object(target, "get_processor", return_value=mock_proc): + process = [ + { + "Processor": "URLDownloader", + "Arguments": {"url": "https://example.com/file.dmg"}, + } + ] + result = target.validate_proc_args(process, "App.download.recipe") + self.assertTrue(result) + + def test_validate_proc_args_invalid_argument_fails(self): + # Invalid argument for a core processor should fail + if not target.HAS_AUTOPKGLIB: + self.skipTest("AutoPkg library not available") + + mock_proc = mock.Mock() + mock_proc.input_variables = {"url": {}, "filename": {}} + + with mock.patch.object( + target, "processor_names", return_value=["URLDownloader"] + ), mock.patch.object( + target, "get_processor", return_value=mock_proc + ), mock.patch( + "builtins.print" + ) as mock_print: + process = [ + { + "Processor": "URLDownloader", + "Arguments": {"invalid_arg": "value"}, + } + ] + result = target.validate_proc_args(process, "App.download.recipe") + self.assertFalse(result) + # Check that the error message contains the key info + calls = mock_print.call_args_list + self.assertEqual(len(calls), 2) # Error message + suggestion + self.assertIn("Unknown argument invalid_arg", str(calls[0])) + + def test_validate_proc_args_ignored_arguments_passes(self): + # Ignored arguments like "note" should pass + if not target.HAS_AUTOPKGLIB: + self.skipTest("AutoPkg library not available") + + mock_proc = mock.Mock() + mock_proc.input_variables = {"url": {}, "filename": {}} + + with mock.patch.object( + target, "processor_names", return_value=["URLDownloader"] + ), mock.patch.object(target, "get_processor", return_value=mock_proc): + process = [ + { + "Processor": "URLDownloader", + "Arguments": { + "url": "https://example.com/file.dmg", + "note": "This is a note", + }, + } + ] + result = target.validate_proc_args(process, "App.download.recipe") + self.assertTrue(result) + + def test_validate_proc_args_non_core_processor_passes(self): + # Non-core processors should be skipped + if not target.HAS_AUTOPKGLIB: + self.skipTest("AutoPkg library not available") + + with mock.patch.object( + target, "processor_names", return_value=["URLDownloader"] + ), mock.patch.object(target, "get_processor", return_value=mock.Mock()): + process = [ + { + "Processor": "com.github.custom.CustomProcessor", + "Arguments": {"any_arg": "value"}, + } + ] + result = target.validate_proc_args(process, "App.download.recipe") + self.assertTrue(result) + + def test_validate_proc_args_processor_with_no_args_fails(self): + # Processor that doesn't accept arguments but receives one should fail + if not target.HAS_AUTOPKGLIB: + self.skipTest("AutoPkg library not available") + + mock_proc = mock.Mock() + mock_proc.input_variables = {} # No input variables + + with mock.patch.object( + target, "processor_names", return_value=["StopProcessingIf"] + ), mock.patch.object( + target, "get_processor", return_value=mock_proc + ), mock.patch( + "builtins.print" + ) as mock_print: + process = [ + { + "Processor": "StopProcessingIf", + "Arguments": {"invalid_arg": "value"}, + } + ] + result = target.validate_proc_args(process, "App.download.recipe") + self.assertFalse(result) + calls = mock_print.call_args_list + self.assertGreater(len(calls), 0) + if __name__ == "__main__": unittest.main()