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 # wails.json, commits the bump, creates an annotated tag carrying the # git-cliff notes, and pushes commit + tag. This workflow verifies # wails.json 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 wails.json 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%%-*}" WAILS_VER="$(python3 -c "import json; print(json.load(open('wails.json'))['info']['productVersion'])")" if [ "${NORM}" = "${BASE}" ]; then if [ "${WAILS_VER}" != "${NORM}" ]; then echo "::error::wails.json productVersion (${WAILS_VER}) != tag (${NORM}). Bump via ./release ${NORM} first." >&2 exit 1 fi else # Pre-release (e.g. 1.6.7-rc1): wails.json must match the base version. if [ "${WAILS_VER}" != "${BASE}" ]; then echo "::error::wails.json productVersion (${WAILS_VER}) != tag base (${BASE}). Bump via ./release ${BASE} first." >&2 exit 1 fi fi echo "VERSION=${NORM}" >> "${GITHUB_ENV}" echo "Version guard passed: wails.json=${WAILS_VER} tag=${NORM}" - name: Set up Go uses: actions/setup-go@v5 with: go-version: '1.25' cache-dependency-path: go.sum - name: Set up Node uses: actions/setup-node@v4 with: node-version: '20' - name: Install Wails Linux build dependencies run: | set -euo pipefail sudo apt-get update sudo apt-get install -y \ build-essential pkg-config \ libgtk-3-dev libwebkit2gtk-4.1-dev \ jq - name: Install Wails CLI run: | set -euo pipefail go install github.com/wailsapp/wails/v2/cmd/wails@v2.15.0 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: Upload release archive 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}" test -f "${ARCHIVE}" || { echo "::error::${ARCHIVE} 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. AID="$(curl -sS -H "${AUTH}" "${API}/${RID}/assets" | jq -r --arg n "${ARCHIVE}" '.[] | 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} (${ARCHIVE})" fi resp="$(curl -sS -w '\n%{http_code}' -X POST -H "${AUTH}" \ -F "attachment=@${ARCHIVE}" "${API}/${RID}/assets?name=${ARCHIVE}")" 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 ${ARCHIVE} to release id=${RID}"