autopkg-wrapper 2025.11.1__tar.gz → 2026.2.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. {autopkg_wrapper-2025.11.1 → autopkg_wrapper-2026.2.5}/.github/workflows/build-publish.yml +17 -20
  2. {autopkg_wrapper-2025.11.1 → autopkg_wrapper-2026.2.5}/.github/workflows/codeql.yml +3 -3
  3. {autopkg_wrapper-2025.11.1 → autopkg_wrapper-2026.2.5}/.github/workflows/dependency-review.yml +1 -1
  4. autopkg_wrapper-2026.2.5/PKG-INFO +107 -0
  5. autopkg_wrapper-2026.2.5/README.md +88 -0
  6. autopkg_wrapper-2026.2.5/actions-demo/requirements.txt +3 -0
  7. {autopkg_wrapper-2025.11.1 → autopkg_wrapper-2026.2.5}/autopkg_wrapper/autopkg_wrapper.py +103 -52
  8. {autopkg_wrapper-2025.11.1 → autopkg_wrapper-2026.2.5}/autopkg_wrapper/notifier/slack.py +1 -1
  9. {autopkg_wrapper-2025.11.1 → autopkg_wrapper-2026.2.5}/autopkg_wrapper/utils/args.py +70 -0
  10. {autopkg_wrapper-2025.11.1 → autopkg_wrapper-2026.2.5}/autopkg_wrapper/utils/git_functions.py +20 -5
  11. autopkg_wrapper-2026.2.5/autopkg_wrapper/utils/recipe_ordering.py +149 -0
  12. autopkg_wrapper-2026.2.5/autopkg_wrapper/utils/report_processor.py +674 -0
  13. autopkg_wrapper-2026.2.5/mise.toml +25 -0
  14. {autopkg_wrapper-2025.11.1 → autopkg_wrapper-2026.2.5}/pyproject.toml +3 -2
  15. autopkg_wrapper-2026.2.5/tests/test_args_utils.py +79 -0
  16. autopkg_wrapper-2026.2.5/tests/test_autopkg_commands.py +89 -0
  17. autopkg_wrapper-2026.2.5/tests/test_git_functions.py +81 -0
  18. autopkg_wrapper-2026.2.5/tests/test_order_recipe_list.py +86 -0
  19. autopkg_wrapper-2026.2.5/tests/test_parse_recipe_list.py +120 -0
  20. autopkg_wrapper-2026.2.5/tests/test_report_processor.py +121 -0
  21. autopkg_wrapper-2026.2.5/tests/test_setup_logger.py +26 -0
  22. autopkg_wrapper-2026.2.5/tests/test_slack_notifier.py +84 -0
  23. autopkg_wrapper-2026.2.5/uv.lock +418 -0
  24. autopkg_wrapper-2025.11.1/.tool-versions +0 -1
  25. autopkg_wrapper-2025.11.1/PKG-INFO +0 -54
  26. autopkg_wrapper-2025.11.1/README.md +0 -36
  27. autopkg_wrapper-2025.11.1/actions-demo/requirements.txt +0 -3
  28. autopkg_wrapper-2025.11.1/uv.lock +0 -279
  29. {autopkg_wrapper-2025.11.1 → autopkg_wrapper-2026.2.5}/.github/dependabot.yml +0 -0
  30. {autopkg_wrapper-2025.11.1 → autopkg_wrapper-2026.2.5}/.gitignore +0 -0
  31. {autopkg_wrapper-2025.11.1 → autopkg_wrapper-2026.2.5}/.pre-commit-config.yaml +0 -0
  32. {autopkg_wrapper-2025.11.1 → autopkg_wrapper-2026.2.5}/CONTRIBUTING +0 -0
  33. {autopkg_wrapper-2025.11.1 → autopkg_wrapper-2026.2.5}/LICENSE +0 -0
  34. {autopkg_wrapper-2025.11.1 → autopkg_wrapper-2026.2.5}/actions-demo/.github/workflows/autopkg-wrapper-demo.yml +0 -0
  35. {autopkg_wrapper-2025.11.1 → autopkg_wrapper-2026.2.5}/actions-demo/overrides/Google_Chrome.pkg.recipe.yaml +0 -0
  36. {autopkg_wrapper-2025.11.1 → autopkg_wrapper-2026.2.5}/actions-demo/repo_list.txt +0 -0
  37. {autopkg_wrapper-2025.11.1 → autopkg_wrapper-2026.2.5}/autopkg_wrapper/__init__.py +0 -0
  38. {autopkg_wrapper-2025.11.1 → autopkg_wrapper-2026.2.5}/autopkg_wrapper/notifier/__init__.py +0 -0
  39. {autopkg_wrapper-2025.11.1 → autopkg_wrapper-2026.2.5}/autopkg_wrapper/utils/__init__.py +0 -0
  40. {autopkg_wrapper-2025.11.1 → autopkg_wrapper-2026.2.5}/autopkg_wrapper/utils/logging.py +0 -0
  41. {autopkg_wrapper-2025.11.1 → autopkg_wrapper-2026.2.5}/tests/__init__.py +0 -0
  42. {autopkg_wrapper-2025.11.1 → autopkg_wrapper-2026.2.5}/tests/prefs.json +0 -0
  43. {autopkg_wrapper-2025.11.1 → autopkg_wrapper-2026.2.5}/tests/prefs.plist +0 -0
  44. {autopkg_wrapper-2025.11.1 → autopkg_wrapper-2026.2.5}/tests/recipe_list.json +0 -0
  45. {autopkg_wrapper-2025.11.1 → autopkg_wrapper-2026.2.5}/tests/recipe_list.txt +0 -0
  46. {autopkg_wrapper-2025.11.1 → autopkg_wrapper-2026.2.5}/tests/recipe_list.yaml +0 -0
@@ -34,7 +34,6 @@ jobs:
34
34
  contents: write
35
35
 
36
36
  steps:
37
- # - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
38
37
  - id: check-inputs
39
38
  env:
40
39
  INPUT_DRY_RUN: ${{ github.event.inputs.dry_run }}
@@ -104,26 +103,24 @@ jobs:
104
103
  id-token: write
105
104
 
106
105
  steps:
107
- - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
106
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
108
107
 
109
- - name: Setup UV
110
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
108
+ - name: Setup mise
109
+ uses: jdx/mise-action@6d1e696aa24c1aa1bcc1adea0212707c71ab78a8 # v3.6.1
111
110
  with:
112
- activate-environment: true
113
- enable-cache: true
114
- cache-dependency-glob: uv.lock
111
+ install: true
112
+ cache: true
115
113
 
116
- - name: Setup Python
117
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
118
- with:
119
- python-version-file: pyproject.toml
114
+ - name: Run tests
115
+ run: mise run test
116
+
117
+ - name: Build package
118
+ env:
119
+ RELEASE_VERSION: ${{ needs.release.outputs.version }}
120
+ run: mise run build
120
121
 
121
- - name: Build Package with UV
122
- run: |
123
- uv version ${{ needs.release.outputs.version }}
124
- uv build
125
122
  - name: Upload Package Artifacts
126
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
123
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
127
124
  with:
128
125
  name: python-package-distributions
129
126
  path: dist/
@@ -142,7 +139,7 @@ jobs:
142
139
 
143
140
  steps:
144
141
  - name: Download Package Artifacts
145
- uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
142
+ uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
146
143
  with:
147
144
  name: python-package-distributions
148
145
  path: dist/
@@ -166,7 +163,7 @@ jobs:
166
163
 
167
164
  steps:
168
165
  - name: Download Package Artifacts
169
- uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
166
+ uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
170
167
  with:
171
168
  name: python-package-distributions
172
169
  path: dist/
@@ -185,11 +182,11 @@ jobs:
185
182
 
186
183
  steps:
187
184
  - name: Download Package Artifacts
188
- uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
185
+ uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
189
186
  with:
190
187
  name: python-package-distributions
191
188
  path: dist/
192
- - uses: sigstore/gh-action-sigstore-python@f832326173235dcb00dd5d92cd3f353de3188e6c # v3.1.0
189
+ - uses: sigstore/gh-action-sigstore-python@a5caf349bc536fbef3668a10ed7f5cd309a4b53d # v3.2.0
193
190
  with:
194
191
  inputs: |
195
192
  dist/*.whl
@@ -27,14 +27,14 @@ jobs:
27
27
 
28
28
  steps:
29
29
  - name: Checkout repository
30
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
30
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
31
31
 
32
32
  - name: Initialize CodeQL
33
- uses: github/codeql-action/init@382a50a0284c0de445104889a9d6003acb4b3c1d # v2.15.4
33
+ uses: github/codeql-action/init@25a224b8085c21d4d61b7fc051468805fc3ac490 # codeql-bundle-v2.24.0
34
34
  with:
35
35
  languages: ${{ matrix.language }}
36
36
 
37
37
  - name: Perform CodeQL Analysis
38
- uses: github/codeql-action/analyze@382a50a0284c0de445104889a9d6003acb4b3c1d # v2.15.4
38
+ uses: github/codeql-action/analyze@25a224b8085c21d4d61b7fc051468805fc3ac490 # codeql-bundle-v2.24.0
39
39
  with:
40
40
  category: "/language:${{matrix.language}}"
@@ -11,7 +11,7 @@ jobs:
11
11
  runs-on: ubuntu-latest
12
12
  steps:
13
13
  - name: "Checkout Repository"
14
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
14
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
15
15
 
16
16
  - name: "Dependency Review"
17
17
  uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2
@@ -0,0 +1,107 @@
1
+ Metadata-Version: 2.4
2
+ Name: autopkg-wrapper
3
+ Version: 2026.2.5
4
+ Summary: A package used to execute some autopkg functions, primarily within the context of a GitHub Actions runner.
5
+ Project-URL: Repository, https://github.com/smithjw/autopkg-wrapper
6
+ Author-email: James Smith <james@smithjw.me>
7
+ License-Expression: BSD-3-Clause
8
+ License-File: LICENSE
9
+ Requires-Python: ~=3.14.0
10
+ Requires-Dist: chardet
11
+ Requires-Dist: idna
12
+ Requires-Dist: jamf-pro-sdk
13
+ Requires-Dist: pygithub
14
+ Requires-Dist: requests
15
+ Requires-Dist: ruamel-yaml
16
+ Requires-Dist: toml
17
+ Requires-Dist: urllib3
18
+ Description-Content-Type: text/markdown
19
+
20
+ # autopkg-wrapper
21
+
22
+ `autopkg_wrapper` is a small package that can be used to run [`autopkg`](https://github.com/autopkg/autopkg) within CI/CD environments such as GitHub Actions.
23
+
24
+ The easiest way to run it is by installing with pip.
25
+
26
+ ```shell
27
+ pip install autopkg-wrapper
28
+ ```
29
+
30
+ ## Command Line Parameters
31
+
32
+ ```shell
33
+ -h, --help Show this help message and exit
34
+ --recipe-file RECIPE_FILE Path to a list of recipes to run (cannot be run with --recipes)
35
+ --recipes [RECIPES ...] Recipes to run with autopkg (cannot be run with --recipe-file)
36
+ --recipe-processing-order [RECIPE_PROCESSING_ORDER ...]
37
+ Optional processing order for recipe "types" (suffix segments after the first '.'); supports partial tokens like upload/auto_update; env var AW_RECIPE_PROCESSING_ORDER expects comma-separated values
38
+ --debug Enable debug logging when running script
39
+ --disable-recipe-trust-check If this option is used, recipe trust verification will not be run prior to a recipe run.
40
+ --github-token GITHUB_TOKEN A token used to publish a PR to your GitHub repo if overrides require their trust to be updated
41
+ --branch-name BRANCH_NAME Branch name to be used where recipe overrides have failed their trust verification and need to be updated.
42
+ By default, this will be in the format of "fix/update_trust_information/YYYY-MM-DDTHH-MM-SS"
43
+ --create-pr If enabled, autopkg_wrapper will open a PR for updated trust information
44
+ --create-issues Create a GitHub issue for recipes that fail during processing
45
+ --disable-git-commands If this option is used, git commands won't be run
46
+ --post-processors [POST_PROCESSORS ...]
47
+ One or more autopkg post processors to run after each recipe execution
48
+ --autopkg-prefs AW_AUTOPKG_PREFS_FILE
49
+ Path to the autopkg preferences you'd like to use
50
+ --overrides-repo-path AUTOPKG_OVERRIDES_REPO_PATH
51
+ The path on disk to the git repository containing the autopkg overrides directory. If none is provided, we will try to determine it for you.
52
+ --concurrency CONCURRENCY Number of recipes to run in parallel (default: 1)
53
+ --process-reports Process autopkg report directories or zip and emit markdown summaries (runs after recipes complete)
54
+ --reports-zip REPORTS_ZIP Path to an autopkg_report-*.zip to extract and process
55
+ --reports-extract-dir REPORTS_EXTRACT_DIR
56
+ Directory to extract the zip into (default: autopkg_reports_summary/reports)
57
+ --reports-dir REPORTS_DIR Directory of reports to process (if no zip provided). Defaults to /private/tmp/autopkg when processing after a run
58
+ --reports-out-dir REPORTS_OUT_DIR
59
+ Directory to write markdown outputs (default: autopkg_reports_summary/summary)
60
+ --reports-run-date REPORTS_RUN_DATE
61
+ Run date string to include in the summary
62
+ --reports-strict Exit non-zero if any errors are detected in processed reports
63
+ ```
64
+
65
+ ## Examples
66
+
67
+ Run recipes (serial):
68
+
69
+ ```bash
70
+ autopkg_wrapper --recipes Foo.download Bar.download
71
+ ```
72
+
73
+ Run 3 recipes concurrently and process reports afterward:
74
+
75
+ ```bash
76
+ autopkg_wrapper \
77
+ --recipe-file /path/to/recipe_list.txt \
78
+ --concurrency 3 \
79
+ --disable-git-commands \
80
+ --process-reports \
81
+ --reports-out-dir /tmp/autopkg_reports_summary \
82
+ --reports-strict
83
+ ```
84
+
85
+ Process a reports zip explicitly (no recipe run):
86
+
87
+ ```bash
88
+ autopkg_wrapper \
89
+ --process-reports \
90
+ --reports-zip /path/to/autopkg_report-2026-02-02.zip \
91
+ --reports-extract-dir /tmp/autopkg_reports \
92
+ --reports-out-dir /tmp/autopkg_reports_summary
93
+ ```
94
+
95
+ Notes:
96
+
97
+ - During recipe runs, per‑recipe plist reports are written to `/private/tmp/autopkg`.
98
+ - When `--process-reports` is supplied without `--reports-zip` or `--reports-dir`, the tool processes `/private/tmp/autopkg`.
99
+ - If `AUTOPKG_JSS_URL`, `AUTOPKG_CLIENT_ID`, and `AUTOPKG_CLIENT_SECRET` are set, uploaded package rows are enriched with Jamf package links.
100
+ - No extra CLI flag is required; enrichment runs automatically when all three env vars are present.
101
+
102
+ An example folder structure and GitHub Actions Workflow is available within the [`actions-demo`](actions-demo)
103
+
104
+ ## Credits
105
+
106
+ - [`autopkg_tools` from Facebook](https://github.com/facebook/IT-CPE/tree/main/legacy/autopkg_tools)
107
+ - [`autopkg_tools` from Facebook, modified by Gusto](https://github.com/Gusto/it-cpe-opensource/tree/main/autopkg)
@@ -0,0 +1,88 @@
1
+ # autopkg-wrapper
2
+
3
+ `autopkg_wrapper` is a small package that can be used to run [`autopkg`](https://github.com/autopkg/autopkg) within CI/CD environments such as GitHub Actions.
4
+
5
+ The easiest way to run it is by installing with pip.
6
+
7
+ ```shell
8
+ pip install autopkg-wrapper
9
+ ```
10
+
11
+ ## Command Line Parameters
12
+
13
+ ```shell
14
+ -h, --help Show this help message and exit
15
+ --recipe-file RECIPE_FILE Path to a list of recipes to run (cannot be run with --recipes)
16
+ --recipes [RECIPES ...] Recipes to run with autopkg (cannot be run with --recipe-file)
17
+ --recipe-processing-order [RECIPE_PROCESSING_ORDER ...]
18
+ Optional processing order for recipe "types" (suffix segments after the first '.'); supports partial tokens like upload/auto_update; env var AW_RECIPE_PROCESSING_ORDER expects comma-separated values
19
+ --debug Enable debug logging when running script
20
+ --disable-recipe-trust-check If this option is used, recipe trust verification will not be run prior to a recipe run.
21
+ --github-token GITHUB_TOKEN A token used to publish a PR to your GitHub repo if overrides require their trust to be updated
22
+ --branch-name BRANCH_NAME Branch name to be used where recipe overrides have failed their trust verification and need to be updated.
23
+ By default, this will be in the format of "fix/update_trust_information/YYYY-MM-DDTHH-MM-SS"
24
+ --create-pr If enabled, autopkg_wrapper will open a PR for updated trust information
25
+ --create-issues Create a GitHub issue for recipes that fail during processing
26
+ --disable-git-commands If this option is used, git commands won't be run
27
+ --post-processors [POST_PROCESSORS ...]
28
+ One or more autopkg post processors to run after each recipe execution
29
+ --autopkg-prefs AW_AUTOPKG_PREFS_FILE
30
+ Path to the autopkg preferences you'd like to use
31
+ --overrides-repo-path AUTOPKG_OVERRIDES_REPO_PATH
32
+ The path on disk to the git repository containing the autopkg overrides directory. If none is provided, we will try to determine it for you.
33
+ --concurrency CONCURRENCY Number of recipes to run in parallel (default: 1)
34
+ --process-reports Process autopkg report directories or zip and emit markdown summaries (runs after recipes complete)
35
+ --reports-zip REPORTS_ZIP Path to an autopkg_report-*.zip to extract and process
36
+ --reports-extract-dir REPORTS_EXTRACT_DIR
37
+ Directory to extract the zip into (default: autopkg_reports_summary/reports)
38
+ --reports-dir REPORTS_DIR Directory of reports to process (if no zip provided). Defaults to /private/tmp/autopkg when processing after a run
39
+ --reports-out-dir REPORTS_OUT_DIR
40
+ Directory to write markdown outputs (default: autopkg_reports_summary/summary)
41
+ --reports-run-date REPORTS_RUN_DATE
42
+ Run date string to include in the summary
43
+ --reports-strict Exit non-zero if any errors are detected in processed reports
44
+ ```
45
+
46
+ ## Examples
47
+
48
+ Run recipes (serial):
49
+
50
+ ```bash
51
+ autopkg_wrapper --recipes Foo.download Bar.download
52
+ ```
53
+
54
+ Run 3 recipes concurrently and process reports afterward:
55
+
56
+ ```bash
57
+ autopkg_wrapper \
58
+ --recipe-file /path/to/recipe_list.txt \
59
+ --concurrency 3 \
60
+ --disable-git-commands \
61
+ --process-reports \
62
+ --reports-out-dir /tmp/autopkg_reports_summary \
63
+ --reports-strict
64
+ ```
65
+
66
+ Process a reports zip explicitly (no recipe run):
67
+
68
+ ```bash
69
+ autopkg_wrapper \
70
+ --process-reports \
71
+ --reports-zip /path/to/autopkg_report-2026-02-02.zip \
72
+ --reports-extract-dir /tmp/autopkg_reports \
73
+ --reports-out-dir /tmp/autopkg_reports_summary
74
+ ```
75
+
76
+ Notes:
77
+
78
+ - During recipe runs, per‑recipe plist reports are written to `/private/tmp/autopkg`.
79
+ - When `--process-reports` is supplied without `--reports-zip` or `--reports-dir`, the tool processes `/private/tmp/autopkg`.
80
+ - If `AUTOPKG_JSS_URL`, `AUTOPKG_CLIENT_ID`, and `AUTOPKG_CLIENT_SECRET` are set, uploaded package rows are enriched with Jamf package links.
81
+ - No extra CLI flag is required; enrichment runs automatically when all three env vars are present.
82
+
83
+ An example folder structure and GitHub Actions Workflow is available within the [`actions-demo`](actions-demo)
84
+
85
+ ## Credits
86
+
87
+ - [`autopkg_tools` from Facebook](https://github.com/facebook/IT-CPE/tree/main/legacy/autopkg_tools)
88
+ - [`autopkg_tools` from Facebook, modified by Gusto](https://github.com/Gusto/it-cpe-opensource/tree/main/autopkg)
@@ -0,0 +1,3 @@
1
+ autopkg-wrapper==2025.11.1 \
2
+ --hash=sha256:1f6e650fb386f7bc010a02be7fc019d90b9a6ebab9b37638492fde1deb962d87 \
3
+ --hash=sha256:7304c6aa7a944ad86526583ec89115d704b49fe450d0bf4f2b4f7a2ab2aa57d6
@@ -4,6 +4,7 @@ import logging
4
4
  import plistlib
5
5
  import subprocess
6
6
  import sys
7
+ from concurrent.futures import ThreadPoolExecutor, as_completed
7
8
  from datetime import datetime
8
9
  from itertools import chain
9
10
  from pathlib import Path
@@ -12,6 +13,8 @@ import autopkg_wrapper.utils.git_functions as git
12
13
  from autopkg_wrapper.notifier import slack
13
14
  from autopkg_wrapper.utils.args import setup_args
14
15
  from autopkg_wrapper.utils.logging import setup_logger
16
+ from autopkg_wrapper.utils.recipe_ordering import order_recipe_list
17
+ from autopkg_wrapper.utils.report_processor import process_reports
15
18
 
16
19
 
17
20
  class Recipe(object):
@@ -34,43 +37,39 @@ class Recipe(object):
34
37
  return name
35
38
 
36
39
  def verify_trust_info(self, args):
37
- verbose_output = ["-vvvv"] if args.debug else None
40
+ verbose_output = ["-vvvv"] if args.debug else []
38
41
  prefs_file = (
39
- ["--prefs", args.autopkg_prefs.as_posix()] if args.autopkg_prefs else None
42
+ ["--prefs", args.autopkg_prefs.as_posix()] if args.autopkg_prefs else []
40
43
  )
41
- cmd = ["/usr/local/bin/autopkg", "verify-trust-info", self.filename]
42
- cmd = cmd + verbose_output if verbose_output else cmd
43
- cmd = cmd + prefs_file if prefs_file else cmd
44
- cmd = " ".join(cmd)
45
- logging.debug(f"cmd: {str(cmd)}")
46
-
47
- p = subprocess.Popen(
48
- cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True
44
+ autopkg_bin = getattr(args, "autopkg_bin", "/usr/local/bin/autopkg")
45
+ cmd = (
46
+ [autopkg_bin, "verify-trust-info", self.filename]
47
+ + verbose_output
48
+ + prefs_file
49
49
  )
50
- (output, err) = p.communicate()
51
- p_status = p.wait()
52
- if p_status == 0:
50
+ logging.debug(f"cmd: {cmd}")
51
+
52
+ result = subprocess.run(cmd, capture_output=True, text=True)
53
+ if result.returncode == 0:
53
54
  self.verified = True
54
55
  else:
55
- err = err.decode()
56
- self.results["message"] = err
56
+ self.results["message"] = (result.stderr or "").strip()
57
57
  self.verified = False
58
58
  return self.verified
59
59
 
60
60
  def update_trust_info(self, args):
61
61
  prefs_file = (
62
- ["--prefs", args.autopkg_prefs.as_posix()] if args.autopkg_prefs else None
62
+ ["--prefs", args.autopkg_prefs.as_posix()] if args.autopkg_prefs else []
63
63
  )
64
- cmd = ["/usr/local/bin/autopkg", "update-trust-info", self.filename]
65
- cmd = cmd + prefs_file if prefs_file else cmd
66
- cmd = " ".join(cmd)
67
- logging.debug(f"cmd: {str(cmd)}")
64
+ autopkg_bin = getattr(args, "autopkg_bin", "/usr/local/bin/autopkg")
65
+ cmd = [autopkg_bin, "update-trust-info", self.filename] + prefs_file
66
+ logging.debug(f"cmd: {cmd}")
68
67
 
69
68
  # Fail loudly if this exits 0
70
69
  try:
71
- subprocess.check_call(cmd, shell=True)
70
+ subprocess.check_call(cmd)
72
71
  except subprocess.CalledProcessError as e:
73
- logging.error(e.stderr)
72
+ logging.error(str(e))
74
73
  raise e
75
74
 
76
75
  def _parse_report(self, report):
@@ -94,7 +93,7 @@ class Recipe(object):
94
93
  self.results["failed"] = True
95
94
  self.results["imported"] = ""
96
95
  else:
97
- report_dir = Path("/tmp/autopkg")
96
+ report_dir = Path("/private/tmp/autopkg")
98
97
  report_time = datetime.now().strftime("%Y-%m-%dT%H-%M-%S")
99
98
  report_name = Path(f"{self.name}-{report_time}.plist")
100
99
 
@@ -106,9 +105,9 @@ class Recipe(object):
106
105
  prefs_file = (
107
106
  ["--prefs", args.autopkg_prefs.as_posix()]
108
107
  if args.autopkg_prefs
109
- else None
108
+ else []
110
109
  )
111
- verbose_output = ["-vvvv"] if args.debug else None
110
+ verbose_output = ["-vvvv"] if args.debug else []
112
111
  post_processor_cmd = (
113
112
  list(
114
113
  chain.from_iterable(
@@ -119,25 +118,25 @@ class Recipe(object):
119
118
  )
120
119
  )
121
120
  if self.post_processors
122
- else None
121
+ else []
123
122
  )
123
+ autopkg_bin = getattr(args, "autopkg_bin", "/usr/local/bin/autopkg")
124
124
  cmd = [
125
- "/usr/local/bin/autopkg",
125
+ autopkg_bin,
126
126
  "run",
127
127
  self.filename,
128
128
  "--report-plist",
129
129
  str(report),
130
130
  ]
131
- cmd = cmd + post_processor_cmd if post_processor_cmd else cmd
132
- cmd = cmd + verbose_output if verbose_output else cmd
133
- cmd = cmd + prefs_file if prefs_file else cmd
134
- cmd = " ".join(cmd)
131
+ cmd = cmd + post_processor_cmd + verbose_output + prefs_file
135
132
 
136
- logging.debug(f"cmd: {str(cmd)}")
133
+ logging.debug(f"cmd: {cmd}")
137
134
 
138
- subprocess.check_call(cmd, shell=True)
135
+ result = subprocess.run(cmd, capture_output=True, text=True)
136
+ if result.returncode != 0:
137
+ self.error = True
139
138
 
140
- except subprocess.CalledProcessError:
139
+ except Exception:
141
140
  self.error = True
142
141
 
143
142
  self._has_run = True
@@ -244,7 +243,12 @@ def update_recipe_repo(recipe, git_info, disable_recipe_trust_check, args):
244
243
 
245
244
 
246
245
  def parse_recipe_list(recipes, recipe_file, post_processors, args):
247
- """Parsing list of recipes into a common format"""
246
+ """Parse recipe inputs into a common list of recipe names.
247
+
248
+ The arguments assume that `recipes` and `recipe_file` are mutually exclusive.
249
+ If `args.recipe_processing_order` is provided, the list is re-ordered before
250
+ creating `Recipe` objects.
251
+ """
248
252
  recipe_list = None
249
253
 
250
254
  logging.info(f"Recipes: {recipes}") if recipes else None
@@ -254,6 +258,12 @@ def parse_recipe_list(recipes, recipe_file, post_processors, args):
254
258
  if recipe_file.suffix == ".json":
255
259
  with open(recipe_file, "r") as f:
256
260
  recipe_list = json.load(f)
261
+ elif recipe_file.suffix in {".yaml", ".yml"}:
262
+ from ruamel.yaml import YAML
263
+
264
+ yaml = YAML(typ="safe")
265
+ with open(recipe_file, "r", encoding="utf-8") as f:
266
+ recipe_list = yaml.load(f)
257
267
  elif recipe_file.suffix == ".txt":
258
268
  with open(recipe_file, "r") as f:
259
269
  recipe_list = f.read().splitlines()
@@ -277,11 +287,16 @@ def parse_recipe_list(recipes, recipe_file, post_processors, args):
277
287
  """Please provide recipes to run via the following methods:
278
288
  --recipes recipe_one.download recipe_two.download
279
289
  --recipe-file path/to/recipe_list.json
280
- Comma separated list in the AUTOPKG_RECIPES env variable"""
290
+ Comma separated list in the AW_RECIPES env variable"""
281
291
  )
282
292
  sys.exit(1)
283
293
 
284
- logging.info(f"Processing the following recipes: {recipe_list}")
294
+ if args.recipe_processing_order:
295
+ recipe_list = order_recipe_list(
296
+ recipe_list=recipe_list, order=args.recipe_processing_order
297
+ )
298
+
299
+ logging.info(f"Processing {len(recipe_list)} recipes.")
285
300
  recipe_map = [Recipe(name, post_processors=post_processors) for name in recipe_list]
286
301
 
287
302
  return recipe_map
@@ -358,31 +373,50 @@ def main():
358
373
 
359
374
  failed_recipes = []
360
375
 
361
- for recipe in recipe_list:
362
- logging.info(f"Processing Recipe: {recipe.name}")
376
+ # Run recipes concurrently using a thread pool to parallelize subprocess calls
377
+ max_workers = max(1, int(getattr(args, "concurrency", 1)))
378
+ logging.info(f"Running recipes with concurrency={max_workers}")
379
+
380
+ def run_one(r: Recipe):
381
+ logging.info(f"Processing Recipe: {r.name}")
363
382
  process_recipe(
364
- recipe=recipe,
383
+ recipe=r,
365
384
  disable_recipe_trust_check=args.disable_recipe_trust_check,
366
385
  args=args,
367
386
  )
387
+ # Git updates and notifications are applied serially after all recipes finish
388
+ return r
389
+
390
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
391
+ futures = [executor.submit(run_one, r) for r in recipe_list]
392
+ for fut in as_completed(futures):
393
+ r = fut.result()
394
+ if r.error or r.results.get("failed"):
395
+ failed_recipes.append(r)
396
+
397
+ # Apply git updates serially to avoid branch/commit conflicts when concurrency > 1
398
+ for r in recipe_list:
368
399
  update_recipe_repo(
369
400
  git_info=override_repo_info,
370
- recipe=recipe,
401
+ recipe=r,
371
402
  disable_recipe_trust_check=args.disable_recipe_trust_check,
372
403
  args=args,
373
404
  )
374
- slack.send_notification(
375
- recipe=recipe, token=args.slack_token
376
- ) if args.slack_token else None
377
405
 
378
- if recipe.error or recipe.results.get("failed"):
379
- failed_recipes.append(recipe)
380
-
381
- recipe.pr_url = (
382
- git.create_pull_request(git_info=override_repo_info, recipe=recipe)
383
- if args.create_pr
384
- else None
385
- )
406
+ # Send notifications serially to simplify rate limiting and ordering
407
+ if args.slack_token:
408
+ for r in recipe_list:
409
+ slack.send_notification(recipe=r, token=args.slack_token)
410
+
411
+ # Optionally open a PR for updated trust information
412
+ if args.create_pr and recipe_list:
413
+ # Choose a representative recipe for the PR title/body
414
+ rep_recipe = next(
415
+ (r for r in recipe_list if r.updated is True or r.verified is False),
416
+ recipe_list[0],
417
+ )
418
+ pr_url = git.create_pull_request(git_info=override_repo_info, recipe=rep_recipe)
419
+ logging.info(f"Created Pull Request for trust info updates: {pr_url}")
386
420
 
387
421
  # Create GitHub issue for failed recipes
388
422
  if args.create_issues and failed_recipes and args.github_token:
@@ -390,3 +424,20 @@ def main():
390
424
  git_info=override_repo_info, failed_recipes=failed_recipes
391
425
  )
392
426
  logging.info(f"Created GitHub issue for failed recipes: {issue_url}")
427
+
428
+ # Optionally process reports after running recipes
429
+ if getattr(args, "process_reports", False):
430
+ rc = process_reports(
431
+ zip_file=getattr(args, "reports_zip", None),
432
+ extract_dir=getattr(
433
+ args, "reports_extract_dir", "autopkg_reports_summary/reports"
434
+ ),
435
+ reports_dir=(getattr(args, "reports_dir", None) or "/private/tmp/autopkg"),
436
+ environment="",
437
+ run_date=getattr(args, "reports_run_date", ""),
438
+ out_dir=getattr(args, "reports_out_dir", "autopkg_reports_summary/summary"),
439
+ debug=bool(getattr(args, "debug", False)),
440
+ strict=bool(getattr(args, "reports_strict", False)),
441
+ )
442
+ if rc:
443
+ sys.exit(rc)
@@ -5,7 +5,7 @@ import requests
5
5
 
6
6
 
7
7
  def send_notification(recipe, token):
8
- logging.debug("Skipping Slack notification as DEBUG is enabled!")
8
+ logging.debug("Preparing Slack notification")
9
9
 
10
10
  if token is None:
11
11
  logging.error("Skipping Slack Notification as no SLACK_WEBHOOK_TOKEN defined!")