Publish a Gitea Release from CI on every v* tag
Benchmarks / CPU and I/O (per commit) (push) Successful in 2m28s
Benchmarks / Frame budget (on demand) (push) Skipped
Build and test / Desktop (Linux) (push) Successful in 45m9s
Build and test / Layer separation (push) Successful in 38s
Traceability / Requirement traces (push) Successful in 44s
🐳 Android image / Build and push (push) Successful in 5s
🐳 Windows image / Build and push (push) Successful in 1s
Build and test / android-image (push) Successful in 5s
Build and test / windows-image (push) Successful in 1s
Build and test / Android (aarch64) (push) Successful in 14m25s
Build and test / Windows (x86_64, cross) (push) Successful in 34m30s
Build and test / Publish the release (push) Skipped

Nothing made a release. CI built the APK and the installer on the master
push and kept them as workflow artefacts, the Linux binary was not kept
at all, and most tags went out with no downloads until they were
attached by hand.

build-and-test now also runs on v* tags. On a tag the desktop job keeps
its release binary, and a release job that needs desktop, Android and
Windows collects the three, names them with the version and runs
tools/publish-release.sh. The script titles and describes the release
from the annotated tag's message as the server holds it, writes
SHA256SUMS, and attaches what is not already there, so a re-run after
an interrupted upload finishes the job instead of duplicating it. The
same script is how a release is made or finished by hand.

Tried on v0.14.1, whose release was made by hand with the same files:
it found the release, reported all four files attached, and changed
nothing.
This commit is contained in:
2026-09-24 03:43:42 +02:00
parent 317a2f40bd
commit d6d27fb062
2 changed files with 158 additions and 0 deletions
+58
View File
@@ -7,6 +7,10 @@ name: Build and test
on:
push:
branches: [main, master, develop]
# A release tag builds again and publishes what it built (the `release`
# job at the end). The master push of the same commit has usually filled
# the caches, so the second run is the warm one.
tags: ['v*']
pull_request:
branches: [main, master, develop]
@@ -154,6 +158,16 @@ jobs:
- name: Build
run: cargo build --workspace --release
# Only on a release tag: the binary is 150 MB and nothing but the
# release job wants it.
- name: Upload the desktop binary
if: startsWith(github.ref, 'refs/tags/v')
uses: actions/upload-artifact@v3
with:
name: darkroom-desktop-x86_64-linux
path: target/release/darkroom-desktop
if-no-files-found: error
- name: Disk after
if: always()
run: df -h /workspace 2>/dev/null || df -h .
@@ -519,3 +533,47 @@ jobs:
fi
done
exit $FAILED
# A v* tag becomes a Gitea Release carrying the three builds and their
# SHA256SUMS, titled and described by the tag's message. Until this job
# existed every release was made by hand, and most tags never got one.
#
# It needs all three platform jobs, so a tag whose tests fail publishes
# nothing; re-run the failed job and this one follows. The work is
# tools/publish-release.sh, which is also how a release is finished by hand.
release:
if: startsWith(github.ref, 'refs/tags/v')
needs: [desktop, android, windows]
runs-on: linux/amd64
name: Publish the release
container:
image: catthehacker/ubuntu:act-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Fetch the builds
uses: actions/download-artifact@v3
with:
path: dist
# Named for the download page, with the version in each name the way
# the hand-made releases had them. The installer already carries its
# version from package.sh.
- name: Publish
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN || github.token }}
TAG: ${{ github.ref_name }}
run: |
set -e
V="${TAG#v}"
ls -lR dist
mkdir -p out
cp dist/darkroom-arm64-v8a-apk/darkroom.apk "out/darkroom-${V}-arm64-v8a.apk"
cp dist/darkroom-desktop-x86_64-linux/darkroom-desktop "out/darkroom-desktop-${V}-x86_64-linux"
chmod +x "out/darkroom-desktop-${V}-x86_64-linux"
cp dist/darkroom-windows-x86_64-setup/DarkRoom-${V}-x86_64-setup.exe out/
bash tools/publish-release.sh "$TAG" out/*
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env bash
# Turn a pushed tag into a Gitea Release with the build's files attached.
#
# GITEA_TOKEN=... tools/publish-release.sh v0.14.1 FILE...
#
# The release is named and described by the tag's own message — its first
# line is the title, the rest the notes — so the notes are written once, in
# the annotated tag, and not restated anywhere. SHA256SUMS over the given
# files is written and attached alongside them.
#
# Run by CI's `release` job on every v* tag, and by hand with the same
# arguments when a release has to be made or finished from a workstation.
# It is safe to repeat: an existing release is reused, and a file already
# attached under the same name and size is skipped, so a second run after an
# interrupted upload attaches only what is missing.
#
# Before this existed nothing made a release: CI built the APK and the
# installer on the master push and kept them as workflow artefacts, and
# most tags went out with no downloads at all.
set -euo pipefail
GITEA_URL="${GITEA_URL:-https://gitea.tourolle.paris}"
GITEA_REPO="${GITEA_REPO:-dtourolle/DarkRoom}"
: "${GITEA_TOKEN:?set GITEA_TOKEN to a token that can write releases}"
if [[ $# -lt 2 ]]; then
echo "usage: tools/publish-release.sh <tag> <file>..." >&2
exit 2
fi
TAG="$1"
shift
API="${GITEA_URL}/api/v1/repos/${GITEA_REPO}"
AUTH=(-H "Authorization: token ${GITEA_TOKEN}")
for f in "$@"; do
[[ -f "${f}" ]] || { echo "error: ${f} does not exist" >&2; exit 1; }
done
# The checksums, beside the files they describe, named as they will be
# downloaded — `sha256sum -c SHA256SUMS` in a download folder has to work.
WORK="$(mktemp -d)"
trap 'rm -rf "${WORK}"' EXIT
(
for f in "$@"; do
printf '%s %s\n' "$(sha256sum "${f}" | cut -d' ' -f1)" "$(basename "${f}")"
done
) > "${WORK}/SHA256SUMS"
cat "${WORK}/SHA256SUMS"
# The notes, from the server's copy of the tag. A CI checkout of a tag can
# hold it as a lightweight ref with no message, so the local repository is
# not asked.
curl -fsS "${AUTH[@]}" "${API}/tags/${TAG}" > "${WORK}/tag.json" \
|| { echo "error: no tag ${TAG} on ${GITEA_REPO}" >&2; exit 1; }
RELEASE_ID="$(curl -sS "${AUTH[@]}" "${API}/releases/tags/${TAG}" \
| python3 -c 'import json,sys; print(json.load(sys.stdin).get("id") or "")' 2>/dev/null || true)"
if [[ -n "${RELEASE_ID}" ]]; then
echo "==> release ${TAG} exists (id ${RELEASE_ID}); attaching what is missing"
else
python3 - "${TAG}" "${WORK}/tag.json" > "${WORK}/release.json" <<'PY'
import json, sys
tag, path = sys.argv[1], sys.argv[2]
message = (json.load(open(path)).get("message") or "").strip()
if not message:
sys.exit(f"error: {tag} has no message; releases are cut from annotated tags")
title, _, body = message.partition("\n")
print(json.dumps({"tag_name": tag, "name": title.strip(), "body": body.strip(),
"draft": False, "prerelease": False}))
PY
RELEASE_ID="$(curl -fsS "${AUTH[@]}" -H 'Content-Type: application/json' \
--data @"${WORK}/release.json" "${API}/releases" \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])')"
echo "==> created release ${TAG} (id ${RELEASE_ID})"
fi
curl -fsS "${AUTH[@]}" "${API}/releases/${RELEASE_ID}/assets" > "${WORK}/assets.json"
for f in "$@" "${WORK}/SHA256SUMS"; do
name="$(basename "${f}")"
size="$(stat -c%s "${f}")"
have="$(python3 -c 'import json,sys
for a in json.load(open(sys.argv[1])):
if a["name"] == sys.argv[2]: print(a["size"])' "${WORK}/assets.json" "${name}")"
if [[ "${have}" == "${size}" ]]; then
echo " ${name}: already attached"
continue
fi
if [[ -n "${have}" ]]; then
echo "error: ${name} is attached at ${have} bytes, not ${size}; remove it by hand" >&2
exit 1
fi
echo " ${name}: uploading ${size} bytes"
curl -fsS "${AUTH[@]}" -F "attachment=@${f}" \
"${API}/releases/${RELEASE_ID}/assets?name=${name}" > /dev/null
done
echo "==> ${GITEA_URL}/${GITEA_REPO}/releases/tag/${TAG}"