Read feature, release workflow (v0.3.0)
Release / release (push) Successful in 6m46s

- Read button (desktop and web): reads the tag now, bypassing the status
  cache, and puts its first link or text record into the input field so it
  can be copied to another tag. POST /api/read in the web version.
- Raw memory: every 4-byte page from page 0 (UID, lock bytes, CC, data) in
  hex and ASCII, collapsed by default.
- Content now shows the records and the note together (a locked tag showed
  only "read-only" before); read failures are flagged with read_failed
  instead of matching message text.
- .gitea/workflows/release.yml: on a v* tag, tests and clippy, then Linux
  (ubuntu-22.04, glibc 2.35) and Windows (MinGW cross-build) packages
  attached to the tag's release with its CHANGELOG.md section.
- README: reading, why the reader program runs on the reader's machine,
  releases.
This commit is contained in:
grabowski committed 2026-10-29 09:38:43 +07:00
1 parent cb1b1b04c8
commit 8b95d0d5d7
10 files changed
+610 -165

No files matched your search

+93
View File
@@ -0,0 +1,93 @@
#!/usr/bin/env python3
"""Attach files to the Gitea release of the current tag; create the release when missing.
TOKEN=<job token> python3 .gitea/scripts/release-attach.py dist/*.zip dist/*.tar.gz
Reads GITHUB_SERVER_URL, GITHUB_REPOSITORY and GITHUB_REF_NAME (the tag), as Gitea Actions sets
them. A new release gets the tag's CHANGELOG.md section as its text. An asset of the same name
from an earlier run of the tag is replaced. Used by .gitea/workflows/release.yml.
"""
import json
import os
import re
import sys
import urllib.error
import urllib.parse
import urllib.request
import uuid
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
def changelog(tag):
path = ROOT / "CHANGELOG.md"
if not path.exists():
return ""
text = path.read_text(encoding="utf-8")
m = re.search(rf"^## {re.escape(tag)}\b.*?$(.*?)(?=^## |\Z)", text, re.M | re.S)
return m.group(1).strip() if m else ""
class Gitea:
def __init__(self, server, repo, token):
self.api = f"{server.rstrip('/')}/api/v1/repos/{repo}"
self.auth = {"Authorization": f"token {token}"}
def call(self, method, path, body=None, ctype="application/json"):
req = urllib.request.Request(
self.api + path,
data=body,
method=method,
headers={**self.auth, "Content-Type": ctype, "Accept": "application/json"},
)
with urllib.request.urlopen(req, timeout=300) as r:
data = r.read()
return json.loads(data) if data.strip() else None
def release(self, tag):
try:
return self.call("GET", f"/releases/tags/{urllib.parse.quote(tag, safe='')}"), False
except urllib.error.HTTPError as e:
if e.code != 404:
raise
body = json.dumps({"tag_name": tag, "name": f"NFC tag writer {tag}", "body": changelog(tag)})
return self.call("POST", "/releases", body.encode()), True
def upload(self, rel, name, data):
b = uuid.uuid4().hex
head = (
f'--{b}\r\nContent-Disposition: form-data; name="attachment"; filename="{name}"\r\n'
"Content-Type: application/octet-stream\r\n\r\n"
).encode()
body = head + data + f"\r\n--{b}--\r\n".encode()
self.call(
"POST",
f"/releases/{rel['id']}/assets?name={urllib.parse.quote(name)}",
body,
f"multipart/form-data; boundary={b}",
)
def main():
paths = [Path(p) for p in sys.argv[1:]]
if not paths:
sys.exit(__doc__)
env = {k: os.environ.get(k, "") for k in ("GITHUB_SERVER_URL", "GITHUB_REPOSITORY", "GITHUB_REF_NAME", "TOKEN")}
missing = [k for k, v in env.items() if not v]
if missing:
sys.exit(f"missing environment: {', '.join(missing)}")
g = Gitea(env["GITHUB_SERVER_URL"], env["GITHUB_REPOSITORY"], env["TOKEN"])
rel, created = g.release(env["GITHUB_REF_NAME"])
print(("created" if created else "found") + f" release {env['GITHUB_REF_NAME']} (id {rel['id']})")
old = {a["name"]: a["id"] for a in rel.get("assets") or []}
for p in paths:
if p.name in old:
g.call("DELETE", f"/releases/{rel['id']}/assets/{old[p.name]}")
data = p.read_bytes()
g.upload(rel, p.name, data)
print(f" {'replaced' if p.name in old else 'attached'} {p.name} ({len(data):,} bytes)")
if __name__ == "__main__":
main()