repofail 0.2.4__tar.gz → 0.2.7__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.
- {repofail-0.2.4 → repofail-0.2.7}/.gitignore +3 -0
- {repofail-0.2.4/homebrew-tap → repofail-0.2.7}/Formula/repofail.rb +2 -2
- repofail-0.2.7/LICENSE +21 -0
- repofail-0.2.4/README.md → repofail-0.2.7/PKG-INFO +55 -9
- repofail-0.2.4/PKG-INFO → repofail-0.2.7/README.md +35 -27
- repofail-0.2.7/docs/logo.png +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/pyproject.toml +2 -1
- {repofail-0.2.4 → repofail-0.2.7}/repofail/cli.py +79 -2
- {repofail-0.2.4 → repofail-0.2.7}/repofail/fleet.py +99 -2
- repofail-0.2.7/repofail/lock.py +78 -0
- {repofail-0.2.4 → repofail-0.2.7}/repofail/scanner/parsers.py +15 -1
- repofail-0.2.4/.github/actions/repofail/README.md +0 -45
- repofail-0.2.4/.github/actions/repofail/action.yml +0 -73
- repofail-0.2.4/Formula/repofail.rb +0 -66
- repofail-0.2.4/docs/QUICKSTART.md +0 -42
- repofail-0.2.4/docs/logo.png +0 -0
- repofail-0.2.4/install.sh +0 -21
- {repofail-0.2.4 → repofail-0.2.7}/.github/workflows/publish.yml +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/.github/workflows/repofail.yml +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/CONTRIBUTING.md +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/RULES.md +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/contract.json +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/docs/assets/demo-autogpt.png +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/docs/assets/repofail-logo.png +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/docs/screenshots/high_failure.png +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/docs/screenshots/nodefail.gif +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/repofail/__init__.py +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/repofail/contract.py +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/repofail/engine.py +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/repofail/format.py +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/repofail/models.py +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/repofail/risk.py +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/repofail/rules/__init__.py +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/repofail/rules/abi_wheel_mismatch.py +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/repofail/rules/apple_silicon.py +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/repofail/rules/base.py +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/repofail/rules/docker_only.py +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/repofail/rules/gpu_memory.py +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/repofail/rules/info_signals.py +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/repofail/rules/lock_file_missing.py +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/repofail/rules/ml_niche.py +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/repofail/rules/native_toolchain.py +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/repofail/rules/node_engine.py +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/repofail/rules/node_eol.py +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/repofail/rules/node_windows.py +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/repofail/rules/port_collision.py +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/repofail/rules/python_eol.py +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/repofail/rules/python_version.py +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/repofail/rules/registry.py +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/repofail/rules/spec_drift.py +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/repofail/rules/system_libs.py +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/repofail/rules/torch_cuda.py +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/repofail/rules/yaml_loader.py +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/repofail/scanner/__init__.py +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/repofail/scanner/ast_scan.py +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/repofail/scanner/host.py +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/repofail/scanner/repo.py +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/repofail/telemetry.py +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/tests/__init__.py +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/tests/test_cli_stage2.py +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/tests/test_contract.py +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/tests/test_fleet.py +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/tests/test_integration.py +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/tests/test_ml_rules.py +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/tests/test_rules.py +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/tests/test_scanner.py +0 -0
- {repofail-0.2.4 → repofail-0.2.7}/tests/test_telemetry.py +0 -0
|
@@ -12,8 +12,8 @@ class Repofail < Formula
|
|
|
12
12
|
|
|
13
13
|
desc "Deterministic runtime compatibility analyzer"
|
|
14
14
|
homepage "https://github.com/jayvenn21/repofail"
|
|
15
|
-
url "https://github.com/jayvenn21/repofail/archive/refs/tags/v0.2.
|
|
16
|
-
sha256 "
|
|
15
|
+
url "https://github.com/jayvenn21/repofail/archive/refs/tags/v0.2.5.tar.gz"
|
|
16
|
+
sha256 "ac1791fcfd24fd9a70df44423f02ac6f70df5df53ee2f2fca7d518a6b639fa86"
|
|
17
17
|
license "MIT"
|
|
18
18
|
|
|
19
19
|
# App supports Python 3.10+. We use 3.12 for current Homebrew; change to
|
repofail-0.2.7/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jayanth Vennamreddy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,20 +1,39 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: repofail
|
|
3
|
+
Version: 0.2.7
|
|
4
|
+
Summary: Predict why a repository will fail on your machine before you run it.
|
|
5
|
+
Author: Jayanth
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Requires-Dist: pyyaml>=6.0
|
|
15
|
+
Requires-Dist: tomli>=2.0; python_version < '3.11'
|
|
16
|
+
Requires-Dist: typer>=0.9.0
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
1
21
|
<p align="center">
|
|
2
|
-
<img src="
|
|
22
|
+
<img src="docs/logo.png" width="65%" alt="repofail logo">
|
|
3
23
|
</p>
|
|
4
24
|
|
|
5
|
-
<h1 align="center">repofail</h1>
|
|
6
|
-
|
|
7
25
|
<p align="center">
|
|
8
26
|
Deterministic runtime compatibility analyzer
|
|
9
27
|
</p>
|
|
10
28
|
|
|
11
29
|
<p align="center">
|
|
12
|
-
<a href="https://pypi.org/project/repofail/"><img src="https://img.shields.io/pypi/v/repofail?
|
|
13
|
-
<img src="https://img.shields.io/pypi/pyversions/repofail" alt="Python">
|
|
14
|
-
<img src="https://img.shields.io/pypi/dm/repofail" alt="Downloads">
|
|
15
|
-
<img src="https://img.shields.io/
|
|
16
|
-
<img src="https://img.shields.io/badge/runtime-validated-
|
|
17
|
-
<img src="https://img.shields.io/badge/rules-20
|
|
30
|
+
<a href="https://pypi.org/project/repofail/"><img src="https://img.shields.io/pypi/v/repofail?cacheSeconds=60" alt="PyPI"></a>
|
|
31
|
+
<a href="https://pypi.org/project/repofail/"><img src="https://img.shields.io/pypi/pyversions/repofail?cacheSeconds=60" alt="Python"></a>
|
|
32
|
+
<a href="https://pypi.org/project/repofail/"><img src="https://img.shields.io/pypi/dm/repofail?cacheSeconds=60" alt="Downloads"></a>
|
|
33
|
+
<a href="https://github.com/jayvenn21/repofail/actions"><img src="https://img.shields.io/github/actions/workflow/status/jayvenn21/repofail/repofail.yml?branch=main" alt="CI"></a>
|
|
34
|
+
<img src="https://img.shields.io/badge/runtime-validated-brightgreen" alt="Runtime">
|
|
35
|
+
<img src="https://img.shields.io/badge/rules-20%2B-blue" alt="Rules">
|
|
36
|
+
<a href="https://github.com/jayvenn21/repofail/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-lightgrey" alt="License"></a>
|
|
18
37
|
</p>
|
|
19
38
|
|
|
20
39
|
<p align="center">
|
|
@@ -215,6 +234,27 @@ The action installs repofail, runs a compatibility check, comments the Markdown
|
|
|
215
234
|
|
|
216
235
|
Exits 1 if HIGH rules fire. Use `--fail-on MEDIUM` to be stricter.
|
|
217
236
|
|
|
237
|
+
**Option C — Lock + verify (enforcement)**
|
|
238
|
+
|
|
239
|
+
Pin the runtime once, then fail CI on drift:
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
repofail lock # generates repofail.lock.json (python, node, arch, os, cuda, docker_base)
|
|
243
|
+
repofail verify # exit 1 if host doesn't match lock
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Commit `repofail.lock.json`. In CI, run `repofail verify` so builds only pass on the locked environment.
|
|
247
|
+
|
|
248
|
+
**Fleet compliance**
|
|
249
|
+
|
|
250
|
+
Scan many repos and get violations, most common drift, and risk clusters:
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
repofail fleet scan ~/org --policy org.policy.yaml
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Policy YAML (optional): `fail_on: HIGH`, `max_repos: 500`, `max_depth: 4`. With `fail_on: HIGH`, exit code is 1 if any repo has a HIGH finding.
|
|
257
|
+
|
|
218
258
|
## Contracts
|
|
219
259
|
|
|
220
260
|
```bash
|
|
@@ -331,3 +371,9 @@ pytest tests/ -v
|
|
|
331
371
|
```
|
|
332
372
|
|
|
333
373
|
**Quick checks:** `bash -n install.sh` (syntax). The GitHub Action runs on every PR in this repo (see [.github/workflows/repofail.yml](.github/workflows/repofail.yml)); to test the reusable action, use it in another repo’s workflow on a PR.
|
|
374
|
+
|
|
375
|
+
---
|
|
376
|
+
|
|
377
|
+
## License
|
|
378
|
+
|
|
379
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -1,38 +1,19 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: repofail
|
|
3
|
-
Version: 0.2.4
|
|
4
|
-
Summary: Predict why a repository will fail on your machine before you run it.
|
|
5
|
-
Author: Jayanth
|
|
6
|
-
Classifier: Programming Language :: Python :: 3
|
|
7
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
8
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
9
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
-
Requires-Python: >=3.10
|
|
12
|
-
Requires-Dist: pyyaml>=6.0
|
|
13
|
-
Requires-Dist: tomli>=2.0; python_version < '3.11'
|
|
14
|
-
Requires-Dist: typer>=0.9.0
|
|
15
|
-
Provides-Extra: dev
|
|
16
|
-
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
17
|
-
Description-Content-Type: text/markdown
|
|
18
|
-
|
|
19
1
|
<p align="center">
|
|
20
|
-
<img src="
|
|
2
|
+
<img src="docs/logo.png" width="65%" alt="repofail logo">
|
|
21
3
|
</p>
|
|
22
4
|
|
|
23
|
-
<h1 align="center">repofail</h1>
|
|
24
|
-
|
|
25
5
|
<p align="center">
|
|
26
6
|
Deterministic runtime compatibility analyzer
|
|
27
7
|
</p>
|
|
28
8
|
|
|
29
9
|
<p align="center">
|
|
30
|
-
<a href="https://pypi.org/project/repofail/"><img src="https://img.shields.io/pypi/v/repofail?
|
|
31
|
-
<img src="https://img.shields.io/pypi/pyversions/repofail" alt="Python">
|
|
32
|
-
<img src="https://img.shields.io/pypi/dm/repofail" alt="Downloads">
|
|
33
|
-
<img src="https://img.shields.io/
|
|
34
|
-
<img src="https://img.shields.io/badge/runtime-validated-
|
|
35
|
-
<img src="https://img.shields.io/badge/rules-20
|
|
10
|
+
<a href="https://pypi.org/project/repofail/"><img src="https://img.shields.io/pypi/v/repofail?cacheSeconds=60" alt="PyPI"></a>
|
|
11
|
+
<a href="https://pypi.org/project/repofail/"><img src="https://img.shields.io/pypi/pyversions/repofail?cacheSeconds=60" alt="Python"></a>
|
|
12
|
+
<a href="https://pypi.org/project/repofail/"><img src="https://img.shields.io/pypi/dm/repofail?cacheSeconds=60" alt="Downloads"></a>
|
|
13
|
+
<a href="https://github.com/jayvenn21/repofail/actions"><img src="https://img.shields.io/github/actions/workflow/status/jayvenn21/repofail/repofail.yml?branch=main" alt="CI"></a>
|
|
14
|
+
<img src="https://img.shields.io/badge/runtime-validated-brightgreen" alt="Runtime">
|
|
15
|
+
<img src="https://img.shields.io/badge/rules-20%2B-blue" alt="Rules">
|
|
16
|
+
<a href="https://github.com/jayvenn21/repofail/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-lightgrey" alt="License"></a>
|
|
36
17
|
</p>
|
|
37
18
|
|
|
38
19
|
<p align="center">
|
|
@@ -233,6 +214,27 @@ The action installs repofail, runs a compatibility check, comments the Markdown
|
|
|
233
214
|
|
|
234
215
|
Exits 1 if HIGH rules fire. Use `--fail-on MEDIUM` to be stricter.
|
|
235
216
|
|
|
217
|
+
**Option C — Lock + verify (enforcement)**
|
|
218
|
+
|
|
219
|
+
Pin the runtime once, then fail CI on drift:
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
repofail lock # generates repofail.lock.json (python, node, arch, os, cuda, docker_base)
|
|
223
|
+
repofail verify # exit 1 if host doesn't match lock
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Commit `repofail.lock.json`. In CI, run `repofail verify` so builds only pass on the locked environment.
|
|
227
|
+
|
|
228
|
+
**Fleet compliance**
|
|
229
|
+
|
|
230
|
+
Scan many repos and get violations, most common drift, and risk clusters:
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
repofail fleet scan ~/org --policy org.policy.yaml
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Policy YAML (optional): `fail_on: HIGH`, `max_repos: 500`, `max_depth: 4`. With `fail_on: HIGH`, exit code is 1 if any repo has a HIGH finding.
|
|
237
|
+
|
|
236
238
|
## Contracts
|
|
237
239
|
|
|
238
240
|
```bash
|
|
@@ -349,3 +351,9 @@ pytest tests/ -v
|
|
|
349
351
|
```
|
|
350
352
|
|
|
351
353
|
**Quick checks:** `bash -n install.sh` (syntax). The GitHub Action runs on every PR in this repo (see [.github/workflows/repofail.yml](.github/workflows/repofail.yml)); to test the reusable action, use it in another repo’s workflow on a PR.
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
## License
|
|
358
|
+
|
|
359
|
+
MIT — see [LICENSE](LICENSE).
|
|
Binary file
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "repofail"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.7"
|
|
8
8
|
description = "Predict why a repository will fail on your machine before you run it."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -20,6 +20,7 @@ classifiers = [
|
|
|
20
20
|
"Programming Language :: Python :: 3.11",
|
|
21
21
|
"Programming Language :: Python :: 3.12",
|
|
22
22
|
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Programming Language :: Python :: Implementation :: CPython",
|
|
23
24
|
]
|
|
24
25
|
|
|
25
26
|
[project.scripts]
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""CLI entry point — scan repo, run rules, output clearly."""
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
import sys
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from typing import Optional
|
|
@@ -13,7 +14,7 @@ def _err(msg: str) -> None:
|
|
|
13
14
|
raise click.BadParameter(msg)
|
|
14
15
|
|
|
15
16
|
# Subcommands (short names so "repofail gen" works)
|
|
16
|
-
_SUBCOMMANDS = {"gen", "s", "a", "sim", "check"}
|
|
17
|
+
_SUBCOMMANDS = {"gen", "s", "a", "sim", "check", "lock", "verify", "fleet"}
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
def _preprocess_argv():
|
|
@@ -45,7 +46,9 @@ def _preprocess_argv():
|
|
|
45
46
|
from .scanner import scan_repo, inspect_host
|
|
46
47
|
from .engine import run_rules
|
|
47
48
|
from .contract import generate_contract, validate_contract, EnvironmentContract
|
|
49
|
+
from .lock import generate_lock, verify_lock, LOCK_FILENAME
|
|
48
50
|
from .telemetry import save_report, get_stats
|
|
51
|
+
from .fleet import audit, fleet_scan
|
|
49
52
|
from .rules.base import Severity
|
|
50
53
|
from .rules.registry import RULE_INFO
|
|
51
54
|
from .risk import estimate_success_probability
|
|
@@ -53,6 +56,49 @@ from .format import format_human
|
|
|
53
56
|
|
|
54
57
|
app = typer.Typer(help="Predict why a repository will fail on your machine.")
|
|
55
58
|
|
|
59
|
+
fleet_app = typer.Typer(help="Fleet-wide compliance and scanning.")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@fleet_app.command("scan")
|
|
63
|
+
def fleet_scan_cmd(
|
|
64
|
+
path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True, help="Root dir to scan (e.g. ~/org)"),
|
|
65
|
+
policy: Optional[Path] = typer.Option(None, "--policy", "-P", path_type=Path, help="Policy YAML (fail_on, max_repos, max_depth)"),
|
|
66
|
+
json_out: bool = typer.Option(False, "--json", "-j", help="Output JSON"),
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Scan all repos under path; report violations, most common drift, risk clusters."""
|
|
69
|
+
if not path.exists() or not path.is_dir():
|
|
70
|
+
_err(f"Directory not found: {path}")
|
|
71
|
+
summary = fleet_scan(path, policy_path=policy)
|
|
72
|
+
if json_out:
|
|
73
|
+
typer.echo(json.dumps(summary, indent=2))
|
|
74
|
+
if summary.get("violations", 0) > 0 and summary.get("policy", {}).get("fail_on") == "HIGH":
|
|
75
|
+
high_count = sum(1 for r in summary.get("repos", []) if r.get("has_high"))
|
|
76
|
+
if high_count > 0:
|
|
77
|
+
raise typer.Exit(1)
|
|
78
|
+
return
|
|
79
|
+
total = summary["total_repos_scanned"]
|
|
80
|
+
violations = summary["violations"]
|
|
81
|
+
typer.echo(f"Total repos scanned: {total}")
|
|
82
|
+
typer.echo(f"Violations: {violations}")
|
|
83
|
+
drift = summary.get("most_common_drift") or {}
|
|
84
|
+
if drift:
|
|
85
|
+
typer.echo("Most common drift:")
|
|
86
|
+
for rule_id, count in list(drift.items())[:8]:
|
|
87
|
+
typer.echo(f" {rule_id}: {count}")
|
|
88
|
+
clusters = summary.get("risk_clusters") or []
|
|
89
|
+
if clusters:
|
|
90
|
+
typer.echo("Risk cluster:")
|
|
91
|
+
for c in clusters[:6]:
|
|
92
|
+
typer.echo(f" {c['category']}: {c['count']} finding(s)")
|
|
93
|
+
if summary.get("policy", {}).get("fail_on") == "HIGH":
|
|
94
|
+
high_count = sum(1 for r in summary.get("repos", []) if r.get("has_high"))
|
|
95
|
+
if high_count > 0:
|
|
96
|
+
typer.echo(f"\n{high_count} repo(s) with HIGH severity (policy fail_on=HIGH).", err=True)
|
|
97
|
+
raise typer.Exit(1)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
app.add_typer(fleet_app, name="fleet")
|
|
101
|
+
|
|
56
102
|
|
|
57
103
|
@app.callback(invoke_without_command=True)
|
|
58
104
|
def main(
|
|
@@ -310,7 +356,6 @@ def audit_cmd(
|
|
|
310
356
|
json_out: bool = typer.Option(False, "-j", help="JSON output"),
|
|
311
357
|
) -> None:
|
|
312
358
|
"""Scan all repos in directory — fleet-wide compatibility check."""
|
|
313
|
-
from .fleet import audit
|
|
314
359
|
if not path.exists() or not path.is_dir():
|
|
315
360
|
_err(f"Directory not found: {path}\nUse a path that exists, e.g. repofail a .")
|
|
316
361
|
results = audit(path)
|
|
@@ -361,6 +406,38 @@ def sim_cmd(
|
|
|
361
406
|
raise typer.Exit(1)
|
|
362
407
|
|
|
363
408
|
|
|
409
|
+
@app.command("lock")
|
|
410
|
+
def lock_cmd(
|
|
411
|
+
path: Path = typer.Option(Path("."), "--path", "-p", exists=True, file_okay=False, dir_okay=True, resolve_path=True, help="Repo path"),
|
|
412
|
+
output: Optional[Path] = typer.Option(None, "-o", help="Output file (default: repofail.lock.json in repo)"),
|
|
413
|
+
) -> None:
|
|
414
|
+
"""Generate repofail.lock.json from current host (and repo Dockerfile base). CI can then use repofail verify."""
|
|
415
|
+
try:
|
|
416
|
+
lock = generate_lock(path)
|
|
417
|
+
except NotADirectoryError as e:
|
|
418
|
+
_err(str(e))
|
|
419
|
+
out = output or path / LOCK_FILENAME
|
|
420
|
+
out.write_text(json.dumps(lock, indent=2))
|
|
421
|
+
typer.echo(f"Wrote {out}", err=True)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
@app.command("verify")
|
|
425
|
+
def verify_cmd(
|
|
426
|
+
path: Path = typer.Option(Path("."), "--path", "-p", exists=True, file_okay=False, dir_okay=True, resolve_path=True, help="Repo path"),
|
|
427
|
+
lock_file: Optional[Path] = typer.Option(None, "-f", help="Lock file path (default: repofail.lock.json in repo)"),
|
|
428
|
+
) -> None:
|
|
429
|
+
"""Verify current host matches repofail.lock.json. Exit 1 on drift (for CI)."""
|
|
430
|
+
lock_path = lock_file or path / LOCK_FILENAME
|
|
431
|
+
failures = verify_lock(lock_path)
|
|
432
|
+
if not failures:
|
|
433
|
+
typer.echo("OK: Host matches lock.")
|
|
434
|
+
return
|
|
435
|
+
typer.echo("Runtime drift detected:", err=True)
|
|
436
|
+
for field, expected, actual in failures:
|
|
437
|
+
typer.echo(f" {field}: expected {expected}, host {actual}", err=True)
|
|
438
|
+
raise typer.Exit(1)
|
|
439
|
+
|
|
440
|
+
|
|
364
441
|
@app.command("check")
|
|
365
442
|
def check_cmd(
|
|
366
443
|
contract_path: Path = typer.Argument(..., exists=True, path_type=Path, help="Contract JSON path"),
|
|
@@ -1,18 +1,43 @@
|
|
|
1
|
-
"""Stage 5 — Fleet/enterprise: audit, simulate."""
|
|
1
|
+
"""Stage 5 — Fleet/enterprise: audit, simulate, fleet scan."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
-
from
|
|
6
|
+
from collections import Counter
|
|
7
7
|
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
8
9
|
|
|
9
10
|
from .models import HostProfile, RepoProfile
|
|
10
11
|
from .scanner import scan_repo, inspect_host
|
|
11
12
|
from .engine import run_rules
|
|
12
13
|
|
|
14
|
+
try:
|
|
15
|
+
import yaml
|
|
16
|
+
except ImportError:
|
|
17
|
+
yaml = None # type: ignore[assignment]
|
|
13
18
|
|
|
14
19
|
SKIP_AUDIT_DIRS = {".git", "__pycache__", ".venv", "venv", "node_modules", ".tox", "build", "dist", "eggs"}
|
|
15
20
|
|
|
21
|
+
# Rule ID -> short category for risk clusters
|
|
22
|
+
RULE_CATEGORIES: dict[str, str] = {
|
|
23
|
+
"torch_cuda_mismatch": "ML/CUDA",
|
|
24
|
+
"python_version_mismatch": "Python",
|
|
25
|
+
"python_eol": "Python",
|
|
26
|
+
"spec_drift": "Spec drift",
|
|
27
|
+
"abi_wheel_mismatch": "ML/ARM",
|
|
28
|
+
"apple_silicon_wheels": "ML/ARM",
|
|
29
|
+
"node_engine_mismatch": "Node",
|
|
30
|
+
"node_eol": "Node",
|
|
31
|
+
"node_lock_file_missing": "Node",
|
|
32
|
+
"native_toolchain_missing": "Native build",
|
|
33
|
+
"gpu_memory_risk": "ML/RAM",
|
|
34
|
+
"node_native_windows": "Node/Windows",
|
|
35
|
+
"missing_system_libs": "System libs",
|
|
36
|
+
"docker_only": "Docker",
|
|
37
|
+
"lock_file_missing": "Lock file",
|
|
38
|
+
}
|
|
39
|
+
DEFAULT_CATEGORY = "Other"
|
|
40
|
+
|
|
16
41
|
|
|
17
42
|
def _is_repo(p: Path) -> bool:
|
|
18
43
|
"""Check if path looks like a repo (has .git or deps file)."""
|
|
@@ -25,6 +50,21 @@ def _is_repo(p: Path) -> bool:
|
|
|
25
50
|
)
|
|
26
51
|
|
|
27
52
|
|
|
53
|
+
def _load_policy(policy_path: Path | None) -> dict[str, Any]:
|
|
54
|
+
"""Load optional policy YAML: fail_on (HIGH/MEDIUM/LOW), max_repos, max_depth."""
|
|
55
|
+
if not policy_path or not policy_path.exists() or not yaml:
|
|
56
|
+
return {}
|
|
57
|
+
try:
|
|
58
|
+
data = yaml.safe_load(policy_path.read_text()) or {}
|
|
59
|
+
return {
|
|
60
|
+
"fail_on": data.get("fail_on", "HIGH"),
|
|
61
|
+
"max_repos": data.get("max_repos", 500),
|
|
62
|
+
"max_depth": data.get("max_depth", 4),
|
|
63
|
+
}
|
|
64
|
+
except Exception:
|
|
65
|
+
return {}
|
|
66
|
+
|
|
67
|
+
|
|
28
68
|
def _find_repos(base_path: Path, max_depth: int = 4, max_repos: int = 50) -> list[Path]:
|
|
29
69
|
"""Find repo roots recursively (nested repos)."""
|
|
30
70
|
found: list[Path] = []
|
|
@@ -87,6 +127,63 @@ def audit(base_path: Path) -> list[dict]:
|
|
|
87
127
|
return results
|
|
88
128
|
|
|
89
129
|
|
|
130
|
+
def fleet_scan(
|
|
131
|
+
base_path: Path,
|
|
132
|
+
policy_path: Path | None = None,
|
|
133
|
+
) -> dict[str, Any]:
|
|
134
|
+
"""
|
|
135
|
+
Scan all repos under base_path; optionally apply policy.
|
|
136
|
+
Returns: total_repos, violations (count), repos (list), by_rule (most common drift), risk_clusters.
|
|
137
|
+
"""
|
|
138
|
+
base_path = Path(base_path).resolve()
|
|
139
|
+
policy = _load_policy(policy_path)
|
|
140
|
+
max_repos = int(policy.get("max_repos", 500))
|
|
141
|
+
max_depth = int(policy.get("max_depth", 4))
|
|
142
|
+
|
|
143
|
+
dirs = _find_repos(base_path, max_depth=max_depth, max_repos=max_repos)
|
|
144
|
+
host = inspect_host()
|
|
145
|
+
repos: list[dict[str, Any]] = []
|
|
146
|
+
rule_counter: Counter[str] = Counter()
|
|
147
|
+
category_counter: Counter[str] = Counter()
|
|
148
|
+
|
|
149
|
+
for d in dirs:
|
|
150
|
+
try:
|
|
151
|
+
repo = scan_repo(d)
|
|
152
|
+
rule_results = run_rules(repo, host)
|
|
153
|
+
rule_ids = [r.rule_id for r in rule_results]
|
|
154
|
+
for rid in rule_ids:
|
|
155
|
+
rule_counter[rid] += 1
|
|
156
|
+
category_counter[RULE_CATEGORIES.get(rid, DEFAULT_CATEGORY)] += 1
|
|
157
|
+
high_count = sum(1 for r in rule_results if r.severity.value == "HIGH")
|
|
158
|
+
med_count = sum(1 for r in rule_results if r.severity.value in ("MEDIUM", "LOW"))
|
|
159
|
+
score = __import__("repofail.risk", fromlist=["estimate_success_probability"]).estimate_success_probability(rule_results)
|
|
160
|
+
repos.append({
|
|
161
|
+
"path": str(d),
|
|
162
|
+
"name": repo.name or d.name,
|
|
163
|
+
"rule_count": len(rule_results),
|
|
164
|
+
"rules": rule_ids,
|
|
165
|
+
"has_high": high_count > 0,
|
|
166
|
+
"high_count": high_count,
|
|
167
|
+
"medium_count": med_count,
|
|
168
|
+
"score": score,
|
|
169
|
+
})
|
|
170
|
+
except Exception:
|
|
171
|
+
pass
|
|
172
|
+
|
|
173
|
+
violations = sum(1 for r in repos if r["rule_count"] > 0)
|
|
174
|
+
by_rule = dict(rule_counter.most_common(15))
|
|
175
|
+
risk_clusters = [{"category": k, "count": v} for k, v in category_counter.most_common(10)]
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
"total_repos_scanned": len(repos),
|
|
179
|
+
"violations": violations,
|
|
180
|
+
"repos": repos,
|
|
181
|
+
"most_common_drift": by_rule,
|
|
182
|
+
"risk_clusters": risk_clusters,
|
|
183
|
+
"policy": policy,
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
|
|
90
187
|
def host_from_dict(data: dict) -> HostProfile:
|
|
91
188
|
"""Build HostProfile from dict (e.g. from JSON file)."""
|
|
92
189
|
return HostProfile(
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Runtime lock — pin host + optional docker base for CI enforcement."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from . import __version__
|
|
10
|
+
from .scanner import inspect_host, scan_repo
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
LOCK_FILENAME = "repofail.lock.json"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def generate_lock(repo_path: Path) -> dict[str, Any]:
|
|
17
|
+
"""Build lock dict from current host and repo (docker base if present)."""
|
|
18
|
+
host = inspect_host()
|
|
19
|
+
lock: dict[str, Any] = {
|
|
20
|
+
"repofail_lock": "1",
|
|
21
|
+
"generated_by": f"repofail {__version__}",
|
|
22
|
+
"python": host.python_version,
|
|
23
|
+
"node": host.node_version,
|
|
24
|
+
"cuda": host.cuda_version if host.cuda_available else None,
|
|
25
|
+
"arch": host.arch,
|
|
26
|
+
"os": host.os,
|
|
27
|
+
"docker_base": None,
|
|
28
|
+
}
|
|
29
|
+
try:
|
|
30
|
+
repo = scan_repo(repo_path)
|
|
31
|
+
if repo.raw.get("dockerfile"):
|
|
32
|
+
# First Dockerfile parsed may have base_image
|
|
33
|
+
df = repo.raw["dockerfile"]
|
|
34
|
+
if isinstance(df, dict) and df.get("base_image"):
|
|
35
|
+
lock["docker_base"] = df["base_image"]
|
|
36
|
+
# Also check first Dockerfile on disk if not in merged raw
|
|
37
|
+
if lock["docker_base"] is None and repo.has_dockerfile:
|
|
38
|
+
for p in (repo_path / "Dockerfile",):
|
|
39
|
+
if p.exists():
|
|
40
|
+
from .scanner.parsers import parse_dockerfile
|
|
41
|
+
data = parse_dockerfile(p)
|
|
42
|
+
if data.get("base_image"):
|
|
43
|
+
lock["docker_base"] = data["base_image"]
|
|
44
|
+
break
|
|
45
|
+
except Exception:
|
|
46
|
+
pass
|
|
47
|
+
return lock
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def verify_lock(lock_path: Path) -> list[tuple[str, str, str]]:
|
|
51
|
+
"""
|
|
52
|
+
Compare current host to lock. Returns list of (field, expected, actual) for mismatches.
|
|
53
|
+
"""
|
|
54
|
+
if not lock_path.exists():
|
|
55
|
+
return [("_lock", "file exists", str(lock_path) + " not found")]
|
|
56
|
+
data = json.loads(lock_path.read_text())
|
|
57
|
+
host = inspect_host()
|
|
58
|
+
|
|
59
|
+
failures: list[tuple[str, str, str]] = []
|
|
60
|
+
|
|
61
|
+
def _cmp(field: str, expected: Any, actual: Any) -> None:
|
|
62
|
+
if expected is None:
|
|
63
|
+
return
|
|
64
|
+
if expected != actual:
|
|
65
|
+
failures.append((field, str(expected), str(actual) if actual is not None else "missing"))
|
|
66
|
+
|
|
67
|
+
_cmp("python", data.get("python"), host.python_version)
|
|
68
|
+
_cmp("node", data.get("node"), host.node_version)
|
|
69
|
+
_cmp("arch", data.get("arch"), host.arch)
|
|
70
|
+
_cmp("os", data.get("os"), host.os)
|
|
71
|
+
|
|
72
|
+
if data.get("cuda") is not None:
|
|
73
|
+
if not host.cuda_available:
|
|
74
|
+
failures.append(("cuda", str(data.get("cuda")), "not available"))
|
|
75
|
+
elif host.cuda_version and data.get("cuda") != host.cuda_version:
|
|
76
|
+
failures.append(("cuda", str(data.get("cuda")), str(host.cuda_version)))
|
|
77
|
+
|
|
78
|
+
return failures
|
|
@@ -255,13 +255,27 @@ def parse_cargo_toml(path: Path) -> dict[str, Any]:
|
|
|
255
255
|
|
|
256
256
|
def parse_dockerfile(path: Path) -> dict[str, Any]:
|
|
257
257
|
"""Parse Dockerfile for base image, platform, Python."""
|
|
258
|
-
result: dict[str, Any] = {
|
|
258
|
+
result: dict[str, Any] = {
|
|
259
|
+
"has_cuda": False,
|
|
260
|
+
"python_version": None,
|
|
261
|
+
"platform_amd64": False,
|
|
262
|
+
"base_image": None,
|
|
263
|
+
}
|
|
259
264
|
if not path.exists():
|
|
260
265
|
return result
|
|
261
266
|
|
|
262
267
|
content = path.read_text(errors="replace")
|
|
263
268
|
result["has_cuda"] = "cuda" in content.lower() or "nvidia" in content.lower()
|
|
264
269
|
for line in content.splitlines():
|
|
270
|
+
stripped = line.strip()
|
|
271
|
+
if stripped.upper().startswith("FROM "):
|
|
272
|
+
# FROM image[:tag] [AS name]; skip --platform= and ARG
|
|
273
|
+
rest = stripped[5:].strip()
|
|
274
|
+
if rest and not result.get("base_image"):
|
|
275
|
+
base = rest.split()[0] if rest.split() else rest
|
|
276
|
+
if "AS " in base.upper():
|
|
277
|
+
base = base.upper().split(" AS ")[0].strip()
|
|
278
|
+
result["base_image"] = base
|
|
265
279
|
if "python:" in line.lower() or "python=" in line.lower():
|
|
266
280
|
m = re.search(r"python[:\s]*([\d.]+)", line, re.I)
|
|
267
281
|
if m:
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
# repofail GitHub Action
|
|
2
|
-
|
|
3
|
-
Run repofail on every PR: get a Markdown report as a comment and fail CI when compatibility violations exceed the threshold.
|
|
4
|
-
|
|
5
|
-
## Usage
|
|
6
|
-
|
|
7
|
-
In your repo, create `.github/workflows/repofail.yml`:
|
|
8
|
-
|
|
9
|
-
```yaml
|
|
10
|
-
name: repofail
|
|
11
|
-
on:
|
|
12
|
-
pull_request:
|
|
13
|
-
branches: [main, master]
|
|
14
|
-
jobs:
|
|
15
|
-
check:
|
|
16
|
-
runs-on: ubuntu-latest
|
|
17
|
-
steps:
|
|
18
|
-
- uses: actions/checkout@v4
|
|
19
|
-
- uses: jayvenn21/repofail/.github/actions/repofail@main
|
|
20
|
-
with:
|
|
21
|
-
path: .
|
|
22
|
-
fail_on: HIGH
|
|
23
|
-
comment: 'true'
|
|
24
|
-
upload_artifact: 'true'
|
|
25
|
-
pr_number: ${{ github.event.pull_request.number }}
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## Inputs
|
|
29
|
-
|
|
30
|
-
| Input | Default | Description |
|
|
31
|
-
|-------|---------|-------------|
|
|
32
|
-
| `path` | `.` | Repository path to scan |
|
|
33
|
-
| `fail_on` | `HIGH` | Fail CI when severity is HIGH, MEDIUM, or LOW |
|
|
34
|
-
| `comment` | `true` | Comment on PR with Markdown report |
|
|
35
|
-
| `upload_artifact` | `true` | Upload JSON report as workflow artifact |
|
|
36
|
-
| `pr_number` | (empty) | Set to `${{ github.event.pull_request.number }}` for PR comments |
|
|
37
|
-
|
|
38
|
-
## Behavior
|
|
39
|
-
|
|
40
|
-
1. Installs repofail from PyPI
|
|
41
|
-
2. Runs `repofail -m` and saves the report
|
|
42
|
-
3. Runs `repofail -j` for machine-readable output
|
|
43
|
-
4. Comments on the PR with the Markdown report (if `comment: true` and `pr_number` set)
|
|
44
|
-
5. Uploads `repofail.json` as an artifact (if `upload_artifact: true`)
|
|
45
|
-
6. Runs `repofail --ci` and fails the job if violations meet or exceed `fail_on`
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
name: repofail
|
|
2
|
-
description: Deterministic runtime compatibility check — fail CI or comment on PR with report
|
|
3
|
-
inputs:
|
|
4
|
-
path:
|
|
5
|
-
description: Repository path to scan (default .)
|
|
6
|
-
required: false
|
|
7
|
-
default: .
|
|
8
|
-
fail_on:
|
|
9
|
-
description: Severity threshold to fail CI (HIGH, MEDIUM, LOW)
|
|
10
|
-
required: false
|
|
11
|
-
default: HIGH
|
|
12
|
-
comment:
|
|
13
|
-
description: Comment on PR with Markdown report
|
|
14
|
-
required: false
|
|
15
|
-
default: 'true'
|
|
16
|
-
upload_artifact:
|
|
17
|
-
description: Upload JSON report as artifact
|
|
18
|
-
required: false
|
|
19
|
-
default: 'true'
|
|
20
|
-
pr_number:
|
|
21
|
-
description: Pull request number (for commenting). Set to ${{ github.event.pull_request.number }}
|
|
22
|
-
required: false
|
|
23
|
-
default: ''
|
|
24
|
-
outputs:
|
|
25
|
-
markdown_file:
|
|
26
|
-
description: Path to Markdown report file (repofail-report.md)
|
|
27
|
-
runs:
|
|
28
|
-
using: composite
|
|
29
|
-
steps:
|
|
30
|
-
- name: Install repofail
|
|
31
|
-
shell: bash
|
|
32
|
-
run: pip install repofail
|
|
33
|
-
|
|
34
|
-
- name: Run repofail (Markdown)
|
|
35
|
-
id: md
|
|
36
|
-
shell: bash
|
|
37
|
-
run: |
|
|
38
|
-
repofail -p "${{ inputs.path }}" -m > repofail-report.md || true
|
|
39
|
-
echo "markdown_file=repofail-report.md" >> $GITHUB_OUTPUT
|
|
40
|
-
|
|
41
|
-
- name: Run repofail (JSON)
|
|
42
|
-
if: inputs.upload_artifact == 'true'
|
|
43
|
-
shell: bash
|
|
44
|
-
run: repofail -p "${{ inputs.path }}" -j > repofail.json || true
|
|
45
|
-
|
|
46
|
-
- name: Comment on PR with report
|
|
47
|
-
if: inputs.comment == 'true' && inputs.pr_number != ''
|
|
48
|
-
uses: actions/github-script@v7
|
|
49
|
-
with:
|
|
50
|
-
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
51
|
-
script: |
|
|
52
|
-
const fs = require('fs');
|
|
53
|
-
const body = fs.existsSync('repofail-report.md')
|
|
54
|
-
? fs.readFileSync('repofail-report.md', 'utf8')
|
|
55
|
-
: 'repofail report not available.';
|
|
56
|
-
const comment = `## repofail · compatibility report\n\n\`\`\`\n${body}\n\`\`\``;
|
|
57
|
-
await github.rest.issues.createComment({
|
|
58
|
-
issue_number: ${{ inputs.pr_number }},
|
|
59
|
-
owner: context.repo.owner,
|
|
60
|
-
repo: context.repo.repo,
|
|
61
|
-
body: comment,
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
- name: Upload repofail JSON artifact
|
|
65
|
-
if: inputs.upload_artifact == 'true' && always()
|
|
66
|
-
uses: actions/upload-artifact@v4
|
|
67
|
-
with:
|
|
68
|
-
name: repofail-report
|
|
69
|
-
path: repofail.json
|
|
70
|
-
|
|
71
|
-
- name: Run repofail (CI — fail if threshold exceeded)
|
|
72
|
-
shell: bash
|
|
73
|
-
run: repofail -p "${{ inputs.path }}" --ci --fail-on "${{ inputs.fail_on }}"
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
# Homebrew formula for repofail
|
|
2
|
-
# To use: copy to your tap repo (e.g. homebrew-tap/Formula/repofail.rb)
|
|
3
|
-
# Then: brew tap jayvenn21/tap && brew install jayvenn21/tap/repofail
|
|
4
|
-
#
|
|
5
|
-
# Homebrew uses --no-deps when installing the main package, so all Python
|
|
6
|
-
# dependencies must be declared as resources below.
|
|
7
|
-
# If SHA256 fails after pushing a new tag, get the correct hash with:
|
|
8
|
-
# curl -sL https://github.com/jayvenn21/repofail/archive/refs/tags/vX.Y.Z.tar.gz | shasum -a 256
|
|
9
|
-
|
|
10
|
-
class Repofail < Formula
|
|
11
|
-
include Language::Python::Virtualenv
|
|
12
|
-
|
|
13
|
-
desc "Deterministic runtime compatibility analyzer"
|
|
14
|
-
homepage "https://github.com/jayvenn21/repofail"
|
|
15
|
-
url "https://github.com/jayvenn21/repofail/archive/refs/tags/v0.2.4.tar.gz"
|
|
16
|
-
sha256 "d5558cd419c8d46bdc958064cb97f963d1ea793866414c025906ec15033512ed"
|
|
17
|
-
license "MIT"
|
|
18
|
-
|
|
19
|
-
# App supports Python 3.10+. We use 3.12 for current Homebrew; change to
|
|
20
|
-
# python@3.10 or python@3.11 if you need an older interpreter.
|
|
21
|
-
depends_on "libyaml"
|
|
22
|
-
depends_on "python@3.12"
|
|
23
|
-
|
|
24
|
-
resource "click" do
|
|
25
|
-
url "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz"
|
|
26
|
-
sha256 "12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
resource "typer" do
|
|
30
|
-
url "https://files.pythonhosted.org/packages/ac/0a/d55af35db5f50f486e3eda0ada747eed773859e2699d3ce570b682a9b70a/typer-0.12.3.tar.gz"
|
|
31
|
-
sha256 "49e73131481d804288ef62598d97a1ceef3058905aa536a1134f90891ba35482"
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
resource "rich" do
|
|
35
|
-
url "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz"
|
|
36
|
-
sha256 "439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
resource "shellingham" do
|
|
40
|
-
url "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz"
|
|
41
|
-
sha256 "8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
resource "typing-extensions" do
|
|
45
|
-
url "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz"
|
|
46
|
-
sha256 "1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
resource "tomli" do
|
|
50
|
-
url "https://files.pythonhosted.org/packages/35/b9/de2a5c0144d7d75a57ff355c0c24054f965b2dc3036456ae03a51ea6264b/tomli-2.0.2.tar.gz"
|
|
51
|
-
sha256 "d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
resource "pyyaml" do
|
|
55
|
-
url "https://github.com/yaml/pyyaml/archive/refs/tags/6.0.1.tar.gz"
|
|
56
|
-
sha256 "57314c984aaa84318eed00cf5a8365afc49f87954969e295efe2ba99f3b21f7a"
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def install
|
|
60
|
-
virtualenv_install_with_resources
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
test do
|
|
64
|
-
assert_match "repofail", shell_output("#{bin}/repofail --help", 0)
|
|
65
|
-
end
|
|
66
|
-
end
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
# repofail — 30-second quickstart
|
|
2
|
-
|
|
3
|
-
## What it does
|
|
4
|
-
|
|
5
|
-
Answers one question: **Will this repository run on this machine?**
|
|
6
|
-
|
|
7
|
-
It does **not** install your project. It reads configs and your environment, then reports compatibility issues (Node version, Python range, CUDA, lock file, spec drift, etc.) with a single score and clear evidence.
|
|
8
|
-
|
|
9
|
-
## Install
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
pip install repofail
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
Or: `pipx install repofail` | [install.sh](https://raw.githubusercontent.com/jayvenn21/repofail/main/install.sh) | Homebrew (see README).
|
|
16
|
-
|
|
17
|
-
## Run
|
|
18
|
-
|
|
19
|
-
```bash
|
|
20
|
-
cd /path/to/any/repo
|
|
21
|
-
repofail
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
You get:
|
|
25
|
-
|
|
26
|
-
- A **compatibility score** (0–100%)
|
|
27
|
-
- **HARD FAILURES** (deterministic blockers) with evidence and suggested fixes
|
|
28
|
-
- Optional: **RUNTIME RISKS**, **STRUCTURAL PROFILE**
|
|
29
|
-
|
|
30
|
-
## CI
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
repofail --ci
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
Exits 1 if HIGH (or configured) severity violations are found. Use in GitHub Actions or any CI.
|
|
37
|
-
|
|
38
|
-
## Next
|
|
39
|
-
|
|
40
|
-
- `repofail -e list` — list all rules
|
|
41
|
-
- `repofail -e spec_drift` — explain a rule
|
|
42
|
-
- [README](https://github.com/jayvenn21/repofail#readme) — full docs, FAQ, when not to use
|
repofail-0.2.4/docs/logo.png
DELETED
|
Binary file
|
repofail-0.2.4/install.sh
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Install repofail — deterministic runtime compatibility analyzer
|
|
3
|
-
# Usage: curl -sSL https://raw.githubusercontent.com/jayvenn21/repofail/main/install.sh | bash
|
|
4
|
-
|
|
5
|
-
set -e
|
|
6
|
-
|
|
7
|
-
if command -v pipx &>/dev/null; then
|
|
8
|
-
echo "Installing repofail via pipx (isolated)..."
|
|
9
|
-
pipx install repofail
|
|
10
|
-
elif command -v pip3 &>/dev/null; then
|
|
11
|
-
echo "Installing repofail via pip..."
|
|
12
|
-
pip3 install repofail
|
|
13
|
-
elif command -v pip &>/dev/null; then
|
|
14
|
-
echo "Installing repofail via pip..."
|
|
15
|
-
pip install repofail
|
|
16
|
-
else
|
|
17
|
-
echo "Error: need pip or pipx. Install Python 3.10+ and run: pip install repofail"
|
|
18
|
-
exit 1
|
|
19
|
-
fi
|
|
20
|
-
|
|
21
|
-
echo "Done. Run: repofail --help"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|