predeploy-check 1.0.0 → 1.1.1
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.
- package/LICENSE +21 -0
- package/README.md +105 -74
- package/bin/cli.js +50 -48
- package/checks/01-python-render.js +207 -123
- package/checks/03-case-sensitivity.js +240 -228
- package/index.js +137 -130
- package/package.json +45 -37
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alok Kushwaha
|
|
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.
|
package/README.md
CHANGED
|
@@ -1,74 +1,105 @@
|
|
|
1
|
-
# predeploy-check
|
|
2
|
-
|
|
3
|
-
> Catch deployment failures **before** you push. Scans your project for known Render & Vercel pitfalls.
|
|
4
|
-
|
|
5
|
-
```bash
|
|
6
|
-
npx predeploy-check
|
|
7
|
-
```
|
|
8
|
-
|
|
9
|
-
No install required — just run it in your project directory.
|
|
10
|
-
|
|
11
|
-
## What it checks
|
|
12
|
-
|
|
13
|
-
| # | Check | Targets | Status |
|
|
14
|
-
|---|-------|---------|--------|
|
|
15
|
-
| 1 | **Python + Render** | Rust-compiled deps (pydantic, fastapi, orjson…) on Python ≥ 3.13 | ⚠️ Warn |
|
|
16
|
-
| 2 | **ESLint + Vercel** | Mismatched eslint / eslint-config-next versions; deprecated `ignoreDuringBuilds` on Next 16+ | ❌ Fail / ⚠️ Warn |
|
|
17
|
-
| 3 | **Case Sensitivity** | Import paths that differ in casing from actual filenames (breaks on Linux) | ⚠️ Warn |
|
|
18
|
-
| 4 | **Missing Engines** | No `"engines"` field in package.json | ⚠️ Warn |
|
|
19
|
-
| 5 | **Env Var Check** | `process.env.X` references not declared in `.env` / `.env.example` | ⚠️ Warn |
|
|
20
|
-
| 6 | **Render Start Cmd** | No Procfile, no `"start"` script, no render.yaml start command | ❌ Fail |
|
|
21
|
-
|
|
22
|
-
## Output
|
|
23
|
-
|
|
24
|
-
Clean, colored terminal output with:
|
|
25
|
-
- ✅ / ⚠️ / ❌ per check
|
|
26
|
-
- File and line context
|
|
27
|
-
- One-line suggested fix
|
|
28
|
-
|
|
29
|
-
Exit code `1` if any ❌ failures, `0` otherwise.
|
|
30
|
-
|
|
31
|
-
## Usage
|
|
32
|
-
|
|
33
|
-
```bash
|
|
34
|
-
# Scan current directory
|
|
35
|
-
npx predeploy-check
|
|
36
|
-
|
|
37
|
-
# Scan a specific project
|
|
38
|
-
npx predeploy-check ./my-project
|
|
39
|
-
|
|
40
|
-
#
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
1
|
+
# predeploy-check
|
|
2
|
+
|
|
3
|
+
> Catch deployment failures **before** you push. Scans your project for known Render & Vercel pitfalls.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx predeploy-check
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
No install required — just run it in your project directory.
|
|
10
|
+
|
|
11
|
+
## What it checks
|
|
12
|
+
|
|
13
|
+
| # | Check | Targets | Status |
|
|
14
|
+
|---|-------|---------|--------|
|
|
15
|
+
| 1 | **Python + Render** | Rust-compiled deps (pydantic, fastapi, orjson…) on Python ≥ 3.13 — add `--live` to verify against PyPI instead of guessing | ⚠️ Warn / ❌ Fail (live) |
|
|
16
|
+
| 2 | **ESLint + Vercel** | Mismatched eslint / eslint-config-next versions; deprecated `ignoreDuringBuilds` on Next 16+ | ❌ Fail / ⚠️ Warn |
|
|
17
|
+
| 3 | **Case Sensitivity** | Import paths that differ in casing from actual filenames (breaks on Linux) | ⚠️ Warn |
|
|
18
|
+
| 4 | **Missing Engines** | No `"engines"` field in package.json | ⚠️ Warn |
|
|
19
|
+
| 5 | **Env Var Check** | `process.env.X` references not declared in `.env` / `.env.example` | ⚠️ Warn |
|
|
20
|
+
| 6 | **Render Start Cmd** | No Procfile, no `"start"` script, no render.yaml start command | ❌ Fail |
|
|
21
|
+
|
|
22
|
+
## Output
|
|
23
|
+
|
|
24
|
+
Clean, colored terminal output with:
|
|
25
|
+
- ✅ / ⚠️ / ❌ per check
|
|
26
|
+
- File and line context
|
|
27
|
+
- One-line suggested fix
|
|
28
|
+
|
|
29
|
+
Exit code `1` if any ❌ failures, `0` otherwise.
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Scan current directory
|
|
35
|
+
npx predeploy-check
|
|
36
|
+
|
|
37
|
+
# Scan a specific project
|
|
38
|
+
npx predeploy-check ./my-project
|
|
39
|
+
|
|
40
|
+
# Verify Python wheel availability live against PyPI, instead of
|
|
41
|
+
# relying on the built-in known-package list (slower, needs internet)
|
|
42
|
+
npx predeploy-check --live
|
|
43
|
+
|
|
44
|
+
# Show help
|
|
45
|
+
npx predeploy-check --help
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
By default, the Python + Render check warns based on a built-in list of
|
|
49
|
+
packages known to have Rust-compiled components — it's instant and works
|
|
50
|
+
offline, but the list can go stale as packages ship new wheels over time.
|
|
51
|
+
Pass `--live` to query PyPI directly for the exact pinned version in your
|
|
52
|
+
`requirements.txt`, which turns a "might be missing" warning into a
|
|
53
|
+
confirmed pass or fail.
|
|
54
|
+
|
|
55
|
+
## Adding custom checks
|
|
56
|
+
|
|
57
|
+
Create a new file in the `checks/` folder:
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
// checks/07-my-check.js
|
|
61
|
+
'use strict';
|
|
62
|
+
|
|
63
|
+
const name = 'My Custom Check';
|
|
64
|
+
|
|
65
|
+
async function run(projectRoot) {
|
|
66
|
+
// Your check logic here
|
|
67
|
+
return {
|
|
68
|
+
status: 'pass', // 'pass' | 'warn' | 'fail' | 'skip'
|
|
69
|
+
message: 'Everything looks good',
|
|
70
|
+
fix: 'Suggested fix if status is warn or fail',
|
|
71
|
+
details: [
|
|
72
|
+
{ file: 'some-file.js', line: 42, message: 'Detail about the issue' }
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = { name, run };
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Checks are loaded alphabetically, so prefix with a number to control order.
|
|
81
|
+
|
|
82
|
+
## Testing
|
|
83
|
+
|
|
84
|
+
The project has a full test suite (49 tests covering all 6 checks) built
|
|
85
|
+
on Node's built-in test runner — no extra dependencies needed.
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npm test
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Contributing
|
|
92
|
+
|
|
93
|
+
Found a deploy-only failure that isn't on this list? Open an issue or a
|
|
94
|
+
PR — the project is intentionally narrow right now (six checks), and it
|
|
95
|
+
gets more useful with every real-world gotcha someone adds.
|
|
96
|
+
|
|
97
|
+
## Author
|
|
98
|
+
|
|
99
|
+
Built by [Alok Kushwaha](https://github.com/Alok-Fusion) — NLP/ML
|
|
100
|
+
engineer, born out of a real afternoon lost to a deploy failure that
|
|
101
|
+
had nothing to do with the actual code.
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
MIT — see [LICENSE](./LICENSE) for details.
|
package/bin/cli.js
CHANGED
|
@@ -1,48 +1,50 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
'use strict';
|
|
4
|
-
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const { runAllChecks } = require('../index');
|
|
7
|
-
|
|
8
|
-
const args = process.argv.slice(2);
|
|
9
|
-
|
|
10
|
-
if (args.includes('--help') || args.includes('-h')) {
|
|
11
|
-
console.log(`
|
|
12
|
-
predeploy-check — catch deployment failures before they happen
|
|
13
|
-
|
|
14
|
-
Usage:
|
|
15
|
-
npx predeploy-check [directory]
|
|
16
|
-
|
|
17
|
-
Arguments:
|
|
18
|
-
directory Path to the project to scan (defaults to current directory)
|
|
19
|
-
|
|
20
|
-
Options:
|
|
21
|
-
-h, --help Show this help message
|
|
22
|
-
-v, --version Show version number
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { runAllChecks } = require('../index');
|
|
7
|
+
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
|
|
10
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
11
|
+
console.log(`
|
|
12
|
+
predeploy-check — catch deployment failures before they happen
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
npx predeploy-check [directory]
|
|
16
|
+
|
|
17
|
+
Arguments:
|
|
18
|
+
directory Path to the project to scan (defaults to current directory)
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
-h, --help Show this help message
|
|
22
|
+
-v, --version Show version number
|
|
23
|
+
--live Verify Python wheel availability against PyPI (slower, needs internet)
|
|
24
|
+
|
|
25
|
+
Checks:
|
|
26
|
+
1. Python + Render: Rust-compiled deps on unsupported Python versions
|
|
27
|
+
2. ESLint + Vercel: Peer-dependency mismatches in Next.js projects
|
|
28
|
+
3. Case Sensitivity: Import paths that differ from actual filenames
|
|
29
|
+
4. Missing Engines: No "engines" field in package.json
|
|
30
|
+
5. Env Var Check: process.env references missing from .env files
|
|
31
|
+
6. Render Start Cmd: Missing start command for Render deployments
|
|
32
|
+
`);
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (args.includes('--version') || args.includes('-v')) {
|
|
37
|
+
const pkg = require('../package.json');
|
|
38
|
+
console.log(pkg.version);
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const projectRoot = path.resolve(args.find((a) => !a.startsWith('-')) || process.cwd());
|
|
43
|
+
const options = { live: args.includes('--live') };
|
|
44
|
+
|
|
45
|
+
runAllChecks(projectRoot, options).then((exitCode) => {
|
|
46
|
+
process.exit(exitCode);
|
|
47
|
+
}).catch((err) => {
|
|
48
|
+
console.error('Fatal error:', err.message);
|
|
49
|
+
process.exit(2);
|
|
50
|
+
});
|
|
@@ -1,123 +1,207 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
|
|
6
|
-
const name = 'Python + Render: Rust-compiled dependencies';
|
|
7
|
-
|
|
8
|
-
// Packages known to have Rust-compiled components that may lack
|
|
9
|
-
// prebuilt wheels for bleeding-edge Python versions.
|
|
10
|
-
const RUST_DEP_PACKAGES = [
|
|
11
|
-
'pydantic',
|
|
12
|
-
'pydantic-core',
|
|
13
|
-
'fastapi',
|
|
14
|
-
'orjson',
|
|
15
|
-
'cryptography',
|
|
16
|
-
'tiktoken',
|
|
17
|
-
'tokenizers',
|
|
18
|
-
'safetensors',
|
|
19
|
-
'polars',
|
|
20
|
-
'ruff',
|
|
21
|
-
'rpds-py',
|
|
22
|
-
'jiter',
|
|
23
|
-
];
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Parse a Python version string like "python-3.13.2" or "3.13" into
|
|
27
|
-
* { major, minor } or null if unparseable.
|
|
28
|
-
*/
|
|
29
|
-
function parsePythonVersion(raw) {
|
|
30
|
-
const match = raw.match(/(\d+)\.(\d+)/);
|
|
31
|
-
if (!match) return null;
|
|
32
|
-
return { major: parseInt(match[1], 10), minor: parseInt(match[2], 10) };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Parse requirements.txt and return an array of
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
.
|
|
42
|
-
.
|
|
43
|
-
.
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const name = 'Python + Render: Rust-compiled dependencies';
|
|
7
|
+
|
|
8
|
+
// Packages known to have Rust-compiled components that may lack
|
|
9
|
+
// prebuilt wheels for bleeding-edge Python versions.
|
|
10
|
+
const RUST_DEP_PACKAGES = [
|
|
11
|
+
'pydantic',
|
|
12
|
+
'pydantic-core',
|
|
13
|
+
'fastapi',
|
|
14
|
+
'orjson',
|
|
15
|
+
'cryptography',
|
|
16
|
+
'tiktoken',
|
|
17
|
+
'tokenizers',
|
|
18
|
+
'safetensors',
|
|
19
|
+
'polars',
|
|
20
|
+
'ruff',
|
|
21
|
+
'rpds-py',
|
|
22
|
+
'jiter',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parse a Python version string like "python-3.13.2" or "3.13" into
|
|
27
|
+
* { major, minor } or null if unparseable.
|
|
28
|
+
*/
|
|
29
|
+
function parsePythonVersion(raw) {
|
|
30
|
+
const match = raw.match(/(\d+)\.(\d+)/);
|
|
31
|
+
if (!match) return null;
|
|
32
|
+
return { major: parseInt(match[1], 10), minor: parseInt(match[2], 10) };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Parse requirements.txt and return an array of { name, version } where
|
|
37
|
+
* version is the pinned version if one was specified with "==", else null.
|
|
38
|
+
*/
|
|
39
|
+
function parseRequirements(content) {
|
|
40
|
+
return content
|
|
41
|
+
.split('\n')
|
|
42
|
+
.map((line) => line.trim())
|
|
43
|
+
.filter((line) => line && !line.startsWith('#') && !line.startsWith('-'))
|
|
44
|
+
.map((line) => {
|
|
45
|
+
const beforeSemicolon = line.split(';')[0].trim(); // strip env markers
|
|
46
|
+
const name = beforeSemicolon.split(/[>=<!~\[\s]/)[0].toLowerCase().replace(/_/g, '-');
|
|
47
|
+
const pinMatch = beforeSemicolon.match(/==\s*([\d][\w.]*)/);
|
|
48
|
+
return { name, version: pinMatch ? pinMatch[1] : null };
|
|
49
|
+
})
|
|
50
|
+
.filter((pkg) => pkg.name);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Query PyPI's JSON API to check whether a prebuilt wheel exists for the
|
|
55
|
+
* given package, version, and CPython minor version (e.g. "313" for 3.13).
|
|
56
|
+
* Returns true/false/null (null = couldn't determine, e.g. network error
|
|
57
|
+
* or unpinned version — caller should fall back to the static warning).
|
|
58
|
+
*/
|
|
59
|
+
async function checkWheelAvailability(pkgName, pkgVersion, major, minor) {
|
|
60
|
+
if (!pkgVersion) return null; // no pinned version, can't query a specific release
|
|
61
|
+
|
|
62
|
+
if (typeof fetch !== 'function') {
|
|
63
|
+
// Node < 18 has no global fetch. Treat as unknown rather than crashing,
|
|
64
|
+
// so --live degrades to the same "unverified" warn path as a network failure.
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const cpTag = `cp${major}${minor}`;
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const res = await fetch(`https://pypi.org/pypi/${pkgName}/${pkgVersion}/json`, {
|
|
72
|
+
signal: AbortSignal.timeout(5000),
|
|
73
|
+
});
|
|
74
|
+
if (!res.ok) return null;
|
|
75
|
+
|
|
76
|
+
const data = await res.json();
|
|
77
|
+
const urls = data.urls || [];
|
|
78
|
+
|
|
79
|
+
const hasMatchingWheel = urls.some(
|
|
80
|
+
(u) => u.packagetype === 'bdist_wheel' && u.filename.includes(`-${cpTag}-`)
|
|
81
|
+
);
|
|
82
|
+
return hasMatchingWheel;
|
|
83
|
+
} catch {
|
|
84
|
+
return null; // network error, timeout, bad JSON, etc. — treat as unknown
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function run(projectRoot, options = {}) {
|
|
89
|
+
const runtimePath = path.join(projectRoot, 'runtime.txt');
|
|
90
|
+
const requirementsPath = path.join(projectRoot, 'requirements.txt');
|
|
91
|
+
|
|
92
|
+
// Check if this is a Python project at all
|
|
93
|
+
if (!fs.existsSync(runtimePath) && !fs.existsSync(requirementsPath)) {
|
|
94
|
+
return {
|
|
95
|
+
status: 'skip',
|
|
96
|
+
message: `${name} — no Python project detected`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// If there's no runtime.txt, we can't check the version
|
|
101
|
+
if (!fs.existsSync(runtimePath)) {
|
|
102
|
+
return {
|
|
103
|
+
status: 'skip',
|
|
104
|
+
message: `${name} — no runtime.txt found, skipping version check`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const runtimeContent = fs.readFileSync(runtimePath, 'utf-8').trim();
|
|
109
|
+
const version = parsePythonVersion(runtimeContent);
|
|
110
|
+
|
|
111
|
+
if (!version) {
|
|
112
|
+
return {
|
|
113
|
+
status: 'warn',
|
|
114
|
+
message: `${name} — could not parse Python version from runtime.txt`,
|
|
115
|
+
fix: 'Ensure runtime.txt contains a valid version like "python-3.12.3"',
|
|
116
|
+
details: [{ file: 'runtime.txt', message: `Content: "${runtimeContent}"` }],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Only flag Python >= 3.13
|
|
121
|
+
if (version.major < 3 || (version.major === 3 && version.minor < 13)) {
|
|
122
|
+
return {
|
|
123
|
+
status: 'pass',
|
|
124
|
+
message: `${name} — Python ${version.major}.${version.minor} is well-supported`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Now check requirements.txt for Rust-compiled deps
|
|
129
|
+
if (!fs.existsSync(requirementsPath)) {
|
|
130
|
+
return {
|
|
131
|
+
status: 'warn',
|
|
132
|
+
message: `${name} — Python ${version.major}.${version.minor} detected but no requirements.txt found`,
|
|
133
|
+
fix: 'Add a requirements.txt or pin Python to 3.12 in runtime.txt',
|
|
134
|
+
details: [{ file: 'runtime.txt' }],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const reqContent = fs.readFileSync(requirementsPath, 'utf-8');
|
|
139
|
+
const packages = parseRequirements(reqContent);
|
|
140
|
+
const flagged = packages.filter((pkg) => RUST_DEP_PACKAGES.includes(pkg.name));
|
|
141
|
+
|
|
142
|
+
if (flagged.length === 0) {
|
|
143
|
+
return {
|
|
144
|
+
status: 'pass',
|
|
145
|
+
message: `${name} — Python ${version.major}.${version.minor} with no known Rust-compiled deps`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── Static mode (default): warn based on the known-package list alone ──
|
|
150
|
+
if (!options.live) {
|
|
151
|
+
return {
|
|
152
|
+
status: 'warn',
|
|
153
|
+
message: `${name} — Python ${version.major}.${version.minor} with Rust-compiled dependencies`,
|
|
154
|
+
fix: `Pin to Python 3.12 in runtime.txt ("python-3.12.7"), or re-run with --live to verify wheel availability for ${version.major}.${version.minor}`,
|
|
155
|
+
details: flagged.map((pkg) => ({
|
|
156
|
+
file: 'requirements.txt',
|
|
157
|
+
message: `"${pkg.name}" has Rust-compiled components — prebuilt wheels may not exist for Python ${version.major}.${version.minor}`,
|
|
158
|
+
})),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── Live mode: actually query PyPI to confirm wheel availability ──
|
|
163
|
+
const liveResults = await Promise.all(
|
|
164
|
+
flagged.map(async (pkg) => {
|
|
165
|
+
const available = await checkWheelAvailability(pkg.name, pkg.version, version.major, version.minor);
|
|
166
|
+
return { ...pkg, available };
|
|
167
|
+
})
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const confirmedMissing = liveResults.filter((r) => r.available === false);
|
|
171
|
+
const unknown = liveResults.filter((r) => r.available === null);
|
|
172
|
+
const confirmedOk = liveResults.filter((r) => r.available === true);
|
|
173
|
+
|
|
174
|
+
if (confirmedMissing.length === 0 && unknown.length === 0) {
|
|
175
|
+
return {
|
|
176
|
+
status: 'pass',
|
|
177
|
+
message: `${name} — verified wheels exist on PyPI for Python ${version.major}.${version.minor}`,
|
|
178
|
+
details: confirmedOk.map((pkg) => ({
|
|
179
|
+
file: 'requirements.txt',
|
|
180
|
+
message: `"${pkg.name}${pkg.version ? `==${pkg.version}` : ''}" has a confirmed cp${version.major}${version.minor} wheel`,
|
|
181
|
+
})),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const details = [];
|
|
186
|
+
for (const pkg of confirmedMissing) {
|
|
187
|
+
details.push({
|
|
188
|
+
file: 'requirements.txt',
|
|
189
|
+
message: `"${pkg.name}${pkg.version ? `==${pkg.version}` : ''}" — confirmed: no wheel for Python ${version.major}.${version.minor}`,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
for (const pkg of unknown) {
|
|
193
|
+
details.push({
|
|
194
|
+
file: 'requirements.txt',
|
|
195
|
+
message: `"${pkg.name}" — could not verify (unpinned version or PyPI lookup failed); treat as at-risk`,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
status: confirmedMissing.length > 0 ? 'fail' : 'warn',
|
|
201
|
+
message: `${name} — wheel availability ${confirmedMissing.length > 0 ? 'confirmed missing' : 'unverified'} for Python ${version.major}.${version.minor}`,
|
|
202
|
+
fix: `Pin to Python 3.12 in runtime.txt ("python-3.12.7"), or upgrade the affected package(s) to a version with a cp${version.major}${version.minor} wheel`,
|
|
203
|
+
details,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
module.exports = { name, run };
|