tagpoint 0.1.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.
tagpoint-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 hao li
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.
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: tagpoint
3
+ Version: 0.1.0
4
+ Summary: Verify a PyPI release actually matches its GitHub tag — name AND content
5
+ Author-email: hao li <haoli5933@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/hahahahahahahahah6/tagpoint
8
+ Project-URL: Repository, https://github.com/hahahahahahahahah6/tagpoint
9
+ Keywords: pypi,github,supply-chain,release,tag,integrity
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Software Development :: Build Tools
16
+ Classifier: Topic :: Security
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Dynamic: license-file
21
+
22
+ # tagpoint
23
+
24
+ Verify that a PyPI release **actually matches** its GitHub tag — the name *and* the content.
25
+
26
+ ```bash
27
+ pip install tagpoint
28
+ tagpoint check requests --repo psf/requests
29
+ ```
30
+
31
+ Output:
32
+
33
+ ```
34
+ tagpoint report: requests==2.32.3 repo=psf/requests
35
+ tag: v2.32.3 commit: abc123def456
36
+
37
+ [PASS] tag-exists: tag v2.32.3 -> abc123def456
38
+ [PASS] sdist-version: PKG-INFO version 2.32.3 matches PyPI
39
+ [PASS] file-list: 128 files match the tag tree
40
+ [PASS] tag-before-build: tag commit 2024-05-29T... <= upload 2024-05-29T...
41
+
42
+ verdict: CONSISTENT
43
+ ```
44
+
45
+ Exit code `0` = consistent, `1` = inconsistent, `2` = check error (network, missing data).
46
+
47
+ ## Install
48
+
49
+ ```bash
50
+ pip install tagpoint
51
+ ```
52
+
53
+ tagpoint is stdlib-only — no third-party dependencies.
54
+
55
+ ## Usage
56
+
57
+ ```
58
+ tagpoint check <pypi-package> --repo owner/name [--shallow] [--token TOKEN]
59
+ ```
60
+
61
+ - `tagpoint check flask --repo pallets/flask` — full check (default)
62
+ - `--shallow` — only verify the tag exists and the sdist version matches
63
+ - `--token` — GitHub token (or `GITHUB_TOKEN` env var) to raise API rate limits
64
+
65
+ Four checks run by default:
66
+
67
+ 1. **tag-exists** — a tag named `v{version}` or `{version}` exists and resolves (annotated tags are peeled to the commit).
68
+ 2. **sdist-version** — the sdist's `PKG-INFO` version equals the PyPI version.
69
+ 3. **file-list** — the sdist file list matches the tag's git tree (catches files added/removed after the build).
70
+ 4. **tag-before-build** — the tag commit date is not newer than the release upload time (a tag moved *after* the build is drift).
71
+
72
+ ## Differentiation
73
+
74
+ There are tools that check whether a release tag **exists** (e.g. my
75
+ [tagtruth](https://github.com/hahahahahahahahah6/tagtruth) project checks tag
76
+ presence as part of release hygiene). tagpoint goes one step further: it checks
77
+ whether the tag **points at the code the release was actually built from**.
78
+
79
+ The failure mode it catches is real: a tag named `v1.2.3` exists, CI is green,
80
+ everything looks fine — but the tag was force-moved after the sdist was built,
81
+ or the release was cut from a dirty working tree. The name matches; the
82
+ content doesn't. tagpoint compares the sdist contents against the tag's git
83
+ tree and flags the drift. Think of it as a "name matches reality" check for
84
+ your supply chain.
85
+
86
+ ## License
87
+
88
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,67 @@
1
+ # tagpoint
2
+
3
+ Verify that a PyPI release **actually matches** its GitHub tag — the name *and* the content.
4
+
5
+ ```bash
6
+ pip install tagpoint
7
+ tagpoint check requests --repo psf/requests
8
+ ```
9
+
10
+ Output:
11
+
12
+ ```
13
+ tagpoint report: requests==2.32.3 repo=psf/requests
14
+ tag: v2.32.3 commit: abc123def456
15
+
16
+ [PASS] tag-exists: tag v2.32.3 -> abc123def456
17
+ [PASS] sdist-version: PKG-INFO version 2.32.3 matches PyPI
18
+ [PASS] file-list: 128 files match the tag tree
19
+ [PASS] tag-before-build: tag commit 2024-05-29T... <= upload 2024-05-29T...
20
+
21
+ verdict: CONSISTENT
22
+ ```
23
+
24
+ Exit code `0` = consistent, `1` = inconsistent, `2` = check error (network, missing data).
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ pip install tagpoint
30
+ ```
31
+
32
+ tagpoint is stdlib-only — no third-party dependencies.
33
+
34
+ ## Usage
35
+
36
+ ```
37
+ tagpoint check <pypi-package> --repo owner/name [--shallow] [--token TOKEN]
38
+ ```
39
+
40
+ - `tagpoint check flask --repo pallets/flask` — full check (default)
41
+ - `--shallow` — only verify the tag exists and the sdist version matches
42
+ - `--token` — GitHub token (or `GITHUB_TOKEN` env var) to raise API rate limits
43
+
44
+ Four checks run by default:
45
+
46
+ 1. **tag-exists** — a tag named `v{version}` or `{version}` exists and resolves (annotated tags are peeled to the commit).
47
+ 2. **sdist-version** — the sdist's `PKG-INFO` version equals the PyPI version.
48
+ 3. **file-list** — the sdist file list matches the tag's git tree (catches files added/removed after the build).
49
+ 4. **tag-before-build** — the tag commit date is not newer than the release upload time (a tag moved *after* the build is drift).
50
+
51
+ ## Differentiation
52
+
53
+ There are tools that check whether a release tag **exists** (e.g. my
54
+ [tagtruth](https://github.com/hahahahahahahahah6/tagtruth) project checks tag
55
+ presence as part of release hygiene). tagpoint goes one step further: it checks
56
+ whether the tag **points at the code the release was actually built from**.
57
+
58
+ The failure mode it catches is real: a tag named `v1.2.3` exists, CI is green,
59
+ everything looks fine — but the tag was force-moved after the sdist was built,
60
+ or the release was cut from a dirty working tree. The name matches; the
61
+ content doesn't. tagpoint compares the sdist contents against the tag's git
62
+ tree and flags the drift. Think of it as a "name matches reality" check for
63
+ your supply chain.
64
+
65
+ ## License
66
+
67
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "tagpoint"
7
+ version = "0.1.0"
8
+ description = "Verify a PyPI release actually matches its GitHub tag — name AND content"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.9"
12
+ authors = [{ name = "hao li", email = "haoli5933@gmail.com" }]
13
+ keywords = ["pypi", "github", "supply-chain", "release", "tag", "integrity"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Environment :: Console",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Topic :: Software Development :: Build Tools",
21
+ "Topic :: Security",
22
+ ]
23
+
24
+ [project.scripts]
25
+ tagpoint = "tagpoint.cli:main"
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/hahahahahahahahah6/tagpoint"
29
+ Repository = "https://github.com/hahahahahahahahah6/tagpoint"
30
+
31
+ [tool.setuptools.packages.find]
32
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,4 @@
1
+ """tagpoint: verify that a PyPI release matches its GitHub tag."""
2
+
3
+ __version__ = "0.1.0"
4
+ __all__ = ["__version__"]
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -0,0 +1,221 @@
1
+ """Core consistency checks between a PyPI release and its GitHub tag.
2
+
3
+ tagpoint answers one question: does the tag named after the PyPI version
4
+ actually point at the code the release was built from? It fetches the PyPI
5
+ metadata, resolves the matching GitHub tag, downloads the sdist, and compares
6
+ what can be compared without a full git checkout:
7
+
8
+ 1. the tag exists (v{version} or {version})
9
+ 2. the sdist PKG-INFO version matches the PyPI version
10
+ 3. (deep) the sdist file list matches the tag's git tree file list,
11
+ flagging files the tag gained/lost after the release was built
12
+ 4. (deep) the tag commit date is not newer than the release upload time
13
+ (a tag moved *after* the build is suspicious drift)
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import hashlib
19
+ import io
20
+ import json
21
+ import tarfile
22
+ import urllib.error
23
+ import urllib.request
24
+ import zipfile
25
+ from dataclasses import dataclass, field
26
+
27
+ PYPI_JSON_URL = "https://pypi.org/pypi/{name}/json"
28
+ GITHUB_API = "https://api.github.com"
29
+
30
+ _USER_AGENT = "tagpoint/0.1.0 (+https://github.com/hahahahahahahahah6/tagpoint)"
31
+
32
+
33
+ class CheckError(Exception):
34
+ """Raised when a check cannot be completed (network, missing data, ...)."""
35
+
36
+
37
+ @dataclass
38
+ class Finding:
39
+ ok: bool
40
+ name: str
41
+ detail: str
42
+
43
+
44
+ @dataclass
45
+ class Report:
46
+ package: str
47
+ version: str
48
+ repo: str
49
+ tag: str | None
50
+ commit: str | None
51
+ findings: list[Finding] = field(default_factory=list)
52
+
53
+ @property
54
+ def consistent(self) -> bool:
55
+ return bool(self.findings) and all(f.ok for f in self.findings)
56
+
57
+ def render(self) -> str:
58
+ lines = [
59
+ f"tagpoint report: {self.package}=={self.version} repo={self.repo}",
60
+ f"tag: {self.tag or '(none)'} commit: {(self.commit or '(none)')[:12]}",
61
+ "",
62
+ ]
63
+ for f in self.findings:
64
+ mark = "PASS" if f.ok else "FAIL"
65
+ lines.append(f"[{mark}] {f.name}: {f.detail}")
66
+ lines.append("")
67
+ verdict = "CONSISTENT" if self.consistent else "INCONSISTENT"
68
+ lines.append(f"verdict: {verdict}")
69
+ return "\n".join(lines)
70
+
71
+
72
+ def _fetch_json(url: str, token: str | None = None) -> dict:
73
+ req = urllib.request.Request(url, headers={"User-Agent": _USER_AGENT})
74
+ if token:
75
+ req.add_header("Authorization", f"Bearer {token}")
76
+ try:
77
+ with urllib.request.urlopen(req, timeout=30) as resp:
78
+ return json.loads(resp.read().decode("utf-8"))
79
+ except urllib.error.HTTPError as e:
80
+ raise CheckError(f"HTTP {e.code} fetching {url}") from e
81
+ except urllib.error.URLError as e:
82
+ raise CheckError(f"network error fetching {url}: {e.reason}") from e
83
+
84
+
85
+ def _fetch_bytes(url: str) -> bytes:
86
+ req = urllib.request.Request(url, headers={"User-Agent": _USER_AGENT})
87
+ try:
88
+ with urllib.request.urlopen(req, timeout=60) as resp:
89
+ return resp.read()
90
+ except urllib.error.HTTPError as e:
91
+ raise CheckError(f"HTTP {e.code} downloading {url}") from e
92
+ except urllib.error.URLError as e:
93
+ raise CheckError(f"network error downloading {url}: {e.reason}") from e
94
+
95
+
96
+ def _sdist_files(blob: bytes, filename: str) -> tuple[str, list[str]]:
97
+ """Return (PKG-INFO version, sorted file list) from an sdist blob."""
98
+ version = ""
99
+ names: list[str] = []
100
+ if filename.endswith(".zip"):
101
+ with zipfile.ZipFile(io.BytesIO(blob)) as zf:
102
+ for info in zf.infolist():
103
+ names.append(info.filename)
104
+ if info.filename.endswith("PKG-INFO"):
105
+ version = _pkg_info_version(zf.read(info.filename).decode("utf-8", "replace"))
106
+ else: # .tar.gz
107
+ with tarfile.open(fileobj=io.BytesIO(blob), mode="r:gz") as tf:
108
+ for member in tf.getmembers():
109
+ names.append(member.name)
110
+ if member.name.endswith("PKG-INFO") and member.isfile():
111
+ f = tf.extractfile(member)
112
+ if f:
113
+ version = _pkg_info_version(f.read().decode("utf-8", "replace"))
114
+ return version, sorted(names)
115
+
116
+
117
+ def _pkg_info_version(text: str) -> str:
118
+ for line in text.splitlines():
119
+ if line.lower().startswith("version:"):
120
+ return line.split(":", 1)[1].strip()
121
+ return ""
122
+
123
+
124
+ def _sha256_file(path: str) -> str:
125
+ h = hashlib.sha256()
126
+ with open(path, "rb") as f:
127
+ for chunk in iter(lambda: f.read(65536), b""):
128
+ h.update(chunk)
129
+ return h.hexdigest()
130
+
131
+
132
+ def _strip_top_dir(names: list[str]) -> set[str]:
133
+ """Drop the sdist's single top-level directory so paths compare to a git tree."""
134
+ out = set()
135
+ for n in names:
136
+ parts = n.strip("/").split("/")
137
+ if len(parts) > 1:
138
+ out.add("/".join(parts[1:]))
139
+ return out
140
+
141
+
142
+ def check(package: str, repo: str, *, deep: bool = True, token: str | None = None) -> Report:
143
+ """Run the full consistency check and return a Report."""
144
+ meta = _fetch_json(PYPI_JSON_URL.format(name=package), token)
145
+ version = meta["info"]["version"]
146
+ report = Report(package=package, version=version, repo=repo, tag=None, commit=None)
147
+
148
+ # 1. resolve the tag
149
+ tag_name = None
150
+ commit = None
151
+ for candidate in (f"v{version}", version):
152
+ try:
153
+ ref = _fetch_json(f"{GITHUB_API}/repos/{repo}/git/refs/tags/{candidate}", token)
154
+ except CheckError:
155
+ continue
156
+ tag_name = candidate
157
+ obj = ref["object"]
158
+ if obj["type"] == "tag": # annotated tag -> peel to commit
159
+ peeled = _fetch_json(obj["url"], token)
160
+ commit = peeled["object"]["sha"]
161
+ else:
162
+ commit = obj["sha"]
163
+ break
164
+ report.tag, report.commit = tag_name, commit
165
+ if tag_name is None:
166
+ report.findings.append(Finding(False, "tag-exists",
167
+ f"no tag v{version} or {version} on {repo}"))
168
+ return report
169
+ report.findings.append(Finding(True, "tag-exists",
170
+ f"tag {tag_name} -> {commit[:12]}"))
171
+
172
+ # 2. sdist sanity: PKG-INFO version must match PyPI version
173
+ files = meta["releases"].get(version, [])
174
+ sdists = [f for f in files if f["packagetype"] == "sdist"]
175
+ if not sdists:
176
+ report.findings.append(Finding(False, "sdist-present",
177
+ "no sdist file in this release"))
178
+ return report
179
+ sdist = sdists[0]
180
+ blob = _fetch_bytes(sdist["url"])
181
+ pkg_version, sdist_names = _sdist_files(blob, sdist["filename"])
182
+ if pkg_version == version:
183
+ report.findings.append(Finding(True, "sdist-version",
184
+ f"PKG-INFO version {pkg_version} matches PyPI"))
185
+ else:
186
+ report.findings.append(Finding(False, "sdist-version",
187
+ f"PKG-INFO says {pkg_version!r}, PyPI says {version!r}"))
188
+
189
+ if not deep:
190
+ return report
191
+
192
+ # 3. file-list comparison: sdist contents vs tag tree
193
+ tree = _fetch_json(f"{GITHUB_API}/repos/{repo}/git/trees/{commit}?recursive=1", token)
194
+ tag_paths = {t["path"] for t in tree.get("tree", []) if t["type"] == "blob"}
195
+ # PKG-INFO is generated at build time; it never lives in the git tree.
196
+ sdist_paths = _strip_top_dir(sdist_names) - {"PKG-INFO"}
197
+ only_tag = sorted(tag_paths - sdist_paths)
198
+ only_sdist = sorted(sdist_paths - tag_paths)
199
+ if not only_tag and not only_sdist:
200
+ report.findings.append(Finding(True, "file-list",
201
+ f"{len(tag_paths)} files match the tag tree"))
202
+ else:
203
+ detail = []
204
+ if only_tag:
205
+ detail.append(f"{len(only_tag)} only in tag (e.g. {', '.join(only_tag[:3])})")
206
+ if only_sdist:
207
+ detail.append(f"{len(only_sdist)} only in sdist (e.g. {', '.join(only_sdist[:3])})")
208
+ report.findings.append(Finding(False, "file-list", "; ".join(detail)))
209
+
210
+ # 4. tag commit must not be newer than the release upload
211
+ commit_meta = _fetch_json(f"{GITHUB_API}/repos/{repo}/commits/{commit}", token)
212
+ commit_date = commit_meta["commit"]["committer"]["date"]
213
+ upload_time = sdist["upload_time_iso_8601"]
214
+ if commit_date <= upload_time:
215
+ report.findings.append(Finding(True, "tag-before-build",
216
+ f"tag commit {commit_date} <= upload {upload_time}"))
217
+ else:
218
+ report.findings.append(Finding(False, "tag-before-build",
219
+ f"tag commit {commit_date} is NEWER than upload {upload_time} — tag drifted"))
220
+
221
+ return report
@@ -0,0 +1,45 @@
1
+ """Command-line interface for tagpoint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ import sys
8
+
9
+ from . import __version__
10
+ from .checker import CheckError, check
11
+
12
+
13
+ def build_parser() -> argparse.ArgumentParser:
14
+ p = argparse.ArgumentParser(
15
+ prog="tagpoint",
16
+ description="Verify a PyPI release matches its GitHub tag (name AND content).",
17
+ )
18
+ p.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
19
+ sub = p.add_subparsers(dest="command", required=True)
20
+
21
+ c = sub.add_parser("check", help="check one package release against its repo tag")
22
+ c.add_argument("package", help="PyPI package name, e.g. requests")
23
+ c.add_argument("--repo", required=True, help="GitHub repo as owner/name, e.g. psf/requests")
24
+ c.add_argument("--shallow", action="store_true",
25
+ help="skip file-list and tag-date checks (tag existence + sdist version only)")
26
+ c.add_argument("--token", default=os.environ.get("GITHUB_TOKEN"),
27
+ help="GitHub token (or set GITHUB_TOKEN) to raise API rate limits")
28
+ return p
29
+
30
+
31
+ def main(argv: list[str] | None = None) -> int:
32
+ args = build_parser().parse_args(argv)
33
+ if args.command == "check":
34
+ try:
35
+ report = check(args.package, args.repo, deep=not args.shallow, token=args.token)
36
+ except CheckError as e:
37
+ print(f"tagpoint: error: {e}", file=sys.stderr)
38
+ return 2
39
+ print(report.render())
40
+ return 0 if report.consistent else 1
41
+ return 2
42
+
43
+
44
+ if __name__ == "__main__":
45
+ raise SystemExit(main())
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: tagpoint
3
+ Version: 0.1.0
4
+ Summary: Verify a PyPI release actually matches its GitHub tag — name AND content
5
+ Author-email: hao li <haoli5933@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/hahahahahahahahah6/tagpoint
8
+ Project-URL: Repository, https://github.com/hahahahahahahahah6/tagpoint
9
+ Keywords: pypi,github,supply-chain,release,tag,integrity
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Software Development :: Build Tools
16
+ Classifier: Topic :: Security
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Dynamic: license-file
21
+
22
+ # tagpoint
23
+
24
+ Verify that a PyPI release **actually matches** its GitHub tag — the name *and* the content.
25
+
26
+ ```bash
27
+ pip install tagpoint
28
+ tagpoint check requests --repo psf/requests
29
+ ```
30
+
31
+ Output:
32
+
33
+ ```
34
+ tagpoint report: requests==2.32.3 repo=psf/requests
35
+ tag: v2.32.3 commit: abc123def456
36
+
37
+ [PASS] tag-exists: tag v2.32.3 -> abc123def456
38
+ [PASS] sdist-version: PKG-INFO version 2.32.3 matches PyPI
39
+ [PASS] file-list: 128 files match the tag tree
40
+ [PASS] tag-before-build: tag commit 2024-05-29T... <= upload 2024-05-29T...
41
+
42
+ verdict: CONSISTENT
43
+ ```
44
+
45
+ Exit code `0` = consistent, `1` = inconsistent, `2` = check error (network, missing data).
46
+
47
+ ## Install
48
+
49
+ ```bash
50
+ pip install tagpoint
51
+ ```
52
+
53
+ tagpoint is stdlib-only — no third-party dependencies.
54
+
55
+ ## Usage
56
+
57
+ ```
58
+ tagpoint check <pypi-package> --repo owner/name [--shallow] [--token TOKEN]
59
+ ```
60
+
61
+ - `tagpoint check flask --repo pallets/flask` — full check (default)
62
+ - `--shallow` — only verify the tag exists and the sdist version matches
63
+ - `--token` — GitHub token (or `GITHUB_TOKEN` env var) to raise API rate limits
64
+
65
+ Four checks run by default:
66
+
67
+ 1. **tag-exists** — a tag named `v{version}` or `{version}` exists and resolves (annotated tags are peeled to the commit).
68
+ 2. **sdist-version** — the sdist's `PKG-INFO` version equals the PyPI version.
69
+ 3. **file-list** — the sdist file list matches the tag's git tree (catches files added/removed after the build).
70
+ 4. **tag-before-build** — the tag commit date is not newer than the release upload time (a tag moved *after* the build is drift).
71
+
72
+ ## Differentiation
73
+
74
+ There are tools that check whether a release tag **exists** (e.g. my
75
+ [tagtruth](https://github.com/hahahahahahahahah6/tagtruth) project checks tag
76
+ presence as part of release hygiene). tagpoint goes one step further: it checks
77
+ whether the tag **points at the code the release was actually built from**.
78
+
79
+ The failure mode it catches is real: a tag named `v1.2.3` exists, CI is green,
80
+ everything looks fine — but the tag was force-moved after the sdist was built,
81
+ or the release was cut from a dirty working tree. The name matches; the
82
+ content doesn't. tagpoint compares the sdist contents against the tag's git
83
+ tree and flags the drift. Think of it as a "name matches reality" check for
84
+ your supply chain.
85
+
86
+ ## License
87
+
88
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,13 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/tagpoint/__init__.py
5
+ src/tagpoint/__main__.py
6
+ src/tagpoint/checker.py
7
+ src/tagpoint/cli.py
8
+ src/tagpoint.egg-info/PKG-INFO
9
+ src/tagpoint.egg-info/SOURCES.txt
10
+ src/tagpoint.egg-info/dependency_links.txt
11
+ src/tagpoint.egg-info/entry_points.txt
12
+ src/tagpoint.egg-info/top_level.txt
13
+ tests/test_checker.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ tagpoint = tagpoint.cli:main
@@ -0,0 +1 @@
1
+ tagpoint
@@ -0,0 +1,192 @@
1
+ """Smoke tests for tagpoint. All network access is mocked — no real HTTP."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import io
6
+ import json
7
+ import tarfile
8
+ import urllib.error
9
+
10
+ import pytest
11
+
12
+ from tagpoint import checker
13
+ from tagpoint.checker import CheckError, check
14
+
15
+ PKG = "fakepkg"
16
+ REPO = "owner/fakepkg"
17
+ VERSION = "1.2.3"
18
+
19
+
20
+ def make_sdist(version: str, files: tuple[str, ...] = ("fakepkg/__init__.py",)) -> bytes:
21
+ buf = io.BytesIO()
22
+ top = f"fakepkg-{version}"
23
+ with tarfile.open(fileobj=buf, mode="w:gz") as tf:
24
+ pkg_info = f"Metadata-Version: 2.1\nName: fakepkg\nVersion: {version}\n".encode()
25
+ for name in (f"{top}/PKG-INFO",) + tuple(f"{top}/{f}" for f in files):
26
+ data = pkg_info if name.endswith("PKG-INFO") else b"x = 1\n"
27
+ ti = tarfile.TarInfo(name=name)
28
+ ti.size = len(data)
29
+ tf.addfile(ti, io.BytesIO(data))
30
+ return buf.getvalue()
31
+
32
+
33
+ def pypi_meta(version=VERSION, with_sdist=True):
34
+ files = []
35
+ if with_sdist:
36
+ files.append({
37
+ "packagetype": "sdist",
38
+ "filename": f"fakepkg-{version}.tar.gz",
39
+ "url": f"https://files.pythonhosted.org/fakepkg-{version}.tar.gz",
40
+ "upload_time_iso_8601": "2026-10-01T12:00:00Z",
41
+ })
42
+ return {"info": {"version": version}, "releases": {version: files}}
43
+
44
+
45
+ class FakeResp:
46
+ def __init__(self, payload: bytes):
47
+ self._payload = payload
48
+
49
+ def read(self):
50
+ return self._payload
51
+
52
+ def __enter__(self):
53
+ return self
54
+
55
+ def __exit__(self, *a):
56
+ return False
57
+
58
+
59
+ class Router:
60
+ """Maps URL substrings to payloads for urlopen."""
61
+
62
+ def __init__(self):
63
+ self.routes: list[tuple[str, bytes]] = []
64
+
65
+ def add(self, needle: str, payload):
66
+ if isinstance(payload, (dict, list)):
67
+ payload = json.dumps(payload).encode()
68
+ self.routes.append((needle, payload))
69
+
70
+ def __call__(self, req, timeout=None):
71
+ url = req.full_url if hasattr(req, "full_url") else req
72
+ for needle, payload in self.routes:
73
+ if needle in url:
74
+ return FakeResp(payload)
75
+ raise urllib.error.HTTPError(url, 404, "not found", {}, None)
76
+
77
+
78
+ @pytest.fixture
79
+ def net(monkeypatch):
80
+ router = Router()
81
+ monkeypatch.setattr(checker.urllib.request, "urlopen", router)
82
+ return router
83
+
84
+
85
+ def wire_happy(router: Router, version=VERSION, tag_commit_date="2026-09-30T12:00:00Z",
86
+ tree_files=("fakepkg/__init__.py",), sdist_files=("fakepkg/__init__.py",),
87
+ sdist_version=None):
88
+ sdist_version = sdist_version or version
89
+ router.add("pypi.org", pypi_meta(version))
90
+ router.add("git/refs/tags/v", {"object": {"type": "commit",
91
+ "sha": "abc123" + "0" * 34,
92
+ "url": "x"}})
93
+ router.add("git/trees", {"tree": [{"path": p, "type": "blob"} for p in tree_files]})
94
+ router.add("repos/owner/fakepkg/commits",
95
+ {"commit": {"committer": {"date": tag_commit_date}}})
96
+ router.add("files.pythonhosted.org", make_sdist(sdist_version, sdist_files))
97
+
98
+
99
+ def test_happy_path_consistent(net):
100
+ wire_happy(net)
101
+ r = check(PKG, REPO)
102
+ assert r.consistent
103
+ assert r.version == VERSION
104
+ assert r.tag == f"v{VERSION}"
105
+ assert len(r.findings) == 4
106
+ out = r.render()
107
+ assert "CONSISTENT" in out
108
+ assert "[PASS]" in out and "[FAIL]" not in out
109
+
110
+
111
+ def test_missing_tag_is_inconsistent(net):
112
+ net.add("pypi.org", pypi_meta())
113
+ # no tag routes -> 404 on both v{version} and {version}
114
+ r = check(PKG, REPO)
115
+ assert not r.consistent
116
+ assert r.tag is None
117
+ assert "INCONSISTENT" in r.render()
118
+
119
+
120
+ def test_sdist_version_mismatch(net):
121
+ wire_happy(net, sdist_version="9.9.9")
122
+ r = check(PKG, REPO)
123
+ assert not r.consistent
124
+ assert any(f.name == "sdist-version" and not f.ok for f in r.findings)
125
+
126
+
127
+ def test_no_sdist_in_release(net):
128
+ net.add("pypi.org", pypi_meta(with_sdist=False))
129
+ net.add("git/refs/tags/v", {"object": {"type": "commit", "sha": "a" * 40, "url": "x"}})
130
+ r = check(PKG, REPO)
131
+ assert not r.consistent
132
+ assert any(f.name == "sdist-present" and not f.ok for f in r.findings)
133
+
134
+
135
+ def test_file_list_drift_detected(net):
136
+ # tag gained a file after the release was built
137
+ wire_happy(net, tree_files=("fakepkg/__init__.py", "fakepkg/backdoor.py"))
138
+ r = check(PKG, REPO)
139
+ assert not r.consistent
140
+ bad = [f for f in r.findings if f.name == "file-list"]
141
+ assert bad and not bad[0].ok
142
+ assert "backdoor.py" in bad[0].detail
143
+
144
+
145
+ def test_tag_newer_than_upload_detected(net):
146
+ wire_happy(net, tag_commit_date="2026-10-05T12:00:00Z") # after upload 10-01
147
+ r = check(PKG, REPO)
148
+ assert not r.consistent
149
+ bad = [f for f in r.findings if f.name == "tag-before-build"]
150
+ assert bad and not bad[0].ok
151
+
152
+
153
+ def test_annotated_tag_is_peeled(net):
154
+ net.add("pypi.org", pypi_meta())
155
+ net.add("git/refs/tags/v",
156
+ {"object": {"type": "tag", "sha": "t" * 40, "url": "https://api.github.com/peeled"}})
157
+ net.add("peeled", {"object": {"type": "commit", "sha": "c" * 40}})
158
+ net.add("git/trees", {"tree": [{"path": "fakepkg/__init__.py", "type": "blob"}]})
159
+ net.add("repos/owner/fakepkg/commits",
160
+ {"commit": {"committer": {"date": "2026-09-30T12:00:00Z"}}})
161
+ net.add("files.pythonhosted.org", make_sdist(VERSION))
162
+ r = check(PKG, REPO)
163
+ assert r.consistent
164
+ assert r.commit == "c" * 40
165
+
166
+
167
+ def test_pypi_404_raises_check_error(net):
168
+ # Router raises HTTPError 404 -> CheckError
169
+ with pytest.raises(CheckError):
170
+ check("no-such-pkg", REPO)
171
+
172
+
173
+ def test_shallow_skips_deep_checks(net):
174
+ wire_happy(net)
175
+ r = check(PKG, REPO, deep=False)
176
+ assert r.consistent
177
+ assert [f.name for f in r.findings] == ["tag-exists", "sdist-version"]
178
+
179
+
180
+ def test_cli_exit_codes(net, capsys, monkeypatch):
181
+ from tagpoint import cli
182
+ wire_happy(net)
183
+ monkeypatch.setattr(checker.urllib.request, "urlopen", net)
184
+ assert cli.main(["check", PKG, "--repo", REPO]) == 0
185
+ out, _ = capsys.readouterr()
186
+ assert "CONSISTENT" in out
187
+
188
+ # drift case -> exit 1
189
+ net2 = Router()
190
+ wire_happy(net2, tree_files=("fakepkg/__init__.py", "fakepkg/evil.py"))
191
+ monkeypatch.setattr(checker.urllib.request, "urlopen", net2)
192
+ assert cli.main(["check", PKG, "--repo", REPO]) == 1