- 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:
1 parent
cb1b1b04c8
commit
8b95d0d5d7
10 files changed
+610
-165
No files matched your search
@@ -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()
|
||||
@@ -0,0 +1,88 @@
|
||||
# Release builds on git.b4l.co.th (Gitea Actions).
|
||||
#
|
||||
# On a v* tag: tests, then the Linux and Windows x86_64 binaries (desktop app
|
||||
# and web version), packaged and attached to the tag's release (created when
|
||||
# missing, text from CHANGELOG.md). On demand (workflow_dispatch) the same
|
||||
# packages become the artifact `nfc-writer-packages` instead.
|
||||
#
|
||||
# Linux is built on ubuntu-22.04 so the binary needs only glibc 2.35 (Debian 12,
|
||||
# Ubuntu 22.04 and newer). Windows is cross-built with MinGW-w64. macOS has no
|
||||
# runner here: build it on a Mac (README, "Build and run").
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ['v*']
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUST_TOOLCHAIN: "1.98.1"
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: System packages
|
||||
run: |
|
||||
SUDO=$(command -v sudo || true)
|
||||
$SUDO apt-get update -qq
|
||||
$SUDO apt-get install -y -qq curl ca-certificates gcc libc6-dev pkg-config \
|
||||
libpcsclite-dev gcc-mingw-w64-x86-64 zip python3
|
||||
|
||||
- name: Rust toolchain
|
||||
run: |
|
||||
if ! command -v rustup >/dev/null && [ ! -x "$HOME/.cargo/bin/rustup" ]; then
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain none
|
||||
fi
|
||||
export PATH="$HOME/.cargo/bin:$PATH"
|
||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
rustup toolchain install "$RUST_TOOLCHAIN" --profile minimal -c clippy -t x86_64-pc-windows-gnu
|
||||
rustup default "$RUST_TOOLCHAIN"
|
||||
rustc --version
|
||||
|
||||
- name: Tests and clippy
|
||||
run: |
|
||||
cargo test --locked
|
||||
cargo clippy --locked --all-targets -- -D warnings
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
cargo build --locked --release
|
||||
cargo build --locked --release --target x86_64-pc-windows-gnu
|
||||
|
||||
- name: Package
|
||||
run: |
|
||||
case "$GITHUB_REF" in
|
||||
refs/tags/*) version="$GITHUB_REF_NAME" ;;
|
||||
*) version="dev-${GITHUB_SHA::7}" ;;
|
||||
esac
|
||||
mkdir -p dist
|
||||
linux="nfc-writer-$version-linux-x86_64"
|
||||
mkdir "$linux"
|
||||
cp target/release/nfc-writer target/release/nfc-writer-web README.md "$linux/"
|
||||
tar czf "dist/$linux.tar.gz" "$linux"
|
||||
windows="nfc-writer-$version-windows-x86_64"
|
||||
mkdir "$windows"
|
||||
cp target/x86_64-pc-windows-gnu/release/nfc-writer.exe \
|
||||
target/x86_64-pc-windows-gnu/release/nfc-writer-web.exe README.md "$windows/"
|
||||
zip -qr "dist/$windows.zip" "$windows"
|
||||
ls -l dist
|
||||
|
||||
- name: Attach to the release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
env:
|
||||
TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: python3 .gitea/scripts/release-attach.py dist/*
|
||||
|
||||
- name: Artifact (on-demand runs)
|
||||
if: "!startsWith(github.ref, 'refs/tags/v')"
|
||||
uses: https://github.com/christopherHX/gitea-upload-artifact@v4
|
||||
with:
|
||||
name: nfc-writer-packages
|
||||
path: dist/
|
||||
Reference in new issue
Block a user