Add SyncPlay stats-for-nerds page, manual sync button, and release pipeline
Build Check / build (push) Successful in 29s
Release Plugin / release (push) Successful in 22s

Phase 0 scaffold plus everything since: quality negotiation, host-relay,
and active drift correction were each built and reverted after real bugs
in live use (see PLAN.md for the full account of each). What remains is
read-only SyncPlay observability (live drift/playback stats page) and a
one-shot manual "sync me to group" action, plus Phase 5 packaging/release
tooling (build.yaml, manifest.json generator, Gitea Actions release
workflow) to distribute it as a proper plugin repository.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-09 17:06:46 +02:00
co-authored by Claude Sonnet 5
parent cf8badcdda
commit b83074d55f
21 changed files with 1435 additions and 21 deletions
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""Appends (or replaces) this build's version entry in manifest.json, using build.yaml
for the plugin-level metadata (guid, name, description, etc.) and changelog. Run by
dev/package-release.sh; not meant to be invoked directly outside that flow."""
import argparse
import datetime
import json
import re
def parse_build_yaml(path):
# build.yaml is simple enough to not need a real YAML parser dependency.
text = open(path, encoding="utf-8").read()
fields = {}
for key in ("name", "guid", "targetAbi", "framework", "owner", "overview", "category"):
m = re.search(rf'^{key}:\s*"([^"]*)"', text, re.MULTILINE)
if m:
fields[key] = m.group(1)
def block(key):
m = re.search(rf'^{key}:\s*>\s*\n((?: .*\n?)+)', text, re.MULTILINE)
if not m:
return ""
lines = [line[2:].rstrip() for line in m.group(1).splitlines()]
return " ".join(line for line in lines if line).strip()
fields["description"] = block("description")
fields["changelog"] = block("changelog")
return fields
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--build-yaml", required=True)
parser.add_argument("--manifest", required=True)
parser.add_argument("--version", required=True)
parser.add_argument("--checksum", required=True)
parser.add_argument("--source-url", required=True)
args = parser.parse_args()
build = parse_build_yaml(args.build_yaml)
try:
with open(args.manifest, encoding="utf-8") as f:
manifest = json.load(f)
except FileNotFoundError:
manifest = []
entry = next((p for p in manifest if p.get("guid") == build["guid"]), None)
if entry is None:
entry = {
"guid": build["guid"],
"name": build["name"],
"description": build["description"],
"overview": build["overview"],
"owner": build["owner"],
"category": build["category"],
"versions": [],
}
manifest.append(entry)
else:
entry["description"] = build["description"]
entry["overview"] = build["overview"]
entry["owner"] = build["owner"]
entry["category"] = build["category"]
entry["versions"] = [v for v in entry["versions"] if v["version"] != args.version]
entry["versions"].insert(0, {
"version": args.version,
"changelog": build["changelog"],
"targetAbi": build["targetAbi"],
"sourceUrl": args.source_url,
"checksum": args.checksum,
"timestamp": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
})
with open(args.manifest, "w", encoding="utf-8") as f:
json.dump(manifest, f, indent=4)
f.write("\n")
print(f"Updated {args.manifest} with version {args.version}")
if __name__ == "__main__":
main()