diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..d2b6939 --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,112 @@ +name: Release + +# Overrides the default run name (the tagged commit's message) so the Actions +# runs list shows "Release v0.1.0" instead. +run-name: "Release ${{ gitea.event.inputs.tag || gitea.ref_name }}" + +# Publishes the games-api container image to the Gitea container registry AND +# creates a Gitea Release whose body is the annotated tag's message (generated +# locally by `make release VERSION=...` via git-cliff). Triggered by a version +# tag push, or manually via workflow_dispatch with a tag. Pushing to main does +# nothing, so work-in-progress commits never ship. Each release publishes two +# image tags: the version (e.g. v0.1.0) and "latest". +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + tag: + description: 'Tag to release (e.g. v0.1.0)' + required: true + type: string + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + 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: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Gitea Container Registry + uses: docker/login-action@v3 + with: + registry: git.linuxhg.com + username: ${{ gitea.actor }} + # PAT stored as a repo Actions secret (auto GITHUB_TOKEN lacks package scope in Gitea) + password: ${{ secrets.REGISTRY_TOKEN }} + + - name: Build and push image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: true + # Publishes both the exact version (e.g. v0.1.0) and the movable "latest" tag. + # Deployments default to "latest" via ${IMAGE_TAG:-latest} in docker-compose.yml; + # pin or roll back by setting IMAGE_TAG in .env. + tags: | + git.linuxhg.com/games-database/games-api:${{ env.TAG }} + git.linuxhg.com/games-database/games-api:latest + + - name: Create Gitea Release + env: + # REGISTRY_TOKEN is reused for release creation because Gitea's auto + # GITHUB_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}" + 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). + BODY="$(git tag -l --format='%(contents)' "${TAG}")" + + # Tags containing a '-' (e.g. v0.1.0-rc1) are published as pre-releases. + PRE="false"; case "${TAG}" in *-*) PRE="true";; esac + + PAYLOAD=$(jq -n \ + --arg t "${TAG}" --arg n "${TAG}" --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 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c604114 --- /dev/null +++ b/Makefile @@ -0,0 +1,33 @@ +.PHONY: help release + +# Default target +help: + @echo "Available targets:" + @echo "" + @echo "Release:" + @echo " ./release v0.1.0 - Tag, push, and release (notes auto-generated from commits)" + @echo " make release VERSION=v0.1.0 - Same, explicit make form" + +# Create an annotated version tag carrying auto-generated release notes (git-cliff) +# and push it. The tag push triggers .gitea/workflows/release.yml, which builds the +# image and publishes a Gitea Release whose body is this tag's message. Notes come +# entirely from Conventional Commits — no hand-written message required. +# +# git-cliff's --latest needs the tag to exist to scope the notes, so we create a +# throwaway lightweight tag, generate the notes, replace it with an annotated tag, +# then push. --cleanup=verbatim keeps the markdown "###" group headers (git's +# default cleanup would strip lines starting with "#"). +# +# Requires git-cliff: https://git-cliff.org/install +# Usage: make release VERSION=v0.1.0 +release: + @test -n "$(VERSION)" || { echo "Usage: make release VERSION=v0.1.0"; exit 1; } + @command -v git-cliff >/dev/null 2>&1 || { echo "git-cliff not found — install: https://git-cliff.org/install"; exit 1; } + @if git rev-parse "$(VERSION)" >/dev/null 2>&1; then echo "Tag $(VERSION) already exists locally — delete it first: git tag -d $(VERSION)"; exit 1; fi + @echo "Generating release notes for $(VERSION)..." + @git tag "$(VERSION)" HEAD && \ + (git cliff --latest --config cliff.toml > .release-notes.tmp && git tag -d "$(VERSION)" >/dev/null) || \ + { git tag -d "$(VERSION)" >/dev/null 2>&1; rm -f .release-notes.tmp; echo "git-cliff failed"; exit 1; } + @git tag -a --cleanup=verbatim -F .release-notes.tmp "$(VERSION)" HEAD && rm -f .release-notes.tmp + @git push origin "$(VERSION)" + @echo "Pushed $(VERSION) — Gitea Actions will build the image and publish the Release." diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 0000000..0d74f52 --- /dev/null +++ b/cliff.toml @@ -0,0 +1,37 @@ +# git-cliff configuration — generates the body of each Gitea Release from +# Conventional Commits accumulated since the previous tag. Used locally by +# `make release` with --latest so only the current tag's section is emitted +# (no full history, no header — the Gitea Release title is the tag). +# Docs: https://git-cliff.org/docs/configuration + +[changelog] +header = "" +body = """ +{% for group, commits in commits | group_by(attribute="group") %}\ +### {{ group | upper_first }} +{% for commit in commits %}\ +- {% if commit.scope %}*({{ commit.scope }})* {% endif %}{{ commit.message | upper_first }} ({{ commit.id | truncate(length=7, end="") }}) +{% endfor %}\ +{% endfor %}\ +""" +trim = true +footer = "" + +[git] +conventional_commits = true +filter_unconventional = false +require_conventional = false +split_commits = false +commit_parsers = [ + { message = "^feat", group = "Features" }, + { message = "^fix", group = "Bug Fixes" }, + { message = "^perf", group = "Performance" }, + { message = "^refactor", group = "Refactor" }, + { message = "^docs", group = "Documentation" }, + { message = "^test", group = "Tests" }, + { message = "^chore|^ci", group = "Miscellaneous Tasks" }, + { message = ".*", group = "Other" }, +] +filter_commits = false +tag_pattern = "v[0-9].*" +sort_commits = "oldest" diff --git a/release b/release new file mode 100755 index 0000000..365d6d0 --- /dev/null +++ b/release @@ -0,0 +1,15 @@ +#!/usr/bin/env sh +# Project-attached wrapper around `make release` so you can run: +# ./release v0.1.0 (or) ./release 0.1.0 +# instead of: +# make release VERSION=v0.1.0 +# Lives in the repo (no machine-specific alias needed). +set -eu + +[ "$#" -ge 1 ] || { echo "Usage: ./release v0.1.0" >&2; exit 1; } + +# Accept "0.1.0" or "v0.1.0"; ensure the tag starts with 'v' (the workflow +# only triggers on v* tags). +VERSION="v${1#v}" + +exec make release "VERSION=${VERSION}"