name: Release # Overrides the default run name (the tagged commit's message) so the Actions # runs list shows "Release 1.6.7" instead. run-name: "Release ${{ gitea.event.inputs.tag || gitea.ref_name }}" # Builds the Wails Linux binary via `make build`, packages # AniTrack-.tar.gz from build/, and creates/updates a Gitea Release # named AniTrack- with the archive attached. Triggered by a plain # version tag push (1.6.7), or manually via workflow_dispatch with a tag for # re-runs and secrets tests. Pushing to main does nothing, so # work-in-progress commits never ship. # # Local flow: `./release 1.6.7` (or `make release VERSION=1.6.7`) bumps # build/config.yml (and the generated version.go), commits the bump, # creates an annotated tag carrying the git-cliff notes, and pushes # commit + tag. This workflow verifies build/config.yml matches the tag # before building. # # Secrets test (verifies environment.go wiring without shipping): tag the # current bumped commit with a suffix and push, e.g. # git tag 1.6.7-rc1 && git push origin 1.6.7-rc1 # Tags containing '-' are published as pre-releases; delete the test # Release/tag afterwards. on: push: tags: - '[0-9]*.[0-9]*.[0-9]*' workflow_dispatch: inputs: tag: description: 'Tag to release (e.g. 1.6.7)' required: true type: string jobs: release: runs-on: ubuntu-latest permissions: contents: read env: # Resolve the target tag for both triggers: explicit input on manual # dispatch, otherwise the pushed tag ref. TAG: ${{ gitea.event.inputs.tag || gitea.ref_name }} steps: - name: Checkout uses: actions/checkout@v4 with: # Full history ensures the tag annotation (the release notes) is present. fetch-depth: 0 ref: ${{ gitea.event.inputs.tag || gitea.ref }} - name: Guard build/config.yml matches tag run: | set -euo pipefail : "${TAG:?TAG is required}" # Normalize "v1.6.7" / "AniTrack-1.6.7" to plain "1.6.7" (make release # only ever creates plain tags; this tolerates manual typos). NORM="${TAG#v}" NORM="${NORM#AniTrack-}" BASE="${NORM%%-*}" CFG_VER="$(python3 -c "import re; print(re.search(r'^ version: \"([^\"]*)\"', open('build/config.yml').read(), flags=re.M).group(1))")" if [ "${NORM}" = "${BASE}" ]; then if [ "${CFG_VER}" != "${NORM}" ]; then echo "::error::build/config.yml info.version (${CFG_VER}) != tag (${NORM}). Bump via ./release ${NORM} first." >&2 exit 1 fi else # Pre-release (e.g. 1.6.7-rc1): build/config.yml must match the base version. if [ "${CFG_VER}" != "${BASE}" ]; then echo "::error::build/config.yml info.version (${CFG_VER}) != tag base (${BASE}). Bump via ./release ${BASE} first." >&2 exit 1 fi fi echo "VERSION=${NORM}" >> "${GITHUB_ENV}" echo "Version guard passed: build/config.yml=${CFG_VER} tag=${NORM}" - name: Set up Go uses: actions/setup-go@v5 with: go-version: '1.25' cache-dependency-path: go.sum - name: Cache Go build cache uses: actions/cache@v4 with: # setup-go only caches modules (GOMODCACHE). This caches compiled # packages (GOCACHE) so the cgo/WebKit compile goes incremental. # restore-keys gives a close cache instead of a cold one when # go.sum changes. First run after adding this still compiles cold # (it saves); the payoff shows from the second run on. path: ~/.cache/go-build key: go-build-1.25-${{ runner.os }}-${{ hashFiles('go.sum') }} restore-keys: | go-build-1.25-${{ runner.os }}- - name: Set up Node uses: actions/setup-node@v4 with: node-version: '20' # Caches npm's download cache keyed on the frontend lockfile, so # the `npm install` inside `wails build` stops fetching cold. cache: 'npm' cache-dependency-path: frontend/package-lock.json - name: Install Wails Linux build dependencies run: | set -euo pipefail sudo apt-get update # v3 desktop defaults to the GTK4/WebKitGTK 6.0 stack. sudo apt-get install -y \ build-essential pkg-config \ libgtk-4-dev libwebkitgtk-6.0-dev \ jq - name: Cache Wails CLI uses: actions/cache@v4 id: wails-cli with: # The compiled CLI binary. Bump the key whenever the @version pin # below changes, or the old CLI will be silently reused. path: ~/go/bin/wails3 key: wails3-v3.0.0-beta.22-${{ runner.os }} - name: Install Wails CLI if: steps.wails-cli.outputs.cache-hit != 'true' run: | set -euo pipefail go install github.com/wailsapp/wails/v3/cmd/wails3@v3.0.0-beta.22 - name: Add Go bin to PATH run: echo "${HOME}/go/bin" >> "${GITHUB_PATH}" - name: Write environment.go from secrets env: ANILIST_SECRET_TOKEN: ${{ secrets.ANILIST_SECRET_TOKEN }} ANILIST_APP_ID: ${{ secrets.ANILIST_APP_ID }} ANILIST_APP_NAME: ${{ secrets.ANILIST_APP_NAME }} ANILIST_CALLBACK_URI: ${{ secrets.ANILIST_CALLBACK_URI }} SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }} SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }} SIMKL_CALLBACK_URI: ${{ secrets.SIMKL_CALLBACK_URI }} MAL_CLIENT_ID: ${{ secrets.MAL_CLIENT_ID }} MAL_CLIENT_SECRET: ${{ secrets.MAL_CLIENT_SECRET }} MAL_CALLBACK_URI: ${{ secrets.MAL_CALLBACK_URI }} run: | set -euo pipefail # All 10 fields come from Gitea Actions secrets (never committed). # Values are masked in logs; do not echo them or run with set -x. for v in ANILIST_SECRET_TOKEN ANILIST_APP_ID ANILIST_APP_NAME ANILIST_CALLBACK_URI SIMKL_CLIENT_ID SIMKL_CLIENT_SECRET SIMKL_CALLBACK_URI MAL_CLIENT_ID MAL_CLIENT_SECRET MAL_CALLBACK_URI; do if [ -z "${!v:-}" ]; then echo "::error::Missing secret ${v}. Add it under Settings → Secrets → Actions." >&2 exit 1 fi done cat > environment.go <&2; exit 1; } - name: Package release archive run: | set -euo pipefail : "${VERSION:?VERSION missing from version-guard step}" STAGE="dist/AniTrack-${VERSION}" rm -rf dist "AniTrack-${VERSION}.tar.gz" mkdir -p "${STAGE}/bin" cp build/bin/AniTrack "${STAGE}/bin/" cp -r build/icon "${STAGE}/" cp build/AniTrack.desktop build/install_linux.sh build/README.md "${STAGE}/" chmod +x "${STAGE}/bin/AniTrack" "${STAGE}/install_linux.sh" tar -czf "AniTrack-${VERSION}.tar.gz" -C dist "AniTrack-${VERSION}" tar tzf "AniTrack-${VERSION}.tar.gz" echo "ARCHIVE=AniTrack-${VERSION}.tar.gz" >> "${GITHUB_ENV}" - name: Create Gitea Release env: # REGISTRY_TOKEN is reused for release creation because Gitea's auto # token cannot create releases on this instance. The PAT must carry # write:repository scope. Idempotent: re-runs update an existing # release for this tag instead of failing with 409. On any HTTP error # the API response body is printed so a 403 names the missing scope. TOKEN: ${{ secrets.REGISTRY_TOKEN }} REPO: ${{ gitea.repository }} run: | set -euo pipefail : "${TAG:?TAG is required}" : "${VERSION:?VERSION missing from version-guard step}" API="https://git.linuxhg.com/api/v1/repos/${REPO}/releases" AUTH="Authorization: token ${TOKEN}" # Release body = the annotated tag's message (the git-cliff notes). # Manual test tags without an annotation fall back to the tag name. BODY="$(git tag -l --format='%(contents)' "${TAG}")" if [ -z "$(printf '%s' "${BODY}" | tr -d '[:space:]')" ]; then BODY="${TAG}" fi # Tags containing a '-' (e.g. 1.6.7-rc1) are published as pre-releases. PRE="false"; case "${TAG}" in *-*) PRE="true";; esac PAYLOAD=$(jq -n \ --arg t "${TAG}" --arg n "AniTrack-${VERSION}" --arg b "${BODY}" --argjson p "${PRE}" \ '{tag_name:$t, name:$n, body:$b, draft:false, prerelease:$p}') # POST/PATCH the release, surfacing Gitea's error message on failure # (e.g. "token does not have write scope") instead of failing silently. api_call() { local method="$1" url="$2" resp code rbody resp="$(curl -sS -w '\n%{http_code}' -X "${method}" \ -H "${AUTH}" -H "Content-Type: application/json" \ -d "${PAYLOAD}" "${url}")" code="$(printf '%s' "${resp}" | tail -n1)" rbody="$(printf '%s' "${resp}" | sed '$d')" if [ "${code}" -ge 400 ]; then echo "::error::Release API ${code} (${method} ${url}): ${rbody}" >&2 return 1 fi } EXISTING_ID="$(curl -sS -H "${AUTH}" "${API}/tags/${TAG}" | jq -r '.id // empty' 2>/dev/null || true)" if [ -n "${EXISTING_ID}" ]; then api_call PATCH "${API}/${EXISTING_ID}" echo "Updated existing release id=${EXISTING_ID} for ${TAG}" else api_call POST "${API}" echo "Created new release for ${TAG}" fi - name: Build updater asset and sign env: UPDATER_SIGNING_KEY: ${{ secrets.UPDATER_SIGNING_KEY }} run: | set -euo pipefail : "${VERSION:?VERSION missing from version-guard step}" export PATH="${HOME}/go/bin:${PATH}" : "${UPDATER_SIGNING_KEY:?Missing secret UPDATER_SIGNING_KEY. Add the updater private key under Settings → Secrets → Actions.}" # Bare updater binary: the updater swaps os.Executable() in place, # so it takes the single binary, not the user tarball (whose # top-level directory would fail the single-entry rule). mkdir -p updater-dist cp build/bin/AniTrack updater-dist/AniTrack-linux-amd64 printf '%s' "${UPDATER_SIGNING_KEY}" > updater-dist/updater.key chmod 600 updater-dist/updater.key wails3 updater sign -key updater-dist/updater.key updater-dist/AniTrack-linux-amd64 > updater-dist/sign.json # Split the sign output into the sidecar assets the Gitea provider # fetches: .sha512 (sha512sum format) + .sig (base64). python3 - <<'EOF' import base64, binascii, json entries = json.load(open('updater-dist/sign.json')) entry = next(e for e in entries if e['filename'].endswith('AniTrack-linux-amd64')) assert entry['digestAlgo'] == 'sha512', entry assert entry['signatureAlgo'] == 'ed25519ph', entry digest_hex = binascii.hexlify(base64.b64decode(entry['digest'])).decode() open('updater-dist/AniTrack-linux-amd64.sha512', 'w').write(f"{digest_hex} AniTrack-linux-amd64\n") open('updater-dist/AniTrack-linux-amd64.sig', 'w').write(entry['signature'].strip() + '\n') EOF shred -u updater-dist/updater.key ls -la updater-dist/ - name: Upload release assets env: TOKEN: ${{ secrets.REGISTRY_TOKEN }} REPO: ${{ gitea.repository }} run: | set -euo pipefail : "${TAG:?TAG is required}" : "${ARCHIVE:?ARCHIVE missing from packaging step}" API="https://git.linuxhg.com/api/v1/repos/${REPO}/releases" AUTH="Authorization: token ${TOKEN}" for ASSET in "${ARCHIVE}" updater-dist/AniTrack-linux-amd64 updater-dist/AniTrack-linux-amd64.sha512 updater-dist/AniTrack-linux-amd64.sig; do test -f "${ASSET}" || { echo "::error::${ASSET} not found" >&2; exit 1; } RID="$(curl -sS -H "${AUTH}" "${API}/tags/${TAG}" | jq -r '.id // empty')" if [ -z "${RID}" ]; then echo "::error::No release found for tag ${TAG} after create step" >&2 exit 1 fi # Replace a same-named asset so re-runs stay idempotent. ANAME="$(basename "${ASSET}")" AID="$(curl -sS -H "${AUTH}" "${API}/${RID}/assets" | jq -r --arg n "${ANAME}" '.[] | select(.name==$n) | .id // empty')" if [ -n "${AID}" ]; then curl -sS -X DELETE -H "${AUTH}" "${API}/${RID}/assets/${AID}" >/dev/null echo "Deleted existing asset id=${AID} (${ANAME})" fi resp="$(curl -sS -w '\n%{http_code}' -X POST -H "${AUTH}" \ -F "attachment=@${ASSET}" "${API}/${RID}/assets?name=${ANAME}")" code="$(printf '%s' "${resp}" | tail -n1)" rbody="$(printf '%s' "${resp}" | sed '$d')" if [ "${code}" -ge 400 ]; then echo "::error::Asset upload ${code}: ${rbody}" >&2 exit 1 fi echo "Uploaded ${ANAME} to release id=${RID}" done