Some checks failed
Research feed publisher tests / test (push) Failing after 3s
Release / Pre-flight checks (push) Failing after 0s
Release / Research Edition — patch and packaging smoke tests (push) Has been skipped
Release / Build Qt artifacts (false, 22, ubuntu-22.04, 3.9, ${{ github.event_name == 'pull_request' || (github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/')) }}, false, false) (push) Has been skipped
Release / Build Qt artifacts (false, 22, ubuntu-22.04, 3.9, false, false, false) (push) Has been skipped
Release / Build Qt artifacts (manylinux_2_28 — glibc 2.28 ABI floor) (22, 3.9) (push) Has been skipped
Release / Generate release notes (push) Has been skipped
Release / Create dev release tag (push) Has been skipped
CodeQL / Analyze (javascript) (push) Failing after 1s
CodeQL / Analyze (python) (push) Failing after 1s
Release / Build Qt artifacts (false, 22, windows-latest, 3.9, false, false, false) (push) Has been cancelled
Release / Build Tauri artifacts (false, 22, macos-15-intel, 3.9, false, false, false) (push) Has been cancelled
Release / Build Tauri artifacts (false, 22, macos-latest, 3.9, false, false, false) (push) Has been cancelled
Release / Build Tauri artifacts (false, 22, ubuntu-24.04, 3.9, ${{ github.event_name == 'pull_request' || (github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/')) }}, false, false) (push) Has been cancelled
Release / Build Tauri artifacts (false, 22, ubuntu-24.04, 3.9, false, false, false) (push) Has been cancelled
Release / Build Tauri artifacts (false, 22, ubuntu-24.04-arm, 3.9, false, false, false) (push) Has been cancelled
Release / Build Tauri artifacts (false, 22, windows-latest, 3.9, false, false, false) (push) Has been cancelled
Release / Build Qt artifacts (false, 22, macos-15-intel, 3.9, false, false, false) (push) Has been cancelled
Release / Build Qt artifacts (false, 22, macos-latest, 3.9, false, false, false) (push) Has been cancelled
Release / Publish draft release (push) Has been cancelled
182 lines
6.0 KiB
Python
Executable File
182 lines
6.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Assemble a Tauri updater manifest from per-platform .sig files.
|
|
|
|
Expects updater artifacts named
|
|
`activitywatch-tauri[-research]-<version>-<platform-key>.<ext>` with a
|
|
matching `<...>.sig` file alongside it (as produced by the "Package Tauri
|
|
updater artifacts" step in release.yml), where <platform-key> is a Tauri
|
|
updater platform identifier such as "darwin-aarch64" or "linux-x86_64".
|
|
|
|
Standard and Research Edition releases are partitioned by filename:
|
|
|
|
- standard: `latest.json` + `activitywatch-tauri-<ver>-...`
|
|
- research: `latest-research.json` + `activitywatch-tauri-research-<ver>-...`
|
|
|
|
so the two lines cannot overwrite each other's GitHub release assets or
|
|
share an updater endpoint.
|
|
"""
|
|
import argparse
|
|
import json
|
|
import os
|
|
import re
|
|
from datetime import datetime, timezone
|
|
|
|
EDITIONS = ("standard", "research")
|
|
|
|
|
|
def normalize_version(version: str) -> str:
|
|
"""Strip a leading 'v' and a trailing '-research' edition suffix."""
|
|
if version.startswith("v"):
|
|
version = version[1:]
|
|
if version.endswith("-research"):
|
|
version = version[: -len("-research")]
|
|
return version
|
|
|
|
|
|
def tauri_version(version: str) -> str:
|
|
"""Convert AW release/dev labels to the SemVer embedded by Tauri.
|
|
|
|
Asset filenames and GitHub tags keep their original AW version spelling.
|
|
"""
|
|
version = normalize_version(version)
|
|
number = r"(?:0|[1-9][0-9]*)"
|
|
match = re.fullmatch(
|
|
rf"(?P<base>{number}\.{number}\.{number})"
|
|
rf"(?:(?P<pre>a|b|rc)(?P<serial>{number}))?"
|
|
r"(?:\.dev-(?P<dev>[0-9a-f]+|unknown))?",
|
|
version,
|
|
)
|
|
if not match:
|
|
raise ValueError(f"Unsupported ActivityWatch version: {version!r}")
|
|
suffix = []
|
|
if match["pre"]:
|
|
suffix.extend(
|
|
({"a": "alpha", "b": "beta", "rc": "rc"}[match["pre"]], match["serial"])
|
|
)
|
|
if match["dev"]:
|
|
# Prefix hashes so an all-digit hash with a leading zero stays valid.
|
|
suffix.extend(("dev", "g" + match["dev"]))
|
|
return match["base"] + ("-" + ".".join(suffix) if suffix else "")
|
|
|
|
|
|
def infer_edition(tag: str, edition=None) -> str:
|
|
if edition:
|
|
if edition not in EDITIONS:
|
|
raise ValueError(f"unknown edition {edition!r}")
|
|
return edition
|
|
return "research" if tag.endswith("-research") else "standard"
|
|
|
|
|
|
def asset_prefix(edition: str) -> str:
|
|
if edition == "research":
|
|
return "activitywatch-tauri-research"
|
|
return "activitywatch-tauri"
|
|
|
|
|
|
def manifest_filename(edition: str) -> str:
|
|
return "latest-research.json" if edition == "research" else "latest.json"
|
|
|
|
|
|
# Tauri v2 recommends NSIS for Windows updater bundles. When both NSIS and
|
|
# MSI signatures exist for the same platform key, keep NSIS regardless of
|
|
# os.walk order. Unlisted extensions share rank 0 (first one wins).
|
|
WINDOWS_BUNDLE_RANK = {
|
|
"nsis.zip": 2,
|
|
"exe": 1,
|
|
"msi.zip": 0,
|
|
"msi": 0,
|
|
}
|
|
|
|
|
|
def bundle_rank(ext: str) -> int:
|
|
return WINDOWS_BUNDLE_RANK.get(ext, 0)
|
|
|
|
|
|
def collect_platforms(dist: str, version: str, repo: str, tag: str, edition: str) -> dict:
|
|
prefix = asset_prefix(edition)
|
|
# Non-greedy platform group: extensions can be multi-part (.app.tar.gz,
|
|
# .AppImage.tar.gz, .nsis.zip), so stop at the first dot after the
|
|
# platform key rather than the last.
|
|
pattern = re.compile(
|
|
rf"^{re.escape(prefix)}-{re.escape(version)}-(?P<platform>.+?)\.(?P<ext>.+)$"
|
|
)
|
|
|
|
platforms = {}
|
|
chosen_ext = {}
|
|
for root, _, files in os.walk(dist):
|
|
for name in files:
|
|
if not name.endswith(".sig"):
|
|
continue
|
|
asset_name = name[: -len(".sig")]
|
|
m = pattern.match(asset_name)
|
|
if not m:
|
|
continue
|
|
platform = m.group("platform")
|
|
ext = m.group("ext")
|
|
prev_ext = chosen_ext.get(platform)
|
|
if prev_ext is not None and bundle_rank(ext) <= bundle_rank(prev_ext):
|
|
continue
|
|
with open(os.path.join(root, name)) as f:
|
|
signature = f.read().strip()
|
|
chosen_ext[platform] = ext
|
|
platforms[platform] = {
|
|
"signature": signature,
|
|
"url": (
|
|
f"https://github.com/{repo}/releases/download/"
|
|
f"{tag}/{asset_name}"
|
|
),
|
|
}
|
|
return platforms
|
|
|
|
|
|
def build_manifest(version: str, notes: str, platforms: dict, pub_date=None) -> dict:
|
|
if pub_date is None:
|
|
pub_date = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
return {
|
|
"version": version,
|
|
"notes": notes,
|
|
"pub_date": pub_date,
|
|
"platforms": platforms,
|
|
}
|
|
|
|
|
|
def main(argv=None):
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--version", required=True)
|
|
parser.add_argument("--notes", required=True)
|
|
parser.add_argument(
|
|
"--repo", required=True, help="e.g. ActivityWatch/activitywatch"
|
|
)
|
|
parser.add_argument("--tag", required=True, help="e.g. v0.13.3 or v0.13.3-research")
|
|
parser.add_argument(
|
|
"--edition",
|
|
choices=EDITIONS,
|
|
default=None,
|
|
help="Release line. Inferred from --tag (*-research) if omitted.",
|
|
)
|
|
parser.add_argument("--dist", required=True, help="directory to search for *.sig files")
|
|
parser.add_argument("--output", required=True)
|
|
args = parser.parse_args(argv)
|
|
|
|
version = normalize_version(args.version)
|
|
edition = infer_edition(args.tag, args.edition)
|
|
platforms = collect_platforms(args.dist, version, args.repo, args.tag, edition)
|
|
|
|
if not platforms:
|
|
raise SystemExit(
|
|
f"No {edition} updater artifacts found - refusing to write an empty "
|
|
f"{os.path.basename(args.output)}"
|
|
)
|
|
|
|
manifest = build_manifest(tauri_version(version), args.notes, platforms)
|
|
|
|
with open(args.output, "w") as f:
|
|
json.dump(manifest, f, indent=2)
|
|
f.write("\n")
|
|
|
|
print(f"Wrote {args.output} ({edition}) with platforms: {', '.join(sorted(platforms))}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|