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.
Files changed (48) hide show
  1. forge_publish-2.3.0/.github/scripts/pypi-distributions.py +54 -0
  2. forge_publish-2.3.0/.github/scripts/release-checksums.sh +7 -0
  3. forge_publish-2.3.0/.github/scripts/release-sbom.py +245 -0
  4. forge_publish-2.3.0/.github/scripts/tooling-constraints.py +122 -0
  5. forge_publish-2.3.0/.github/scripts/typecheck.py +36 -0
  6. forge_publish-2.3.0/.github/typecheck/package-lock.json +45 -0
  7. forge_publish-2.3.0/.github/typecheck/package.json +7 -0
  8. forge_publish-2.3.0/.pre-commit-config.yaml +16 -0
  9. forge_publish-2.3.0/LICENSE +202 -0
  10. forge_publish-2.3.0/MANIFEST.in +11 -0
  11. forge_publish-2.3.0/PKG-INFO +188 -0
  12. forge_publish-2.3.0/README.md +158 -0
  13. forge_publish-2.3.0/pyproject.toml +87 -0
  14. forge_publish-2.3.0/requirements/build.txt +3 -0
  15. forge_publish-2.3.0/requirements/generator.txt +2 -0
  16. forge_publish-2.3.0/requirements/tooling.in +4 -0
  17. forge_publish-2.3.0/requirements/tooling.txt +110 -0
  18. forge_publish-2.3.0/setup.cfg +4 -0
  19. forge_publish-2.3.0/src/forge_publish/__init__.py +3 -0
  20. forge_publish-2.3.0/src/forge_publish/__main__.py +4 -0
  21. forge_publish-2.3.0/src/forge_publish/cli.py +211 -0
  22. forge_publish-2.3.0/src/forge_publish/client.py +167 -0
  23. forge_publish-2.3.0/src/forge_publish/config.py +229 -0
  24. forge_publish-2.3.0/src/forge_publish/errors.py +10 -0
  25. forge_publish-2.3.0/src/forge_publish/publishers/__init__.py +0 -0
  26. forge_publish-2.3.0/src/forge_publish/publishers/deb.py +460 -0
  27. forge_publish-2.3.0/src/forge_publish/publishers/generic.py +84 -0
  28. forge_publish-2.3.0/src/forge_publish/publishers/npm.py +281 -0
  29. forge_publish-2.3.0/src/forge_publish.egg-info/PKG-INFO +188 -0
  30. forge_publish-2.3.0/src/forge_publish.egg-info/SOURCES.txt +46 -0
  31. forge_publish-2.3.0/src/forge_publish.egg-info/dependency_links.txt +1 -0
  32. forge_publish-2.3.0/src/forge_publish.egg-info/entry_points.txt +2 -0
  33. forge_publish-2.3.0/src/forge_publish.egg-info/requires.txt +18 -0
  34. forge_publish-2.3.0/src/forge_publish.egg-info/top_level.txt +1 -0
  35. forge_publish-2.3.0/tests/conftest.py +52 -0
  36. forge_publish-2.3.0/tests/fixtures/npm_lifecycle/lifecycle.cjs +14 -0
  37. forge_publish-2.3.0/tests/fixtures/npm_lifecycle/package.json +17 -0
  38. forge_publish-2.3.0/tests/test_cli.py +462 -0
  39. forge_publish-2.3.0/tests/test_client.py +362 -0
  40. forge_publish-2.3.0/tests/test_config.py +484 -0
  41. forge_publish-2.3.0/tests/test_deb.py +1000 -0
  42. forge_publish-2.3.0/tests/test_deb_fuzz.py +253 -0
  43. forge_publish-2.3.0/tests/test_generic.py +114 -0
  44. forge_publish-2.3.0/tests/test_npm.py +637 -0
  45. forge_publish-2.3.0/tests/test_pypi_distributions.py +102 -0
  46. forge_publish-2.3.0/tests/test_release_checksums.py +87 -0
  47. forge_publish-2.3.0/tests/test_release_sbom.py +207 -0
  48. 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,7 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Missing a distribution or its SBOM must fail before attestation/publication.
5
+ cd "${1:-dist}"
6
+ sha256sum -- *.whl *.tar.gz forge-publish.cdx.json > SHA256SUMS
7
+ sha256sum --check SHA256SUMS
@@ -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,7 @@
1
+ {
2
+ "name": "forge-publish-typecheck",
3
+ "private": true,
4
+ "devDependencies": {
5
+ "pyright": "1.1.414"
6
+ }
7
+ }
@@ -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]