diff --git a/build.py b/build.py index 9d51845..d20a140 100644 --- a/build.py +++ b/build.py @@ -31,23 +31,50 @@ def read(path): return f.read() -def resolve_version(cwd=BASE): - """Return git-tag version string for the repo at *cwd*. - - Order of preference: - 1. ``git describe --tags --always`` (tag, or short SHA if no tag). - 2. ``unknown`` if git is unavailable or *cwd* is not a repo. - """ +def _git(args, cwd): try: result = subprocess.run( - ['git', 'describe', '--tags', '--always'], - cwd=cwd, capture_output=True, text=True, + ['git', *args], cwd=cwd, capture_output=True, text=True, ) except (FileNotFoundError, OSError): - return 'unknown' + return None if result.returncode != 0: - return 'unknown' - return result.stdout.strip() or 'unknown' + return None + return result.stdout.strip() + + +def _tag_at_head_via_ls_remote(cwd): + head = _git(['rev-parse', 'HEAD'], cwd) + refs = _git(['ls-remote', '--tags', 'origin'], cwd) + if not head or not refs: + return None + for line in refs.splitlines(): + parts = line.split() + if len(parts) >= 2 and parts[0] == head: + name = parts[1].replace('refs/tags/', '').replace('^{}', '') + if name: + return name + return None + + +def resolve_version(cwd=BASE): + """Return git-tag version string for the repo at *cwd*. + + Order of preference: + 1. Local exact-match tag at HEAD (``git describe --exact-match --tags``). + 2. Remote tag at HEAD via ``git ls-remote`` — fallback for shallow CI + clones where local tag refs aren't connected to HEAD. + 3. ``git describe --tags --always`` (describe-with-distance or short SHA). + 4. ``unknown`` if git is unavailable or *cwd* is not a repo. + """ + tag = _git(['describe', '--exact-match', '--tags', 'HEAD'], cwd) + if tag: + return tag + tag = _tag_at_head_via_ls_remote(cwd) + if tag: + return tag + described = _git(['describe', '--tags', '--always'], cwd) + return described or 'unknown' def build(): diff --git a/test/build/test_resolve_version.py b/test/build/test_resolve_version.py index eb51801..192a1a5 100644 --- a/test/build/test_resolve_version.py +++ b/test/build/test_resolve_version.py @@ -75,6 +75,30 @@ def test_not_a_repo(self): with tempfile.TemporaryDirectory() as d: self.assertEqual(build.resolve_version(d), 'unknown') + def test_ls_remote_fallback_when_local_tag_refs_missing(self): + # Vercel-like failure mode: HEAD is at a tagged commit on origin, + # but local tag refs aren't populated (shallow clone quirks). The + # ls-remote fallback should still surface the tag. + with tempfile.TemporaryDirectory() as remote, \ + tempfile.TemporaryDirectory() as clone: + init_repo(remote) + git(remote, 'tag', 'v9.9.9') + git(remote, 'config', 'receive.denyCurrentBranch', 'ignore') + git(clone, 'clone', '--depth=1', remote, '.') + # Strip local tag refs to simulate missing-tag-locally state + for entry in os.listdir(os.path.join(clone, '.git', 'refs', 'tags')): + os.remove(os.path.join(clone, '.git', 'refs', 'tags', entry)) + packed = os.path.join(clone, '.git', 'packed-refs') + if os.path.exists(packed): + with open(packed) as f: + lines = [l for l in f if 'refs/tags/' not in l] + with open(packed, 'w') as f: + f.writelines(lines) + self.assertIsNone( + build._git(['describe', '--exact-match', '--tags', 'HEAD'], clone) + ) + self.assertEqual(build.resolve_version(clone), 'v9.9.9') + if __name__ == '__main__': unittest.main()