name: Release # Builds the Docker image, the Windows installers (NSIS + portable) and the Linux # AppImage, then publishes all of it: the image goes to the Gitea container # registry as `latest` + the tag, the installers are attached to a Gitea release # for that tag. Flatpak is intentionally not built here — flatpak-builder's # sandbox (bwrap) needs working user namespaces, which a containerized Actions # runner is not guaranteed to have; it stays a local-only step via # scripts/publish.ps1. # # There is no Windows runner in this setup, so the "win" target (NSIS + portable) # is cross-built on the Linux runner via Wine, the same path # scripts/build-release.ps1 uses for -UseWsl. Unsigned either way. # # Requires two Actions secrets on this repo (Settings > Actions > Secrets), a # token with package read/write *and* repository write (write:repository) scope: # BUILD_USER - registry/API username # BUILD_TOKEN - registry/API token # (named BUILD_* rather than GITEA_* because Gitea rejects secrets whose name # starts with GITEA) on: push: tags: - '*' workflow_dispatch: inputs: tag: description: 'Tag to publish (e.g. v1.2.3)' required: true type: string env: IMAGE: git.azuze.fr/kawa/motionity jobs: release: runs-on: docker-build steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # full tag history, for the changelog compare link - name: Resolve tag id: vars env: EVENT_NAME: ${{ github.event_name }} DISPATCH_TAG: ${{ inputs.tag }} REF_TAG: ${{ github.ref_name }} run: | if [ "$EVENT_NAME" = "workflow_dispatch" ]; then echo "tag=$DISPATCH_TAG" >> "$GITHUB_OUTPUT" else echo "tag=$REF_TAG" >> "$GITHUB_OUTPUT" fi - uses: actions/setup-node@v4 with: node-version: '22' - name: Install build tools (jq, Wine for the NSIS uninstaller) run: | sudo dpkg --add-architecture i386 sudo apt-get update sudo apt-get install -y jq wine locales # makensis converts its argv through the locale charset and aborts with # "FATAL: main argv conversion failed!" under C/POSIX. C.UTF-8 is a glibc # built-in only from 2.35 (Debian 12+), so en_US.UTF-8 is generated instead. # # `locale-gen ` is an Ubuntu extension: plain Debian's locale-gen # takes no arguments and only reads /etc/locale.gen, so the entry goes in # there first. The anchored grep does not match the line the package ships # commented out. grep -q '^en_US.UTF-8 UTF-8' /etc/locale.gen \ || echo 'en_US.UTF-8 UTF-8' | sudo tee -a /etc/locale.gen sudo locale-gen locale -a locale -a | grep -qiE '^en_US\.utf-?8$' || { echo "en_US.UTF-8 was not generated; makensis will abort on argv conversion" >&2 exit 1 } - name: Ensure PowerShell # Debian ships no snapd by default, so this pulls Microsoft's portable # linux-x64 tarball instead — no package repo or GPG key setup needed. # A no-op on repeat runs once it's on PATH, since this runner is a # persistent host, not a fresh container per job. run: | if command -v pwsh >/dev/null; then exit 0; fi url=$(curl -sL https://api.github.com/repos/PowerShell/PowerShell/releases/latest \ | jq -r '.assets[] | select(.name | test("linux-x64\\.tar\\.gz$")) | .browser_download_url') [ -n "$url" ] || { echo "could not resolve a PowerShell linux-x64 release asset" >&2; exit 1; } sudo mkdir -p /opt/microsoft/powershell/7 curl -sL "$url" | sudo tar xz -C /opt/microsoft/powershell/7 sudo ln -sf /opt/microsoft/powershell/7/pwsh /usr/bin/pwsh sudo chmod +x /opt/microsoft/powershell/7/pwsh - name: makensis locale probe # Diagnostic only, never fails the job. If `-VERSION` prints a version under # en_US.UTF-8 and "FATAL: main argv conversion failed!" under C, the locale # really is the whole story. If it fails under both, it is not the locale and # the next thing to try is DEBUG=electron-builder on the build step to see the # argv makensis is actually handed. Prints nothing useful on the very first # run of a fresh runner, where the NSIS bundle has not been downloaded yet. continue-on-error: true run: | locale || true mk=$(find "$HOME/.cache/electron-builder" /var/lib/gitea-runner/.cache/electron-builder \ -type f -name makensis -path '*/linux/*' 2>/dev/null | head -n1) echo "makensis: ${mk:-}" [ -n "$mk" ] || exit 0 echo "--- LC_ALL=en_US.UTF-8" LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 "$mk" -VERSION || echo "failed, exit $?" echo "--- LC_ALL=C" LC_ALL=C LANG=C "$mk" -VERSION || echo "failed, exit $?" - name: Build Windows installers + Linux AppImage shell: pwsh env: TAG: ${{ steps.vars.outputs.tag }} # makensis's argv conversion aborts with ERR_ELECTRON_BUILDER_CANNOT_EXECUTE # / "main argv conversion failed!" under the C/POSIX locale that a # minimal Debian box defaults to. en_US.UTF-8 is generated above. LANG: en_US.UTF-8 LC_ALL: en_US.UTF-8 run: ./scripts/build-release.ps1 -Targets win,linux-appimage -Tag $env:TAG # Plain docker rather than docker/login-action + setup-buildx-action + # build-push-action. This is a single-platform build of one Dockerfile, which # dockerd's own integrated BuildKit does; the actions' only added value here # would be the docker-container driver, and standing that up means pulling # moby/buildkit and mounting the socket into it for no gain. # # --password-stdin, never -p: an argument is visible in ps and in the daemon's # process list for as long as the login runs. - name: Build and push image env: TAG: ${{ steps.vars.outputs.tag }} REGISTRY_USER: ${{ secrets.BUILD_USER }} REGISTRY_TOKEN: ${{ secrets.BUILD_TOKEN }} run: | set -euo pipefail printf '%s' "$REGISTRY_TOKEN" \ | docker login git.azuze.fr -u "$REGISTRY_USER" --password-stdin docker build -t "$IMAGE:latest" -t "$IMAGE:$TAG" . docker push "$IMAGE:latest" docker push "$IMAGE:$TAG" # This runner is a persistent host, so the credentials written by docker login # would otherwise sit in ~/.docker/config.json between jobs. always(): a failed # build is exactly when they must not be left behind. - name: Log out of git.azuze.fr if: always() run: docker logout git.azuze.fr || true - name: Publish Gitea release env: TAG: ${{ steps.vars.outputs.tag }} GITEA_TOKEN: ${{ secrets.BUILD_TOKEN }} SERVER_URL: ${{ github.server_url }} REPO_PATH: ${{ github.repository }} run: | set -euo pipefail api="$SERVER_URL/api/v1/repos/$REPO_PATH/releases" auth=(-H "Authorization: token $GITEA_TOKEN") # Fail here rather than on an opaque 401 four curls later. [ -n "${GITEA_TOKEN:-}" ] || { echo "the BUILD_TOKEN secret is empty" >&2; exit 1; } # The tag before $TAG, for the compare link. $TAG is injected into the list # before sorting because it need not be a tag that exists: a # workflow_dispatch run publishes a release for a tag nobody has pushed, # and Gitea creates it. Without the injection grep matches nothing, and # under `set -e -o pipefail` that took the whole step down before the first # echo. `sort -uV` also puts v2.9 before v2.10, which -v:refname alone on a # list missing $TAG cannot help with. prev_tag=$( { git tag; echo "$TAG"; } | sort -uV | grep -B1 -x -F "$TAG" | head -n1 ) if [ "$prev_tag" = "$TAG" ]; then prev_tag=""; fi if [ -n "$prev_tag" ]; then body="**Full Changelog**: $SERVER_URL/$REPO_PATH/compare/$prev_tag...$TAG" else body="**Full Changelog**: first release" fi echo "changelog: $body" status=$(curl -s -o /tmp/release.json -w '%{http_code}' "${auth[@]}" "$api/tags/$TAG") if [ "$status" = "200" ]; then release_id=$(jq -r '.id' /tmp/release.json) echo "reusing release $TAG (id $release_id)" elif [ "$status" = "404" ]; then echo "creating release $TAG" payload=$(jq -n --arg tag "$TAG" --arg name "Motionity $TAG" --arg body "$body" \ '{tag_name:$tag, name:$name, body:$body, draft:false}') status=$(curl -s -o /tmp/release.json -w '%{http_code}' -X POST "${auth[@]}" \ -H "Content-Type: application/json" -d "$payload" "$api") [ "$status" = "201" ] || { echo "release creation failed ($status):"; cat /tmp/release.json; exit 1; } release_id=$(jq -r '.id' /tmp/release.json) else echo "unexpected status $status fetching release:"; cat /tmp/release.json; exit 1 fi # Keep the changelog link authoritative even when reusing an existing # release (a manual re-run after a later tag was pushed). curl -sf -X PATCH "${auth[@]}" -H "Content-Type: application/json" \ -d "$(jq -n --arg body "$body" '{body:$body}')" \ "$api/$release_id" -o /dev/null for f in "dist/motionity-$TAG-win-x64-setup.exe" \ "dist/motionity-$TAG-win-x64-portable.exe" \ "dist/motionity-$TAG-linux-x86_64.AppImage" \ "dist/SHA256SUMS.txt"; do [ -f "$f" ] || { echo "expected artifact missing: $f" >&2; exit 1; } name=$(basename "$f") existing_id=$(jq -r --arg n "$name" '.assets[]? | select(.name==$n) | .id' /tmp/release.json) if [ -n "$existing_id" ]; then echo "replacing attachment $name (asset $existing_id)" curl -sf -X DELETE "${auth[@]}" "$api/$release_id/assets/$existing_id" -o /dev/null else echo "adding attachment $name" fi encoded=$(jq -rn --arg n "$name" '$n|@uri') curl -sf "${auth[@]}" -F "attachment=@$f" "$api/$release_id/assets?name=$encoded" -o /dev/null done echo "release: $SERVER_URL/$REPO_PATH/releases/tag/$TAG"