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:
grabowski committed 2026-10-16 18:50:01 +07:00
1 parent 0e002be80c
commit 52a40c1c3e
11 files changed
+654 -53

No files matched your search

+53 -41
View File
@@ -1,9 +1,14 @@
name: Release outputs
name: Release
# On a tag v* (a prototype stage): the KiBot outputs of every board, zipped per
# board, attached to the tag's release (created when missing). The job token
# must be allowed to write releases. Adapted from LVSolarBuck; runs for the
# first time at the first tag.
# On a tag v* (a prototype stage), .gitea/scripts/release-files.py builds the
# stage's release files: for every board Gerbers and drill (zip), BOM, pick
# and place, schematic and PCB PDFs and 3D renders; the documentation
# (docs/*.md, docs/tex/*.tex) as PDF. They are attached to the tag's release
# (created when missing) and, when the tag is the newest stage, committed to
# release/ on main, so release/ always holds the latest stage. The commit
# carries [skip ci] and touches no path another workflow watches. The job
# token must be allowed to write releases and to push to main. Firmware and
# software tags (firmware/..., software/...) do not start it.
on:
push:
tags: ['v*']
@@ -11,47 +16,54 @@ on:
jobs:
release:
runs-on: kicad10_auto
permissions:
contents: write
concurrency:
group: release-${{ github.repository }}
cancel-in-progress: false
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: KiBot outputs per board
run: |
mkdir -p dist
for pro in $(find hardware -mindepth 2 -maxdepth 2 -name '*.kicad_pro' | sort); do
dir=$(dirname "$pro"); board=$(basename "$dir")
(cd "$dir" && kibot -c "$GITHUB_WORKSPACE/kibot.yaml" -d "$GITHUB_WORKSPACE/out/$board") \
|| echo "::warning::KiBot reported problems for $board"
python3 -c 'import shutil, sys; shutil.make_archive(sys.argv[1], "zip", sys.argv[2])' \
"dist/$board-${GITHUB_REF_NAME}" "out/$board"
done
ls -la dist
- name: Release files
run: python3 .gitea/scripts/release-files.py "$GITHUB_REF_NAME" --out /tmp/release
- name: Attach to the release
env:
TOKEN: ${{ secrets.GITEA_TOKEN }}
run: python3 .gitea/scripts/release-attach.py /tmp/release
- name: Commit to release/ on main
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
python3 - <<'EOF'
import json, os, pathlib, urllib.error, urllib.request, uuid
api = f"{os.environ['GITHUB_SERVER_URL']}/api/v1/repos/{os.environ['GITHUB_REPOSITORY']}"
tag = os.environ["GITHUB_REF_NAME"]
auth = {"Authorization": f"token {os.environ['TOKEN']}"}
def call(method, url, body=None, ctype="application/json"):
req = urllib.request.Request(url, data=body, method=method, headers={**auth, "Content-Type": ctype})
with urllib.request.urlopen(req) as r:
return json.load(r)
try:
rel = call("GET", f"{api}/releases/tags/{tag}")
except urllib.error.HTTPError as e:
if e.code != 404:
raise
rel = call("POST", f"{api}/releases", json.dumps({"tag_name": tag, "name": tag}).encode())
for f in sorted(pathlib.Path("dist").glob("*.zip")):
b = uuid.uuid4().hex
head = (f"--{b}\r\nContent-Disposition: form-data; name=\"attachment\"; filename=\"{f.name}\"\r\n"
"Content-Type: application/zip\r\n\r\n").encode()
body = head + f.read_bytes() + f"\r\n--{b}--\r\n".encode()
call("POST", f"{api}/releases/{rel['id']}/assets?name={f.name}", body, f"multipart/form-data; boundary={b}")
print("attached", f.name)
EOF
tag="$GITHUB_REF_NAME"
# stage tags only (v0.1, v0.2, v1.0); a pre-release such as v0.3-rc.1 leaves release/ alone
newest=$(git tag -l 'v*' --sort=-v:refname | grep -E '^v[0-9]+(\.[0-9]+)+$' | head -1 || true)
if [ "$tag" != "$newest" ]; then
echo "$tag is not the newest stage (${newest:-none}): release/ is left as it is."
exit 0
fi
git config user.name "Release CI"
git config user.email "ci@noreply.git.b4l.co.th"
REPO_URL=$(git config --get remote.origin.url)
AUTHED_URL=${REPO_URL/https:\/\//https:\/\/${GITEA_TOKEN}@}
# main may move while this runs: start again from the newest main
for i in 1 2 3; do
git fetch -q "$AUTHED_URL" main
git checkout -q -f -B main FETCH_HEAD
rm -rf release
mkdir release
cp -r /tmp/release/. release/
git add -A -- release
if git diff --cached --quiet; then echo "release/ already holds $tag."; exit 0; fi
git commit -q -m "Release $tag: manufacturing files and documentation [skip ci]"
if git push -q "$AUTHED_URL" HEAD:main; then
echo "Pushed $(git rev-parse --short HEAD): release/ holds $tag"
exit 0
fi
sleep 5
done
echo "::error::Could not push release/ to main"
exit 1