Template
release/: the newest stage's manufacturing files and documentation
A stage tag (v*) now builds, with kicad-cli, pandoc and pdflatex in the CI image (.gitea/scripts/release-files.py): - per board: Gerbers + Excellon drill zip (JLCPCB-compatible settings), BOM (grouped, DNP left out), pick and place, schematic and PCB PDFs, 3D renders; - docs/*.md and docs/tex/*.tex as PDF, tag and commit in the footer; - release/README.md with tag, commit, ERC/DRC result, files, SHA-256 and the stage's CHANGELOG section. Every file name carries the tag. release-attach.py attaches the files plus a zip of the whole folder to the tag's release (replacing assets of an earlier run); for the newest stage tag (not older stages, not pre-releases) the job commits them to release/ on main with [skip ci]. .gitignore: !/release/ after Release/ (Windows matches it case-insensitively); .gitattributes: release/** -text so the SHA-256 match on every checkout. TEMPLATE.md: "Release files" section, rules and CI updated. Tested in git.b4l.co.th/b4l/kicad10_auto_node with harNES's board and docs: v0.1 committed, v0.0 and v0.2-rc.1 left release/ alone, v0.2 tagged on an older commit landed on top of a moved main; uploads byte-exact against a stub of the Gitea release API.
This commit is contained in:
1 parent
0e002be80c
commit
52a40c1c3e
11 files changed
+654
-53
No files matched your search
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Attach the files of a release folder (release-files.py) to the Gitea release of the tag.
|
||||
|
||||
TOKEN=<job token> python3 .gitea/scripts/release-attach.py /tmp/release
|
||||
|
||||
Reads GITHUB_SERVER_URL, GITHUB_REPOSITORY and GITHUB_REF_NAME (the tag), as Gitea Actions sets
|
||||
them. Creates the release when the tag has none, with the tag's CHANGELOG.md section as its text.
|
||||
Uploads every file of the folder except README.md, and <repository>-<tag>.zip with the whole folder;
|
||||
an asset of the same name from an earlier run of the tag is replaced. Used by
|
||||
.gitea/workflows/release.yml.
|
||||
"""
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
import uuid
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
ZIP_TIME = (2020, 1, 1, 0, 0, 0)
|
||||
|
||||
|
||||
def changelog(tag):
|
||||
path = ROOT / "CHANGELOG.md"
|
||||
if not path.exists():
|
||||
return ""
|
||||
m = re.search(rf"^## {re.escape(tag)}\b.*?$(.*?)(?=^## |\Z)", path.read_text(encoding="utf-8"), re.M | re.S)
|
||||
return m.group(1).strip() if m else ""
|
||||
|
||||
|
||||
def bundle(folder, top):
|
||||
"""The whole folder as a zip under <top>/, with fixed timestamps."""
|
||||
buf = io.BytesIO()
|
||||
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as z:
|
||||
for p in sorted(folder.rglob("*")):
|
||||
if p.is_file():
|
||||
info = zipfile.ZipInfo(f"{top}/{p.relative_to(folder).as_posix()}", date_time=ZIP_TIME)
|
||||
info.compress_type = zipfile.ZIP_DEFLATED
|
||||
z.writestr(info, p.read_bytes())
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
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=120) 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
|
||||
notes = changelog(tag)
|
||||
body = (f"{notes}\n\n" if notes else "") + "Gerbers, BOM, pick and place, PDFs and documentation: the files below."
|
||||
return self.call("POST", "/releases", json.dumps({"tag_name": tag, "name": tag, "body": 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():
|
||||
if len(sys.argv) != 2:
|
||||
sys.exit(__doc__)
|
||||
folder = Path(sys.argv[1])
|
||||
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)}")
|
||||
tag, repo = env["GITHUB_REF_NAME"], env["GITHUB_REPOSITORY"]
|
||||
files = [(p.name, p.read_bytes()) for p in sorted(folder.rglob("*")) if p.is_file() and p.name != "README.md"]
|
||||
top = f"{repo.split('/')[-1]}-{tag}"
|
||||
files.append((f"{top}.zip", bundle(folder, top)))
|
||||
names = [n for n, _ in files]
|
||||
if len(set(names)) != len(names):
|
||||
sys.exit(f"two files of the same name: {sorted(n for n in names if names.count(n) > 1)}")
|
||||
|
||||
g = Gitea(env["GITHUB_SERVER_URL"], repo, env["TOKEN"])
|
||||
rel, created = g.release(tag)
|
||||
print(("created" if created else "found") + f" release {tag} (id {rel['id']})")
|
||||
old = {a["name"]: a["id"] for a in rel.get("assets") or []}
|
||||
for name, data in files:
|
||||
if name in old:
|
||||
g.call("DELETE", f"/releases/{rel['id']}/assets/{old[name]}")
|
||||
g.upload(rel, name, data)
|
||||
print(f" {'replaced' if name in old else 'attached'} {name} ({len(data):,} bytes)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user