Skip to content

Commit 5b51738

Browse files
committed
added version check template script and GHA updates by Opus
1 parent 9216b40 commit 5b51738

4 files changed

Lines changed: 165 additions & 50 deletions

File tree

.github/dependabot.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ updates:
1515
schedule:
1616
interval: "weekly"
1717
target-branch: "test"
18+
# Grouped to match the github-actions entry above and the other four repos —
19+
# this was the last ungrouped ecosystem in the ecosystem (#18).
20+
groups:
21+
pip-deps:
22+
patterns:
23+
- "*"
1824
ignore:
1925
- dependency-name: "torch"
2026
- dependency-name: "torchvision"

.github/workflows/docker.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,7 @@ jobs:
2121
uses: ufal/atrium-project/.github/workflows/docker-tool.reusable.yml@test
2222
with:
2323
image-name: ${{ github.repository }}
24-
secrets: inherit
24+
secrets:
25+
# Explicit instead of `secrets: inherit` — the reusable declares and
26+
# needs only this one (#18). inherit handed it every repo secret.
27+
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

.github/workflows/release.yml

Lines changed: 15 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -13,65 +13,31 @@ jobs:
1313
# A mistagged push (tag != CITATION.cff != para_config.txt [tool] version)
1414
# fails HERE and blocks the create-release job below, so nothing gets
1515
# published. Same check as ufal/atrium-project security.reusable.yml's
16-
# version-check, but run INLINE inside release.yml on purpose: the guard must
17-
# not depend on a cross-repo `uses: …@test` resolving at tag-push time.
16+
# version-check — both now run the SAME vendored check_version.py, so the two
17+
# cannot drift apart the way the old inline copies did. It stays a VENDORED FILE
18+
# rather than a `uses:` on purpose: the guard must not depend on a cross-repo
19+
# `uses: …@test` resolving at tag-push time.
1820
version-guard:
1921
name: Verify tag == CITATION.cff == para_config.txt
2022
runs-on: ubuntu-latest
2123
timeout-minutes: 25
2224
steps:
2325
- uses: actions/checkout@v7
2426
- name: Block release on version/tag mismatch
27+
# Was a ~45-line inline python heredoc, duplicated verbatim across all
28+
# five release.yml files and again (in a drifted variant) in the hub's
29+
# security.reusable.yml. Now one vendored script, held byte-identical to
30+
# docs/templates/shared/check_version.py by para-drift.reusable.yml (#18).
31+
# Kept as a vendored file, not a reusable workflow: this gate must not
32+
# depend on a cross-repo `uses:` resolving at tag-push time.
2533
env:
26-
CITATION_PATH: CITATION.cff
27-
# translator uses the default root para_config.txt
28-
PARA_PATH: para_config.txt
2934
TAG_REF: ${{ github.ref_name }}
3035
run: |
31-
python - <<'PY'
32-
import configparser, os, re, sys
33-
34-
def citation_version(path):
35-
with open(path, encoding="utf-8") as fh:
36-
for line in fh:
37-
m = re.match(r'version:\s*["\']?v?([^"\'\s]+)', line)
38-
if m:
39-
return m.group(1)
40-
return None
41-
42-
def paraconfig_version(path):
43-
cp = configparser.ConfigParser()
44-
cp.read(path, encoding="utf-8")
45-
v = cp.get("tool", "version", fallback=None)
46-
return v[1:] if v and v.lower().startswith("v") else v
47-
48-
cit = citation_version(os.environ["CITATION_PATH"])
49-
para = paraconfig_version(os.environ["PARA_PATH"])
50-
tag = os.environ.get("TAG_REF", "")
51-
tag = tag[1:] if tag.lower().startswith("v") else tag
52-
print(f"tag={tag!r} CITATION.cff={cit!r} para_config={para!r}")
53-
54-
errors = []
55-
if not tag:
56-
errors.append("no tag ref on this release event")
57-
if not cit:
58-
errors.append(f"could not parse version from {os.environ['CITATION_PATH']}")
59-
if not para:
60-
errors.append(f"could not parse [tool] version from {os.environ['PARA_PATH']}")
61-
if cit and para and cit != para:
62-
errors.append(f"version mismatch: CITATION.cff={cit} != para_config={para}")
63-
if tag and cit and tag != cit:
64-
errors.append(f"tag v{tag} != CITATION.cff {cit}")
65-
if tag and para and tag != para:
66-
errors.append(f"tag v{tag} != para_config {para}")
67-
68-
for e in errors:
69-
print(f"::error::{e}")
70-
if errors:
71-
print("::error::Release blocked — make the pushed tag, CITATION.cff, and "
72-
"para_config.txt [tool] version identical, then re-tag.")
73-
sys.exit(1 if errors else 0)
74-
PY
36+
python check_version.py \
37+
--citation CITATION.cff \
38+
--para-config para_config.txt \
39+
--tag "$TAG_REF" \
40+
--require-tag
7541
7642
create-release:
7743
needs: version-guard

check_version.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
#!/usr/bin/env python3
2+
"""Verify that a tool repo's declared version agrees everywhere.
3+
4+
Canonical source: ``ufal/atrium-project`` → ``docs/templates/shared/check_version.py``.
5+
Vendored byte-identically into every tool repo root and held there by
6+
``para-drift.reusable.yml`` — edit the hub copy, never the vendored one.
7+
8+
Why this file exists
9+
--------------------
10+
The same ~45-line ``python - <<'PY'`` heredoc was pasted into five ``release.yml``
11+
files and again, in a subtly different three-way variant, into
12+
``security.reusable.yml`` — roughly 360 lines of copy-paste that had already
13+
drifted apart (issue #18, 2026-07-29 audit).
14+
15+
It stays a *vendored script* rather than a reusable workflow on purpose: the
16+
release gate must not depend on a cross-repo ``uses: …@ref`` resolving at
17+
tag-push time. That ordering gap is what let ``v1.0.0.-beta`` and ``v1.16.2``
18+
publish unchecked. A file in the repo is always there.
19+
20+
Checks
21+
------
22+
* ``CITATION.cff`` top-level ``version:`` parses.
23+
* ``para_config.txt`` ``[tool] version`` parses.
24+
* The two agree.
25+
* If a tag is supplied, it agrees with both.
26+
27+
A leading ``v`` is tolerated and stripped everywhere, so ``v1.2.3`` in
28+
``para_config.txt`` matches ``1.2.3`` in ``CITATION.cff``.
29+
30+
Usage
31+
-----
32+
# release.yml — a tag is mandatory
33+
python check_version.py --citation CITATION.cff \
34+
--para-config setup/para_config.txt \
35+
--tag "${GITHUB_REF_NAME}" --require-tag
36+
37+
# security.reusable.yml — consistency check, tag optional
38+
python check_version.py --citation CITATION.cff --para-config para_config.txt
39+
40+
Exit status is 0 when everything agrees, 1 otherwise. Failures are emitted as
41+
``::error::`` workflow commands so they surface on the run's summary.
42+
"""
43+
44+
from __future__ import annotations
45+
46+
import argparse
47+
import configparser
48+
import re
49+
import sys
50+
51+
52+
def _strip_v(value: str | None) -> str | None:
53+
"""Drop a single leading ``v``/``V`` so v1.2.3 and 1.2.3 compare equal."""
54+
if value and value[:1] in ("v", "V"):
55+
return value[1:]
56+
return value
57+
58+
59+
def citation_version(path: str) -> str | None:
60+
"""First column-0 ``version:`` in a CITATION.cff.
61+
62+
Anchored at column 0 deliberately: CFF nests ``version:`` under
63+
``references:``/``preferred-citation:`` for cited works, and those are not
64+
this tool's version.
65+
"""
66+
with open(path, encoding="utf-8") as fh:
67+
for line in fh:
68+
match = re.match(r'version:\s*["\']?v?([^"\'\s]+)', line)
69+
if match:
70+
return match.group(1)
71+
return None
72+
73+
74+
def paraconfig_version(path: str) -> str | None:
75+
"""``[tool] version`` from a para_config.txt (INI)."""
76+
parser = configparser.ConfigParser()
77+
parser.read(path, encoding="utf-8")
78+
return _strip_v(parser.get("tool", "version", fallback=None))
79+
80+
81+
def collect_errors(citation_path: str, para_path: str, tag: str, require_tag: bool) -> list[str]:
82+
"""Return every disagreement found. Empty list means consistent."""
83+
errors: list[str] = []
84+
85+
try:
86+
cit = citation_version(citation_path)
87+
except OSError as exc:
88+
return [f"could not read {citation_path}: {exc}"]
89+
try:
90+
para = paraconfig_version(para_path)
91+
except (OSError, configparser.Error) as exc:
92+
return [f"could not read {para_path}: {exc}"]
93+
94+
tag = _strip_v(tag.strip()) or ""
95+
print(f"CITATION.cff={cit!r} para_config={para!r} tag={tag!r}")
96+
97+
if not cit:
98+
errors.append(f"could not parse version from {citation_path}")
99+
if not para:
100+
errors.append(f"could not parse [tool] version from {para_path}")
101+
if require_tag and not tag:
102+
errors.append("no tag ref on this event, but --require-tag was given")
103+
104+
if cit and para and cit != para:
105+
errors.append(f"version mismatch: {citation_path}={cit} != {para_path}={para}")
106+
if tag and cit and tag != cit:
107+
errors.append(f"tag v{tag} != {citation_path} {cit}")
108+
if tag and para and tag != para:
109+
errors.append(f"tag v{tag} != {para_path} {para}")
110+
111+
return errors
112+
113+
114+
def main(argv: list[str] | None = None) -> int:
115+
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
116+
ap.add_argument("--citation", default="CITATION.cff", help="path to CITATION.cff")
117+
ap.add_argument(
118+
"--para-config",
119+
default="para_config.txt",
120+
help="path to para_config.txt; repos with a setup/ layout pass setup/para_config.txt",
121+
)
122+
ap.add_argument("--tag", default="", help="tag being released, e.g. v1.2.3; empty to skip tag comparison")
123+
ap.add_argument("--require-tag", action="store_true", help="fail when --tag is empty (release gate)")
124+
args = ap.parse_args(argv)
125+
126+
errors = collect_errors(args.citation, args.para_config, args.tag, args.require_tag)
127+
for err in errors:
128+
print(f"::error::{err}")
129+
if errors:
130+
print(
131+
"::error::Version check failed — make the tag, CITATION.cff and "
132+
"para_config.txt [tool] version identical, then re-tag."
133+
)
134+
return 1
135+
print("OK — versions agree")
136+
return 0
137+
138+
139+
if __name__ == "__main__":
140+
sys.exit(main())

0 commit comments

Comments
 (0)