|
1 | 1 | #!/usr/bin/env python3 |
2 | 2 |
|
3 | 3 | import argparse |
4 | | -from collections import defaultdict |
| 4 | +from collections import Counter, defaultdict |
5 | 5 | import graphlib |
6 | 6 | import json |
7 | 7 | import os |
|
10 | 10 | from typing import ( |
11 | 11 | Any, |
12 | 12 | Dict, |
13 | | - Generator, |
14 | 13 | List, |
15 | 14 | Literal, |
16 | 15 | NotRequired, |
17 | 16 | Optional, |
18 | 17 | Set, |
| 18 | + Tuple, |
19 | 19 | TypedDict, |
20 | 20 | get_args, |
21 | 21 | ) |
22 | 22 |
|
| 23 | +from github_action_utils import debug, error, set_output, warning |
| 24 | +from result import Err, Ok, Result |
| 25 | + |
23 | 26 | System = Literal["x86_64-linux", "aarch64-linux", "aarch64-darwin"] |
24 | 27 | RunnerType = Literal["ephemeral", "self-hosted"] |
25 | 28 |
|
@@ -103,53 +106,75 @@ def build_nix_eval_command(max_workers: int, flake_outputs: List[str]) -> List[s |
103 | 106 |
|
104 | 107 |
|
105 | 108 | def parse_nix_eval_line( |
106 | | - line: str, drv_paths: Set[str], errors: List[str] |
107 | | -) -> Optional[NixEvalJobsOutput]: |
108 | | - """Parse a single line of nix-eval-jobs output""" |
| 109 | + line: str, drv_paths: Set[str] |
| 110 | +) -> Result[Optional[NixEvalJobsOutput], str]: |
| 111 | + """Parse a single line of nix-eval-jobs output. |
| 112 | +
|
| 113 | + Returns: |
| 114 | + Ok(package_data) if successful (None for empty/duplicate lines) |
| 115 | + Err(error_message) if a nix evaluation error occurred |
| 116 | + """ |
109 | 117 | if not line.strip(): |
110 | | - return None |
| 118 | + return Ok(None) |
111 | 119 |
|
112 | 120 | try: |
113 | 121 | data: NixEvalJobsOutput = json.loads(line) |
114 | 122 | if "error" in data: |
115 | 123 | error_msg = ( |
116 | 124 | f"Error in nix-eval-jobs output for {data['attr']}: {data['error']}" |
117 | 125 | ) |
118 | | - errors.append(error_msg) |
119 | | - return None |
| 126 | + error(error_msg, title="Nix Evaluation Error") |
| 127 | + return Err(error_msg) |
120 | 128 | if data["drvPath"] in drv_paths: |
121 | | - return None |
| 129 | + return Ok(None) |
122 | 130 | drv_paths.add(data["drvPath"]) |
123 | | - return data |
124 | | - except json.JSONDecodeError: |
125 | | - error_msg = f"Skipping invalid JSON line: {line}" |
126 | | - print(error_msg, file=sys.stderr) |
127 | | - errors.append(error_msg) |
128 | | - return None |
| 131 | + return Ok(data) |
| 132 | + except json.JSONDecodeError as e: |
| 133 | + warning(f"Skipping invalid JSON line: {line}", title="JSON Parse Warning") |
| 134 | + return Ok(None) |
129 | 135 |
|
130 | 136 |
|
131 | 137 | def run_nix_eval_jobs( |
132 | | - cmd: List[str], errors: List[str] |
133 | | -) -> Generator[NixEvalJobsOutput, None, None]: |
134 | | - """Run nix-eval-jobs and yield parsed package data.""" |
135 | | - print(f"Running command: {' '.join(cmd)}", file=sys.stderr) |
136 | | - |
137 | | - with subprocess.Popen( |
138 | | - cmd, stdout=subprocess.PIPE, stderr=None, text=True |
139 | | - ) as process: |
140 | | - drv_paths: Set[str] = set() |
141 | | - assert process.stdout is not None # for mypy |
142 | | - for line in process.stdout: |
143 | | - package = parse_nix_eval_line(line, drv_paths, errors) |
144 | | - if package: |
145 | | - yield package |
146 | | - |
147 | | - process.wait() |
148 | | - if process.returncode != 0: |
149 | | - error_msg = "Error: nix-eval-jobs process failed with non-zero exit code" |
150 | | - print(error_msg, file=sys.stderr) |
151 | | - errors.append(error_msg) |
152 | | - # Don't exit here - let main() handle it after reporting all errors |
| 138 | + cmd: List[str], |
| 139 | +) -> Tuple[List[NixEvalJobsOutput], List[str], bool]: |
| 140 | + """Run nix-eval-jobs and return parsed package data, warnings, and error status. |
| 141 | +
|
| 142 | + Returns: |
| 143 | + Tuple of (packages, warnings_list, had_errors) |
| 144 | + """ |
| 145 | + debug(f"Running command: {' '.join(cmd)}") |
| 146 | + |
| 147 | + process = subprocess.Popen( |
| 148 | + cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True |
| 149 | + ) |
| 150 | + stdout_data, stderr_data = process.communicate() |
| 151 | + |
| 152 | + # Parse stdout for packages |
| 153 | + packages: List[NixEvalJobsOutput] = [] |
| 154 | + drv_paths: Set[str] = set() |
| 155 | + had_errors = False |
| 156 | + for line in stdout_data.splitlines(): |
| 157 | + result = parse_nix_eval_line(line, drv_paths) |
| 158 | + if result.is_err(): |
| 159 | + had_errors = True |
| 160 | + elif result.ok_value is not None: |
| 161 | + packages.append(result.ok_value) |
| 162 | + |
| 163 | + # Parse stderr for warnings (lines starting with "warning:") |
| 164 | + warnings_list: List[str] = [] |
| 165 | + for line in stderr_data.splitlines(): |
| 166 | + line = line.strip() |
| 167 | + if line.startswith("warning:") or line.startswith("evaluation warning:"): |
| 168 | + # Remove "warning:" prefix for cleaner messages |
| 169 | + warnings_list.append(line[8:].strip()) |
| 170 | + |
| 171 | + if process.returncode != 0: |
| 172 | + error( |
| 173 | + "nix-eval-jobs process failed with non-zero exit code", |
| 174 | + title="Process Failure", |
| 175 | + ) |
| 176 | + |
| 177 | + return packages, warnings_list, had_errors |
153 | 178 |
|
154 | 179 |
|
155 | 180 | def is_extension_pkg(pkg: NixEvalJobsOutput) -> bool: |
@@ -235,9 +260,9 @@ def main() -> None: |
235 | 260 |
|
236 | 261 | cmd = build_nix_eval_command(max_workers, args.flake_outputs) |
237 | 262 |
|
238 | | - # Collect all evaluation errors |
239 | | - errors: List[str] = [] |
240 | | - gh_action_packages = sort_pkgs_by_closures(list(run_nix_eval_jobs(cmd, errors))) |
| 263 | + # Run evaluation and collect packages and warnings |
| 264 | + packages, warnings_list, had_errors = run_nix_eval_jobs(cmd) |
| 265 | + gh_action_packages = sort_pkgs_by_closures(packages) |
241 | 266 |
|
242 | 267 | def clean_package_for_output(pkg: NixEvalJobsOutput) -> GitHubActionPackage: |
243 | 268 | """Convert nix-eval-jobs output to GitHub Actions matrix package""" |
@@ -281,22 +306,24 @@ def clean_package_for_output(pkg: NixEvalJobsOutput) -> GitHubActionPackage: |
281 | 306 | } |
282 | 307 | ] |
283 | 308 | } |
284 | | - print( |
285 | | - f"debug: Generated GitHub Actions matrix: {json.dumps(gh_output, indent=2)}", |
286 | | - file=sys.stderr, |
287 | | - ) |
288 | | - print(json.dumps(gh_output)) |
289 | | - |
290 | | - # Check if any errors occurred during evaluation |
291 | | - if errors: |
292 | | - print("\n=== Evaluation Errors ===", file=sys.stderr) |
293 | | - for i, error in enumerate(errors, 1): |
294 | | - print(f"\nError {i}:", file=sys.stderr) |
295 | | - print(error, file=sys.stderr) |
296 | | - print( |
297 | | - f"\n=== Total: {len(errors)} error(s) occurred during evaluation ===", |
298 | | - file=sys.stderr, |
299 | | - ) |
| 309 | + |
| 310 | + if warnings_list: |
| 311 | + warning_counts = Counter(warnings_list) |
| 312 | + for warn_msg, count in warning_counts.items(): |
| 313 | + if count > 1: |
| 314 | + warning( |
| 315 | + f"{warn_msg} (occurred {count} times)", |
| 316 | + title="Nix Evaluation Warning", |
| 317 | + ) |
| 318 | + else: |
| 319 | + warning(warn_msg, title="Nix Evaluation Warning") |
| 320 | + |
| 321 | + # Output matrix to GitHub Actions |
| 322 | + debug(f"Generated GitHub Actions matrix: {json.dumps(gh_output, indent=2)}") |
| 323 | + set_output("matrix", json.dumps(gh_output)) |
| 324 | + |
| 325 | + # Exit with error code if any evaluation errors occurred |
| 326 | + if had_errors: |
300 | 327 | sys.exit(1) |
301 | 328 |
|
302 | 329 |
|
|
0 commit comments