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}"