forge-publish 2.3.0__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.
- forge_publish-2.3.0/.github/scripts/pypi-distributions.py +54 -0
- forge_publish-2.3.0/.github/scripts/release-checksums.sh +7 -0
- forge_publish-2.3.0/.github/scripts/release-sbom.py +245 -0
- forge_publish-2.3.0/.github/scripts/tooling-constraints.py +122 -0
- forge_publish-2.3.0/.github/scripts/typecheck.py +36 -0
- forge_publish-2.3.0/.github/typecheck/package-lock.json +45 -0
- forge_publish-2.3.0/.github/typecheck/package.json +7 -0
- forge_publish-2.3.0/.pre-commit-config.yaml +16 -0
- forge_publish-2.3.0/LICENSE +202 -0
- forge_publish-2.3.0/MANIFEST.in +11 -0
- forge_publish-2.3.0/PKG-INFO +188 -0
- forge_publish-2.3.0/README.md +158 -0
- forge_publish-2.3.0/pyproject.toml +87 -0
- forge_publish-2.3.0/requirements/build.txt +3 -0
- forge_publish-2.3.0/requirements/generator.txt +2 -0
- forge_publish-2.3.0/requirements/tooling.in +4 -0
- forge_publish-2.3.0/requirements/tooling.txt +110 -0
- forge_publish-2.3.0/setup.cfg +4 -0
- forge_publish-2.3.0/src/forge_publish/__init__.py +3 -0
- forge_publish-2.3.0/src/forge_publish/__main__.py +4 -0
- forge_publish-2.3.0/src/forge_publish/cli.py +211 -0
- forge_publish-2.3.0/src/forge_publish/client.py +167 -0
- forge_publish-2.3.0/src/forge_publish/config.py +229 -0
- forge_publish-2.3.0/src/forge_publish/errors.py +10 -0
- forge_publish-2.3.0/src/forge_publish/publishers/__init__.py +0 -0
- forge_publish-2.3.0/src/forge_publish/publishers/deb.py +460 -0
- forge_publish-2.3.0/src/forge_publish/publishers/generic.py +84 -0
- forge_publish-2.3.0/src/forge_publish/publishers/npm.py +281 -0
- forge_publish-2.3.0/src/forge_publish.egg-info/PKG-INFO +188 -0
- forge_publish-2.3.0/src/forge_publish.egg-info/SOURCES.txt +46 -0
- forge_publish-2.3.0/src/forge_publish.egg-info/dependency_links.txt +1 -0
- forge_publish-2.3.0/src/forge_publish.egg-info/entry_points.txt +2 -0
- forge_publish-2.3.0/src/forge_publish.egg-info/requires.txt +18 -0
- forge_publish-2.3.0/src/forge_publish.egg-info/top_level.txt +1 -0
- forge_publish-2.3.0/tests/conftest.py +52 -0
- forge_publish-2.3.0/tests/fixtures/npm_lifecycle/lifecycle.cjs +14 -0
- forge_publish-2.3.0/tests/fixtures/npm_lifecycle/package.json +17 -0
- forge_publish-2.3.0/tests/test_cli.py +462 -0
- forge_publish-2.3.0/tests/test_client.py +362 -0
- forge_publish-2.3.0/tests/test_config.py +484 -0
- forge_publish-2.3.0/tests/test_deb.py +1000 -0
- forge_publish-2.3.0/tests/test_deb_fuzz.py +253 -0
- forge_publish-2.3.0/tests/test_generic.py +114 -0
- forge_publish-2.3.0/tests/test_npm.py +637 -0
- forge_publish-2.3.0/tests/test_pypi_distributions.py +102 -0
- forge_publish-2.3.0/tests/test_release_checksums.py +87 -0
- forge_publish-2.3.0/tests/test_release_sbom.py +207 -0
- forge_publish-2.3.0/tests/test_tooling_constraints.py +105 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Verify GitHub release assets and stage only their wheel/sdist for PyPI."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import hashlib
|
|
5
|
+
import re
|
|
6
|
+
import shutil
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def stage_distributions(assets: Path, output: Path, tag: str) -> None:
|
|
11
|
+
if not re.fullmatch(r"v\d+\.\d+\.\d+", tag):
|
|
12
|
+
raise ValueError("Expected a stable vMAJOR.MINOR.PATCH release tag")
|
|
13
|
+
version = tag[1:]
|
|
14
|
+
distributions = {
|
|
15
|
+
f"forge_publish-{version}-py3-none-any.whl",
|
|
16
|
+
f"forge_publish-{version}.tar.gz",
|
|
17
|
+
}
|
|
18
|
+
expected = distributions | {"forge-publish.cdx.json"}
|
|
19
|
+
files = expected | {"SHA256SUMS"}
|
|
20
|
+
if {path.name for path in assets.iterdir()} != files:
|
|
21
|
+
raise ValueError("Release assets must contain exactly the expected four files")
|
|
22
|
+
if any(
|
|
23
|
+
(assets / name).is_symlink() or not (assets / name).is_file() for name in files
|
|
24
|
+
):
|
|
25
|
+
raise ValueError("Release assets must be regular files")
|
|
26
|
+
hashes = {}
|
|
27
|
+
for line in (assets / "SHA256SUMS").read_text(encoding="utf-8").splitlines():
|
|
28
|
+
match = re.fullmatch(r"([0-9a-f]{64}) (.+)", line)
|
|
29
|
+
if not match or match[2] not in expected or match[2] in hashes:
|
|
30
|
+
raise ValueError("Invalid, duplicate or unexpected checksum entry")
|
|
31
|
+
hashes[match[2]] = match[1]
|
|
32
|
+
if hashes.keys() != expected:
|
|
33
|
+
raise ValueError("Checksum manifest must cover every release artifact")
|
|
34
|
+
for name, digest in hashes.items():
|
|
35
|
+
with (assets / name).open("rb") as file:
|
|
36
|
+
if hashlib.file_digest(file, "sha256").hexdigest() != digest:
|
|
37
|
+
raise ValueError(f"Checksum mismatch: {name}")
|
|
38
|
+
# Validate everything before creating a directory accepted by the upload action.
|
|
39
|
+
output.mkdir(parents=True, exist_ok=False)
|
|
40
|
+
for name in sorted(distributions):
|
|
41
|
+
shutil.copyfile(assets / name, output / name)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def main() -> None:
|
|
45
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
46
|
+
parser.add_argument("--assets", type=Path, required=True)
|
|
47
|
+
parser.add_argument("--output", type=Path, required=True)
|
|
48
|
+
parser.add_argument("--tag", required=True)
|
|
49
|
+
arguments = parser.parse_args()
|
|
50
|
+
stage_distributions(arguments.assets, arguments.output, arguments.tag)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
main()
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""Generate a validated release SBOM from an installed wheel's runtime closure."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import tarfile
|
|
10
|
+
import tempfile
|
|
11
|
+
import tomllib
|
|
12
|
+
import zipfile
|
|
13
|
+
from email.parser import BytesParser
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from urllib.parse import quote, urlsplit
|
|
16
|
+
|
|
17
|
+
import tomli_w
|
|
18
|
+
from cyclonedx.schema import SchemaVersion
|
|
19
|
+
from cyclonedx.validation.json import JsonStrictValidator
|
|
20
|
+
from packaging.requirements import Requirement
|
|
21
|
+
from packaging.utils import canonicalize_name
|
|
22
|
+
|
|
23
|
+
ROOT = Path(__file__).resolve().parents[2]
|
|
24
|
+
FILENAME = "forge-publish.cdx.json"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def distribution_metadata(path: Path):
|
|
28
|
+
if path.suffix == ".whl":
|
|
29
|
+
with zipfile.ZipFile(path) as archive:
|
|
30
|
+
names = [n for n in archive.namelist() if n.endswith(".dist-info/METADATA")]
|
|
31
|
+
if len(names) != 1:
|
|
32
|
+
raise ValueError("Expected one wheel metadata file")
|
|
33
|
+
data = archive.read(names[0])
|
|
34
|
+
else:
|
|
35
|
+
with tarfile.open(path) as archive:
|
|
36
|
+
members = [
|
|
37
|
+
m
|
|
38
|
+
for m in archive
|
|
39
|
+
if m.name.count("/") == 1 and m.name.endswith("/PKG-INFO")
|
|
40
|
+
]
|
|
41
|
+
if len(members) != 1:
|
|
42
|
+
raise ValueError("Expected one source distribution metadata file")
|
|
43
|
+
stream = archive.extractfile(members[0])
|
|
44
|
+
if stream is None:
|
|
45
|
+
raise ValueError("Source distribution metadata is not a file")
|
|
46
|
+
data = stream.read()
|
|
47
|
+
return BytesParser().parsebytes(data)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def runtime_closure(bom: dict) -> None:
|
|
51
|
+
"""Keep only dependencies reachable from the application, excluding bootstrap tools."""
|
|
52
|
+
root_ref = bom["metadata"]["component"]["bom-ref"]
|
|
53
|
+
graph = {d["ref"]: d for d in bom["dependencies"]}
|
|
54
|
+
components = {c["bom-ref"]: c for c in bom["components"]}
|
|
55
|
+
pending, reachable = [root_ref], set()
|
|
56
|
+
while pending:
|
|
57
|
+
ref = pending.pop()
|
|
58
|
+
if ref in reachable:
|
|
59
|
+
continue
|
|
60
|
+
if ref not in graph or (ref != root_ref and ref not in components):
|
|
61
|
+
raise ValueError("Incomplete runtime dependency graph")
|
|
62
|
+
reachable.add(ref)
|
|
63
|
+
pending.extend(graph[ref].get("dependsOn", []))
|
|
64
|
+
bom["components"] = [c for c in bom["components"] if c["bom-ref"] in reachable]
|
|
65
|
+
bom["dependencies"] = [d for d in bom["dependencies"] if d["ref"] in reachable]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def validate(bom: dict) -> str:
|
|
69
|
+
text = json.dumps(bom, indent=2, sort_keys=True, ensure_ascii=False) + "\n"
|
|
70
|
+
if JsonStrictValidator(SchemaVersion.V1_6).validate_str(text) is not None:
|
|
71
|
+
raise ValueError("SBOM does not conform to the CycloneDX 1.6 JSON schema")
|
|
72
|
+
|
|
73
|
+
# Metadata comes from controlled distributions, never from CI environment
|
|
74
|
+
# variables. Fail closed if a generator/tool update introduces private URLs.
|
|
75
|
+
def check(value):
|
|
76
|
+
if isinstance(value, dict):
|
|
77
|
+
for key, item in value.items():
|
|
78
|
+
if key == "url":
|
|
79
|
+
parsed = urlsplit(item)
|
|
80
|
+
if (
|
|
81
|
+
parsed.scheme not in {"https", "http"}
|
|
82
|
+
or parsed.username
|
|
83
|
+
or parsed.password
|
|
84
|
+
):
|
|
85
|
+
raise ValueError("SBOM contains a local or authenticated URL")
|
|
86
|
+
check(item)
|
|
87
|
+
elif isinstance(value, list):
|
|
88
|
+
for item in value:
|
|
89
|
+
check(item)
|
|
90
|
+
|
|
91
|
+
check(bom)
|
|
92
|
+
return text
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def generate(runtime_python: Path, dist: Path, project_file: Path) -> Path:
|
|
96
|
+
wheels, sources = sorted(dist.glob("*.whl")), sorted(dist.glob("*.tar.gz"))
|
|
97
|
+
if len(wheels) != 1 or len(sources) != 1:
|
|
98
|
+
raise ValueError("Expected exactly one wheel and one source distribution")
|
|
99
|
+
metadata = distribution_metadata(wheels[0])
|
|
100
|
+
source_metadata = distribution_metadata(sources[0])
|
|
101
|
+
project = tomllib.loads(project_file.read_text(encoding="utf-8"))["project"]
|
|
102
|
+
name, version = metadata["Name"], metadata["Version"]
|
|
103
|
+
if (name, version) != (project["name"], project["version"]) or (name, version) != (
|
|
104
|
+
source_metadata["Name"],
|
|
105
|
+
source_metadata["Version"],
|
|
106
|
+
):
|
|
107
|
+
raise ValueError("Project, wheel and source distribution versions/names differ")
|
|
108
|
+
environment = json.loads(
|
|
109
|
+
subprocess.check_output(
|
|
110
|
+
[
|
|
111
|
+
str(runtime_python),
|
|
112
|
+
"-c",
|
|
113
|
+
(
|
|
114
|
+
"import importlib.metadata as m,json,platform,sys; "
|
|
115
|
+
"d=m.distribution('forge-publish'); "
|
|
116
|
+
"print(json.dumps({'version':d.version,'requires':d.requires,"
|
|
117
|
+
"'python':platform.python_version(),'platform':sys.platform,"
|
|
118
|
+
"'isolated':sys.prefix!=sys.base_prefix and "
|
|
119
|
+
"not __import__('site').ENABLE_USER_SITE}))"
|
|
120
|
+
),
|
|
121
|
+
],
|
|
122
|
+
text=True,
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
if not environment["isolated"]:
|
|
126
|
+
raise ValueError("SBOM requires an isolated runtime virtual environment")
|
|
127
|
+
if environment["version"] != version or sorted(
|
|
128
|
+
environment["requires"] or []
|
|
129
|
+
) != sorted(metadata.get_all("Requires-Dist", [])):
|
|
130
|
+
raise ValueError(
|
|
131
|
+
"Runtime environment does not contain the release wheel metadata"
|
|
132
|
+
)
|
|
133
|
+
# Use the built wheel's metadata, after Semantic Release's version bump.
|
|
134
|
+
# Optional dev/release extras are intentionally outside this runtime profile.
|
|
135
|
+
requirements = []
|
|
136
|
+
for value in metadata.get_all("Requires-Dist", []):
|
|
137
|
+
requirement = Requirement(value)
|
|
138
|
+
if requirement.marker is None or "extra" not in str(requirement.marker):
|
|
139
|
+
requirements.append(value)
|
|
140
|
+
root_project = {
|
|
141
|
+
"project": {
|
|
142
|
+
"name": name,
|
|
143
|
+
"version": version,
|
|
144
|
+
"description": metadata["Summary"],
|
|
145
|
+
"license": metadata["License-Expression"],
|
|
146
|
+
"dependencies": requirements,
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
150
|
+
manifest, output = (
|
|
151
|
+
Path(directory) / "pyproject.toml",
|
|
152
|
+
Path(directory) / "sbom.json",
|
|
153
|
+
)
|
|
154
|
+
manifest.write_text(tomli_w.dumps(root_project), encoding="utf-8")
|
|
155
|
+
env = os.environ.copy()
|
|
156
|
+
env.pop("SOURCE_DATE_EPOCH", None)
|
|
157
|
+
subprocess.run(
|
|
158
|
+
[
|
|
159
|
+
sys.executable,
|
|
160
|
+
"-m",
|
|
161
|
+
"cyclonedx_py",
|
|
162
|
+
"environment",
|
|
163
|
+
str(runtime_python),
|
|
164
|
+
"--pyproject",
|
|
165
|
+
str(manifest),
|
|
166
|
+
"--spec-version",
|
|
167
|
+
"1.6",
|
|
168
|
+
"--output-format",
|
|
169
|
+
"JSON",
|
|
170
|
+
"--output-reproducible",
|
|
171
|
+
"--validate",
|
|
172
|
+
"--output-file",
|
|
173
|
+
str(output),
|
|
174
|
+
],
|
|
175
|
+
check=True,
|
|
176
|
+
env=env,
|
|
177
|
+
)
|
|
178
|
+
bom = json.loads(output.read_text(encoding="utf-8"))
|
|
179
|
+
runtime_closure(bom)
|
|
180
|
+
root = bom["metadata"]["component"]
|
|
181
|
+
if (root["name"], root["version"]) != (name, version):
|
|
182
|
+
raise ValueError("Generated SBOM has the wrong release identity")
|
|
183
|
+
root["purl"] = f"pkg:pypi/{canonicalize_name(name)}@{version}"
|
|
184
|
+
root["externalReferences"] = [
|
|
185
|
+
{
|
|
186
|
+
"type": "distribution",
|
|
187
|
+
"url": f"https://github.com/Oxelio/forge-publish/releases/download/v{version}/{quote(path.name)}",
|
|
188
|
+
"hashes": [
|
|
189
|
+
{
|
|
190
|
+
"alg": "SHA-256",
|
|
191
|
+
"content": hashlib.sha256(path.read_bytes()).hexdigest(),
|
|
192
|
+
}
|
|
193
|
+
],
|
|
194
|
+
}
|
|
195
|
+
for path in [*wheels, *sources]
|
|
196
|
+
]
|
|
197
|
+
bom["metadata"].setdefault("properties", []).extend(
|
|
198
|
+
[
|
|
199
|
+
{
|
|
200
|
+
"name": "forge-publish:runtime-profile",
|
|
201
|
+
"value": f"{environment['platform']}; Python {environment['python']}; base runtime extras only",
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
"name": "forge-publish:dependency-policy",
|
|
205
|
+
"value": "Resolved with requirements/tooling.txt; dependencies are not vendored; other platforms, Python versions and user resolutions may differ.",
|
|
206
|
+
},
|
|
207
|
+
]
|
|
208
|
+
)
|
|
209
|
+
result = validate(bom)
|
|
210
|
+
# Do not publish local paths from package metadata or future tool behavior.
|
|
211
|
+
for path in (ROOT, runtime_python.absolute().parent.parent, Path.home()):
|
|
212
|
+
if str(path).replace("\\", "/") in result.replace("\\\\", "/"):
|
|
213
|
+
raise ValueError("SBOM contains a local absolute path")
|
|
214
|
+
target = dist / FILENAME
|
|
215
|
+
target.write_text(result, encoding="utf-8", newline="\n")
|
|
216
|
+
return target
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def main() -> int:
|
|
220
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
221
|
+
parser.add_argument(
|
|
222
|
+
"--python",
|
|
223
|
+
required=True,
|
|
224
|
+
type=Path,
|
|
225
|
+
help="Clean runtime wheel environment's Python",
|
|
226
|
+
)
|
|
227
|
+
parser.add_argument("--dist", type=Path, default=ROOT / "dist")
|
|
228
|
+
args = parser.parse_args()
|
|
229
|
+
try:
|
|
230
|
+
target = generate(
|
|
231
|
+
# Resolving Linux venv symlinks selects the global interpreter and
|
|
232
|
+
# inventories tooling instead of the intended runtime environment.
|
|
233
|
+
args.python.absolute(),
|
|
234
|
+
args.dist.resolve(),
|
|
235
|
+
ROOT / "pyproject.toml",
|
|
236
|
+
)
|
|
237
|
+
except (OSError, ValueError, KeyError, subprocess.CalledProcessError) as error:
|
|
238
|
+
print(f"SBOM generation failed: {error}", file=sys.stderr)
|
|
239
|
+
return 1
|
|
240
|
+
print(f"Validated CycloneDX 1.6 SBOM: {target.name}")
|
|
241
|
+
return 0
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
if __name__ == "__main__":
|
|
245
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Regenerate universal pip constraints and align isolated pre-commit hooks."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import re
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import tempfile
|
|
8
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
ROOT = Path(__file__).resolve().parents[2]
|
|
12
|
+
HEADER = (
|
|
13
|
+
"# Generated by python .github/scripts/tooling-constraints.py; do not edit pins.\n"
|
|
14
|
+
"# Sources: pyproject.toml [dev,release] and requirements/tooling.in.\n"
|
|
15
|
+
"# Targets: Python 3.11-3.14, Linux and Windows (pyproject.toml [tool.uv]).\n"
|
|
16
|
+
"# Build isolation remains separately constrained by requirements/build.txt.\n\n"
|
|
17
|
+
)
|
|
18
|
+
HOOKS = {
|
|
19
|
+
"https://github.com/astral-sh/ruff-pre-commit": "ruff",
|
|
20
|
+
"https://github.com/commitizen-tools/commitizen": "commitizen",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def align_hooks(config: str, constraints: str) -> str:
|
|
25
|
+
for repository, package in HOOKS.items():
|
|
26
|
+
versions = set(
|
|
27
|
+
re.findall(rf"^{package}==([^;\s]+)", constraints, flags=re.MULTILINE)
|
|
28
|
+
)
|
|
29
|
+
if len(versions) != 1:
|
|
30
|
+
raise ValueError(f"Expected one universal version for hook {package}")
|
|
31
|
+
pattern = rf"(repo: {re.escape(repository)}\s+rev: )\S+"
|
|
32
|
+
config, count = re.subn(pattern, rf"\g<1>v{versions.pop()}", config)
|
|
33
|
+
if count != 1:
|
|
34
|
+
raise ValueError(f"Expected one configured hook repository for {package}")
|
|
35
|
+
return config
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def generate(root: Path, *, check: bool, upgrades: list[str]) -> bool:
|
|
39
|
+
bootstrap = (root / "requirements/generator.txt").read_text(encoding="utf-8")
|
|
40
|
+
expected = re.search(r"^uv==([^\s]+)$", bootstrap, flags=re.MULTILINE)
|
|
41
|
+
if expected is None or version("uv") != expected.group(1):
|
|
42
|
+
raise ValueError(
|
|
43
|
+
"Install the pinned requirements/generator.txt before generating"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
target = root / "requirements/tooling.txt"
|
|
47
|
+
hooks = root / ".pre-commit-config.yaml"
|
|
48
|
+
original = target.read_text(encoding="utf-8")
|
|
49
|
+
config = hooks.read_text(encoding="utf-8")
|
|
50
|
+
# Seed uv's output preferences with the reviewed pins, without making them
|
|
51
|
+
# hard constraints: the resolver can update a coupled dependency if needed.
|
|
52
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
53
|
+
output = Path(directory) / "tooling.txt"
|
|
54
|
+
output.write_text(original, encoding="utf-8")
|
|
55
|
+
command = [
|
|
56
|
+
sys.executable,
|
|
57
|
+
"-m",
|
|
58
|
+
"uv",
|
|
59
|
+
"pip",
|
|
60
|
+
"compile",
|
|
61
|
+
"pyproject.toml",
|
|
62
|
+
"requirements/tooling.in",
|
|
63
|
+
"--extra",
|
|
64
|
+
"dev",
|
|
65
|
+
"--extra",
|
|
66
|
+
"release",
|
|
67
|
+
"--universal",
|
|
68
|
+
"--python-version",
|
|
69
|
+
"3.11",
|
|
70
|
+
"--python",
|
|
71
|
+
sys.executable,
|
|
72
|
+
"--no-python-downloads",
|
|
73
|
+
"--no-sources",
|
|
74
|
+
"--no-header",
|
|
75
|
+
"--no-annotate",
|
|
76
|
+
"--output-file",
|
|
77
|
+
str(output),
|
|
78
|
+
]
|
|
79
|
+
for package in upgrades:
|
|
80
|
+
command.extend(["--upgrade-package", package])
|
|
81
|
+
subprocess.run(command, cwd=root, check=True, stdout=subprocess.DEVNULL)
|
|
82
|
+
generated = HEADER + output.read_text(encoding="utf-8")
|
|
83
|
+
aligned = align_hooks(config, generated)
|
|
84
|
+
if check:
|
|
85
|
+
return original == generated and config == aligned
|
|
86
|
+
target.write_text(generated, encoding="utf-8", newline="\n")
|
|
87
|
+
hooks.write_text(aligned, encoding="utf-8", newline="\n")
|
|
88
|
+
return True
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def main() -> int:
|
|
92
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
93
|
+
parser.add_argument(
|
|
94
|
+
"--check", action="store_true", help="Verify without changing files"
|
|
95
|
+
)
|
|
96
|
+
parser.add_argument(
|
|
97
|
+
"--upgrade-package", action="append", default=[], metavar="PACKAGE"
|
|
98
|
+
)
|
|
99
|
+
args = parser.parse_args()
|
|
100
|
+
if args.check and args.upgrade_package:
|
|
101
|
+
parser.error("--check cannot be combined with --upgrade-package")
|
|
102
|
+
try:
|
|
103
|
+
current = generate(ROOT, check=args.check, upgrades=args.upgrade_package)
|
|
104
|
+
except (
|
|
105
|
+
PackageNotFoundError,
|
|
106
|
+
ValueError,
|
|
107
|
+
OSError,
|
|
108
|
+
subprocess.CalledProcessError,
|
|
109
|
+
) as error:
|
|
110
|
+
print(f"Constraints generation failed: {error}", file=sys.stderr)
|
|
111
|
+
return 1
|
|
112
|
+
if not current:
|
|
113
|
+
print(
|
|
114
|
+
"Constraints/hooks are stale; run python .github/scripts/tooling-constraints.py"
|
|
115
|
+
)
|
|
116
|
+
return 1
|
|
117
|
+
print("Constraints and hook versions are current.")
|
|
118
|
+
return 0
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
if __name__ == "__main__":
|
|
122
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Run the locked Pyright CLI against this Python interpreter's environment."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
ROOT = Path(__file__).resolve().parents[2]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main() -> int:
|
|
12
|
+
checker = ROOT / ".github/typecheck/node_modules/pyright/index.js"
|
|
13
|
+
node = shutil.which("node")
|
|
14
|
+
if node is None or not checker.is_file():
|
|
15
|
+
print(
|
|
16
|
+
"Install Node.js and run npm ci --prefix .github/typecheck --ignore-scripts",
|
|
17
|
+
file=sys.stderr,
|
|
18
|
+
)
|
|
19
|
+
return 1
|
|
20
|
+
return subprocess.run(
|
|
21
|
+
[
|
|
22
|
+
node,
|
|
23
|
+
str(checker),
|
|
24
|
+
"--project",
|
|
25
|
+
str(ROOT / "pyproject.toml"),
|
|
26
|
+
"--pythonpath",
|
|
27
|
+
sys.executable,
|
|
28
|
+
"--warnings",
|
|
29
|
+
],
|
|
30
|
+
cwd=ROOT,
|
|
31
|
+
check=False,
|
|
32
|
+
).returncode
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
if __name__ == "__main__":
|
|
36
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "forge-publish-typecheck",
|
|
3
|
+
"lockfileVersion": 3,
|
|
4
|
+
"requires": true,
|
|
5
|
+
"packages": {
|
|
6
|
+
"": {
|
|
7
|
+
"name": "forge-publish-typecheck",
|
|
8
|
+
"devDependencies": {
|
|
9
|
+
"pyright": "1.1.414"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"node_modules/fsevents": {
|
|
13
|
+
"version": "2.3.3",
|
|
14
|
+
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
|
15
|
+
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
|
16
|
+
"dev": true,
|
|
17
|
+
"hasInstallScript": true,
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"optional": true,
|
|
20
|
+
"os": [
|
|
21
|
+
"darwin"
|
|
22
|
+
],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"node_modules/pyright": {
|
|
28
|
+
"version": "1.1.414",
|
|
29
|
+
"resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.414.tgz",
|
|
30
|
+
"integrity": "sha512-FPZZb51jepDX4eP7TEYDeNFtmE3WgwkkEcJpvH3/QmUSsj0EAy3LXu+xB4T/FWDejtsXlCfFr/rbWFjwuwuXww==",
|
|
31
|
+
"dev": true,
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"bin": {
|
|
34
|
+
"pyright": "index.js",
|
|
35
|
+
"pyright-langserver": "langserver.index.js"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=14.0.0"
|
|
39
|
+
},
|
|
40
|
+
"optionalDependencies": {
|
|
41
|
+
"fsevents": "~2.3.3"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
3
|
+
rev: v0.16.9
|
|
4
|
+
hooks:
|
|
5
|
+
- id: ruff-check
|
|
6
|
+
args: [--fix]
|
|
7
|
+
stages: [pre-commit]
|
|
8
|
+
|
|
9
|
+
- id: ruff-format
|
|
10
|
+
stages: [pre-commit]
|
|
11
|
+
|
|
12
|
+
- repo: https://github.com/commitizen-tools/commitizen
|
|
13
|
+
rev: v4.19.0
|
|
14
|
+
hooks:
|
|
15
|
+
- id: commitizen
|
|
16
|
+
stages: [commit-msg]
|