Skip to content

Commit 7181a9b

Browse files
authored
fix(release): validate Tauri aw-server lock (#1413)
* fix(release): validate Tauri aw-server lock * fix(ci): set up Python before Tauri lock check
1 parent 5d26465 commit 7181a9b

5 files changed

Lines changed: 223 additions & 41 deletions

File tree

.github/workflows/release.yml

Lines changed: 6 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -828,50 +828,17 @@ jobs:
828828
echo "Version (no v): ${VERSION_NO_V}"
829829
echo "========================================"
830830
831-
- name: Verify aw-server-rust submodule version matches release tag
832-
if: startsWith(github.ref, 'refs/tags/v')
833-
run: |
834-
# Fail fast if the aw-server-rust submodule is pinned to an older
835-
# release line than the one we're tagging. The Windows 0.14 Tauri
836-
# bundle shipped with aw-server-rust v0.13.1 (activitywatch#1380);
837-
# catch the same class of mismatch here before spending ~30 min on a
838-
# full Tauri build.
839-
#
840-
# The bundled binary's version lives in
841-
# aw-server-rust/aw-server/Cargo.toml (the workspace-level
842-
# aw-server-rust/Cargo.toml has no [package].version field).
843-
BUNDLED_VERSION=$(grep -m1 '^version = ' aw-server-rust/aw-server/Cargo.toml | sed -E 's/^version = "(.*)".*/\1/')
844-
if [ -z "$BUNDLED_VERSION" ]; then
845-
echo "ERROR: could not read aw-server version from aw-server-rust/aw-server/Cargo.toml" >&2
846-
exit 1
847-
fi
848-
849-
AW_VERSION="${VERSION_NO_V}" # e.g. "0.14.0b3"
850-
if [ -z "$AW_VERSION" ]; then
851-
echo "ERROR: VERSION_NO_V is empty — the 'Determine and output version' step must export it to GITHUB_ENV" >&2
852-
exit 1
853-
fi
854-
AW_MAJOR_MINOR=$(echo "$AW_VERSION" | cut -d'.' -f1-2) # "0.14"
855-
AWS_MAJOR_MINOR=$(echo "$BUNDLED_VERSION" | cut -d'.' -f1-2) # "0.14"
856-
857-
echo "AW release tag: ${AW_VERSION} (major.minor: ${AW_MAJOR_MINOR})"
858-
echo "Bundled aw-server: ${BUNDLED_VERSION} (major.minor: ${AWS_MAJOR_MINOR})"
859-
860-
if [ "$AW_MAJOR_MINOR" != "$AWS_MAJOR_MINOR" ]; then
861-
echo ""
862-
echo "ERROR: aw-server-rust major.minor (${AWS_MAJOR_MINOR}) does not match"
863-
echo " AW release major.minor (${AW_MAJOR_MINOR})."
864-
echo " The aw-server-rust submodule is stale for this tag."
865-
echo " Update the submodule (cd aw-server-rust && git pull) and re-tag."
866-
exit 1
867-
fi
868-
echo "OK: aw-server version ${BUNDLED_VERSION} is consistent with AW release ${AW_VERSION}"
869-
870831
- name: Set up Python
871832
uses: actions/setup-python@v7
872833
with:
873834
python-version: ${{ matrix.python_version }}
874835

836+
- name: Verify Tauri and Qt embed the same aw-server-rust revision
837+
# Tauri embeds the Git revision in its Cargo.lock, independently of
838+
# the top-level submodule used by Qt. Check the actual build inputs;
839+
# checking only the submodule allowed v0.14.0b4 Tauri to ship 0.13.1.
840+
run: python3 scripts/check_tauri_server.py "$VERSION_NO_V"
841+
875842
- name: Set up Node
876843
if: ${{ !matrix.skip_webui }}
877844
uses: actions/setup-node@v6

scripts/check_tauri_server.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
#!/usr/bin/env python3
2+
"""Verify that Qt and Tauri bundle the same aw-server-rust revision."""
3+
4+
from __future__ import annotations
5+
6+
import argparse
7+
import re
8+
import subprocess
9+
import sys
10+
from pathlib import Path
11+
from typing import List, Tuple
12+
13+
14+
FIELD_RE = re.compile(r'^\s*([a-zA-Z0-9_-]+)\s*=\s*"([^"]+)"\s*$', re.MULTILINE)
15+
VERSION_RE = re.compile(r"^(\d+)\.(\d+)(?:\.|-|$)")
16+
REVISION_RE = re.compile(r"#([0-9a-f]{40})$")
17+
18+
19+
def major_minor(version: str) -> str:
20+
match = VERSION_RE.match(version)
21+
if not match:
22+
raise ValueError(f"invalid version: {version!r}")
23+
return ".".join(match.groups())
24+
25+
26+
def read_package_version(cargo_toml: Path) -> str:
27+
in_package = False
28+
for line in cargo_toml.read_text(encoding="utf-8").splitlines():
29+
if line.strip() == "[package]":
30+
in_package = True
31+
continue
32+
if in_package and line.startswith("["):
33+
break
34+
if in_package:
35+
match = re.match(r'^version\s*=\s*"([^"]+)"', line)
36+
if match:
37+
return match.group(1)
38+
raise ValueError(f"no [package].version in {cargo_toml}")
39+
40+
41+
def read_locked_server(cargo_lock: Path) -> Tuple[str, str]:
42+
matches = []
43+
for block in re.split(
44+
r"(?m)^\[\[package\]\]\s*$", cargo_lock.read_text(encoding="utf-8")
45+
):
46+
fields = dict(FIELD_RE.findall(block))
47+
source = fields.get("source", "")
48+
if (
49+
fields.get("name") == "aw-server"
50+
and "ActivityWatch/aw-server-rust" in source
51+
):
52+
revision = REVISION_RE.search(source)
53+
if not revision:
54+
raise ValueError(
55+
f"aw-server source has no full Git revision: {source!r}"
56+
)
57+
matches.append((fields["version"], revision.group(1)))
58+
59+
if len(matches) != 1:
60+
raise ValueError(
61+
f"expected one Git-locked aw-server package in {cargo_lock}, found {len(matches)}"
62+
)
63+
return matches[0]
64+
65+
66+
def validation_errors(
67+
release_version: str,
68+
submodule_version: str,
69+
submodule_revision: str,
70+
tauri_version: str,
71+
tauri_revision: str,
72+
) -> List[str]:
73+
release_line = major_minor(release_version)
74+
errors = []
75+
if major_minor(submodule_version) != release_line:
76+
errors.append(
77+
f"aw-server-rust submodule version {submodule_version} does not match "
78+
f"release line {release_line}"
79+
)
80+
if major_minor(tauri_version) != release_line:
81+
errors.append(
82+
f"Tauri locks aw-server {tauri_version}, which does not match release line "
83+
f"{release_line}"
84+
)
85+
if tauri_revision != submodule_revision:
86+
errors.append(
87+
f"Tauri locks aw-server-rust {tauri_revision[:12]}, but the release "
88+
f"submodule is {submodule_revision[:12]}"
89+
)
90+
return errors
91+
92+
93+
def main() -> int:
94+
parser = argparse.ArgumentParser(description=__doc__)
95+
parser.add_argument(
96+
"release_version", help="ActivityWatch version without the leading v"
97+
)
98+
parser.add_argument(
99+
"--repo-root", type=Path, default=Path(__file__).resolve().parents[1]
100+
)
101+
args = parser.parse_args()
102+
103+
root = args.repo_root.resolve()
104+
server_root = root / "aw-server-rust"
105+
try:
106+
submodule_version = read_package_version(server_root / "aw-server/Cargo.toml")
107+
submodule_revision = subprocess.check_output(
108+
["git", "-C", str(server_root), "rev-parse", "HEAD"], text=True
109+
).strip()
110+
tauri_version, tauri_revision = read_locked_server(
111+
root / "aw-tauri/src-tauri/Cargo.lock"
112+
)
113+
errors = validation_errors(
114+
args.release_version,
115+
submodule_version,
116+
submodule_revision,
117+
tauri_version,
118+
tauri_revision,
119+
)
120+
except (OSError, subprocess.CalledProcessError, ValueError) as exc:
121+
print(
122+
f"ERROR: could not inspect bundled aw-server versions: {exc}",
123+
file=sys.stderr,
124+
)
125+
return 1
126+
127+
print(f"ActivityWatch release: {args.release_version}")
128+
print(f"aw-server-rust: {submodule_version} @ {submodule_revision[:12]}")
129+
print(f"Tauri Cargo.lock: {tauri_version} @ {tauri_revision[:12]}")
130+
if errors:
131+
print("\nERROR: inconsistent aw-server build inputs:", file=sys.stderr)
132+
for error in errors:
133+
print(f" - {error}", file=sys.stderr)
134+
print(
135+
"\nUpdate both the aw-server-rust submodule and aw-tauri's Cargo.lock "
136+
"to the same revision before tagging.",
137+
file=sys.stderr,
138+
)
139+
return 1
140+
141+
print("OK: Qt and Tauri will bundle the same aw-server-rust revision")
142+
return 0
143+
144+
145+
if __name__ == "__main__":
146+
raise SystemExit(main())
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import importlib.util
2+
from pathlib import Path
3+
4+
import pytest
5+
6+
7+
SCRIPT = Path(__file__).parents[1] / "check_tauri_server.py"
8+
SPEC = importlib.util.spec_from_file_location("check_tauri_server", SCRIPT)
9+
assert SPEC and SPEC.loader
10+
checker = importlib.util.module_from_spec(SPEC)
11+
SPEC.loader.exec_module(checker)
12+
13+
major_minor = checker.major_minor
14+
read_locked_server = checker.read_locked_server
15+
read_package_version = checker.read_package_version
16+
validation_errors = checker.validation_errors
17+
18+
19+
REVISION = "a" * 40
20+
21+
22+
def test_major_minor_accepts_beta_release():
23+
assert major_minor("0.14.0b5") == "0.14"
24+
25+
26+
def test_read_package_version(tmp_path: Path):
27+
cargo_toml = tmp_path / "Cargo.toml"
28+
cargo_toml.write_text(
29+
'[workspace]\n\n[package]\nname = "aw-server"\nversion = "0.14.0"\n'
30+
)
31+
32+
assert read_package_version(cargo_toml) == "0.14.0"
33+
34+
35+
def test_read_locked_server(tmp_path: Path):
36+
cargo_lock = tmp_path / "Cargo.lock"
37+
cargo_lock.write_text(
38+
'[[package]]\nname = "other"\nversion = "1.0.0"\n\n'
39+
'[[package]]\nname = "aw-server"\nversion = "0.14.0"\n'
40+
'source = "git+https://github.com/ActivityWatch/aw-server-rust.git?branch=master#'
41+
f'{REVISION}"\n'
42+
)
43+
44+
assert read_locked_server(cargo_lock) == ("0.14.0", REVISION)
45+
46+
47+
def test_valid_when_release_line_and_revisions_match():
48+
assert validation_errors("0.14.0b5", "0.14.0", REVISION, "0.14.0", REVISION) == []
49+
50+
51+
def test_rejects_old_tauri_release_line():
52+
errors = validation_errors("0.14.0b5", "0.14.0", REVISION, "0.13.1", REVISION)
53+
54+
assert errors == [
55+
"Tauri locks aw-server 0.13.1, which does not match release line 0.14"
56+
]
57+
58+
59+
def test_rejects_different_revision_even_on_same_release_line():
60+
errors = validation_errors("0.14.0b5", "0.14.0", REVISION, "0.14.0", "b" * 40)
61+
62+
assert errors == [
63+
"Tauri locks aw-server-rust bbbbbbbbbbbb, but the release submodule is aaaaaaaaaaaa"
64+
]
65+
66+
67+
def test_rejects_invalid_version():
68+
with pytest.raises(ValueError, match="invalid version"):
69+
major_minor("dev")

0 commit comments

Comments
 (0)