From d6d27fb06260e38026e6d847cb2800154e734f7e Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Thu, 24 Sep 2026 03:43:42 +0200 Subject: [PATCH] Publish a Gitea Release from CI on every v* tag 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. --- .gitea/workflows/build-and-test.yml | 58 ++++++++++++++++ tools/publish-release.sh | 100 ++++++++++++++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100755 tools/publish-release.sh diff --git a/.gitea/workflows/build-and-test.yml b/.gitea/workflows/build-and-test.yml index ed5d3c9..14fabf6 100644 --- a/.gitea/workflows/build-and-test.yml +++ b/.gitea/workflows/build-and-test.yml @@ -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/* diff --git a/tools/publish-release.sh b/tools/publish-release.sh new file mode 100755 index 0000000..b71d775 --- /dev/null +++ b/tools/publish-release.sh @@ -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 ..." >&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}"