# Fenris release workflow — release path on Coolify-hosted Gitea runner. # The runner is repository-scoped and executes package build, signing, validation, # registry publication, and release attachment. Spec: §5, issue #52 name: Release on: push: tags: - 'v*' workflow_dispatch: # Built-in Gitea token needs write access for release assets and package registry. permissions: contents: read releases: write packages: write jobs: release: runs-on: [self-hosted] steps: - uses: actions/checkout@v4 - name: Validate release tag and notes run: | set -euo pipefail VERSION="$(sed -n 's/^version = "\(.*\)"/\1/p' pyproject.toml)" if [ -z "${VERSION}" ]; then echo "::error::could not determine the project version" exit 1 fi if [ "${GITHUB_EVENT_NAME}" != "workflow_dispatch" ]; then EXPECTED_TAG="v${VERSION}" ACTUAL_TAG="${GITHUB_REF#refs/tags/}" if [ "${ACTUAL_TAG}" != "${EXPECTED_TAG}" ]; then echo "::error::tag ${ACTUAL_TAG} does not match ${EXPECTED_TAG}" exit 1 fi fi python3 scripts/extract_changelog.py CHANGELOG.md "${VERSION}" \ --footer packaging/release-footer.md > "${RUNNER_TEMP}/release-body.md" - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.12' - name: Install build dependencies run: | sudo apt-get update sudo apt-get install -y gnupg2 rpm python3-venv python3 -m venv /tmp/fenris-ci /tmp/fenris-ci/bin/pip install --quiet build echo "/tmp/fenris-ci/bin" >> "$GITHUB_PATH" NFPM_VERSION=2.47.0 curl --fail --silent --show-error --location \ "https://github.com/goreleaser/nfpm/releases/download/v${NFPM_VERSION}/nfpm_${NFPM_VERSION}_Linux_x86_64.tar.gz" \ -o /tmp/nfpm.tar.gz sudo tar -xzf /tmp/nfpm.tar.gz -C /usr/local/bin nfpm nfpm --version - name: Build packages run: make package - name: Import packaging key env: GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} run: | set -euo pipefail if [ -z "${GPG_PRIVATE_KEY}" ]; then echo "::error::GPG_PRIVATE_KEY repository secret is not configured" exit 1 fi printf '%s\n' "${GPG_PRIVATE_KEY}" | gpg --batch --import SECRET_FINGERPRINT="$(gpg --batch --list-secret-keys --with-colons 'packaging@bongbetic.com' | awk -F: '$1 == "fpr" { print $10; exit }')" PUBLIC_FINGERPRINT="$(gpg --batch --show-keys --with-colons packaging/keys/fenris-packaging.asc | awk -F: '$1 == "fpr" { print $10; exit }')" if [ -z "${PUBLIC_FINGERPRINT}" ]; then echo "::error::packaging/keys/fenris-packaging.asc has no OpenPGP key" exit 1 fi if [ "${SECRET_FINGERPRINT}" != "${PUBLIC_FINGERPRINT}" ]; then echo "::error::packaging public key does not match imported private key" exit 1 fi echo "Packaging key fingerprint verified: ${PUBLIC_FINGERPRINT}" - name: Sign RPM payload run: make sign-rpm - name: Generate and clearsign SHA256SUMS run: | set -euo pipefail VERSION="$(sed -n 's/^version = "\(.*\)"/\1/p' pyproject.toml)" cd dist sha256sum "fenris_${VERSION}_amd64.deb" \ "fenris-${VERSION}-1.x86_64.rpm" > SHA256SUMS gpg --batch --yes --clearsign --local-user packaging@bongbetic.com SHA256SUMS - name: Validate signatures and checksums run: | set -euo pipefail VERSION="$(sed -n 's/^version = "\(.*\)"/\1/p' pyproject.toml)" RPM="fenris-${VERSION}-1.x86_64.rpm" RPM_VERIFY="$(rpm -Kv "dist/${RPM}" 2>&1)" printf '%s\n' "${RPM_VERIFY}" printf '%s\n' "${RPM_VERIFY}" | grep -Eiq 'signature.*: *ok' gpg --batch --verify dist/SHA256SUMS.asc (cd dist && sha256sum -c SHA256SUMS) - name: Remove packaging key material if: always() run: | set +e FINGERPRINT="$(gpg --batch --list-secret-keys --with-colons 'packaging@bongbetic.com' 2>/dev/null | awk -F: '$1 == "fpr" { print $10; exit }')" if [ -n "${FINGERPRINT}" ]; then gpg --batch --yes --delete-secret-keys "${FINGERPRINT}" gpg --batch --yes --delete-keys "${FINGERPRINT}" fi - name: Determine version id: version run: echo "version=$(sed -n 's/^version = "\(.*\)"/\1/p' pyproject.toml)" >> "$GITHUB_OUTPUT" - name: Upload deb packages to registry env: GITEA_PUBLISH_TOKEN: ${{ secrets.GITEAPACKAGETOKEN }} run: | set -euo pipefail if [ -z "${GITEA_PUBLISH_TOKEN}" ]; then echo "::error::GITEAPACKAGETOKEN repository secret is not configured" exit 1 fi VERSION=${{ steps.version.outputs.version }} DEB="fenris_${VERSION}_amd64.deb" for CODENAME in bookworm jammy noble; do STATUS=$(curl --silent --show-error --user "xavierk:${GITEA_PUBLISH_TOKEN}" -X PUT \ -T "dist/${DEB}" -o /dev/null -w '%{http_code}' \ "https://git.bongbetic.com/api/packages/xavierk/debian/pool/${CODENAME}/main/upload" || true) case "${STATUS}" in 200|201|204) echo "Debian ${CODENAME}: uploaded" ;; 409) echo "Debian ${CODENAME}: already exists, kept existing package" ;; *) echo "::error::Debian ${CODENAME} upload failed with HTTP ${STATUS}"; exit 1 ;; esac done - name: Upload RPM to registry env: GITEA_PUBLISH_TOKEN: ${{ secrets.GITEAPACKAGETOKEN }} run: | set -euo pipefail VERSION=${{ steps.version.outputs.version }} RPM="fenris-${VERSION}-1.x86_64.rpm" STATUS=$(curl --silent --show-error --user "xavierk:${GITEA_PUBLISH_TOKEN}" -X PUT \ -T "dist/${RPM}" -o /dev/null -w '%{http_code}' \ "https://git.bongbetic.com/api/packages/xavierk/rpm/fenris/upload" || true) case "${STATUS}" in 200|201|204) echo "RPM: uploaded" ;; 409) echo "RPM: already exists, kept existing package" ;; *) echo "::error::RPM upload failed with HTTP ${STATUS}"; exit 1 ;; esac - name: Create Gitea release env: GITEA_PUBLISH_TOKEN: ${{ secrets.GITEAPACKAGETOKEN }} run: | set -euo pipefail if [ -z "${GITEA_PUBLISH_TOKEN}" ]; then echo "::error::GITEAPACKAGETOKEN repository secret is not configured" exit 1 fi VERSION=${{ steps.version.outputs.version }} RELEASE_BODY="${RUNNER_TEMP}/release-body.md" if [ ! -s "${RELEASE_BODY}" ]; then echo "::error::validated release body is missing or empty" exit 1 fi EXISTING_RELEASE="${RUNNER_TEMP}/existing-release.json" EXISTING=$(curl --silent --show-error -o "${EXISTING_RELEASE}" -w '%{http_code}' \ -H "Authorization: token ${GITEA_PUBLISH_TOKEN}" \ "https://git.bongbetic.com/api/v1/repos/xavierk/Fenris/releases/tags/v${VERSION}" || true) case "${EXISTING}" in 200) echo "Release v${VERSION} exists; resynchronizing its notes" REQUEST="$(python3 scripts/release_request.py --version "${VERSION}" \ --body-file "${RELEASE_BODY}" --existing-release "${EXISTING_RELEASE}")" ;; 404) REQUEST="$(python3 scripts/release_request.py --version "${VERSION}" \ --body-file "${RELEASE_BODY}")" ;; *) echo "::error::release lookup failed with HTTP ${EXISTING}" exit 1 ;; esac METHOD="$(printf '%s' "${REQUEST}" | python3 -c "import json,sys; print(json.load(sys.stdin)['method'])")" PATH="$(printf '%s' "${REQUEST}" | python3 -c "import json,sys; print(json.load(sys.stdin)['path'])")" PAYLOAD="$(printf '%s' "${REQUEST}" | python3 -c "import json,sys; print(json.dumps(json.load(sys.stdin)['payload']))")" curl --fail --silent --show-error -X "${METHOD}" \ -H "Authorization: token ${GITEA_PUBLISH_TOKEN}" \ -H "Content-Type: application/json" \ -d "${PAYLOAD}" \ "https://git.bongbetic.com/api/v1/repos/xavierk/Fenris${PATH}" - name: Attach artifacts to release env: GITEA_PUBLISH_TOKEN: ${{ secrets.GITEAPACKAGETOKEN }} run: | set -euo pipefail VERSION=${{ steps.version.outputs.version }} # Get release ID for this tag RELEASE_JSON=$(curl --fail --silent --show-error \ -H "Authorization: token ${GITEA_PUBLISH_TOKEN}" \ "https://git.bongbetic.com/api/v1/repos/xavierk/Fenris/releases/tags/v${VERSION}") RELEASE_ID=$(printf '%s' "${RELEASE_JSON}" \ | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])") # Attach deb, rpm, and clearsigned checksums once. for FILE in "dist/fenris_${VERSION}_amd64.deb" \ "dist/fenris-${VERSION}-1.x86_64.rpm" \ "dist/SHA256SUMS.asc"; do ASSET_NAME="${FILE##*/}" if python3 -c 'import json,sys; name=sys.argv[1]; sys.exit(0 if any(a.get("name") == name for a in json.load(sys.stdin).get("assets", [])) else 1)' "${ASSET_NAME}" <<<"${RELEASE_JSON}"; then echo "${ASSET_NAME}: already attached" else curl --fail --silent --show-error -X POST \ -H "Authorization: token ${GITEA_PUBLISH_TOKEN}" \ -F "attachment=@${FILE}" \ "https://git.bongbetic.com/api/v1/repos/xavierk/Fenris/releases/${RELEASE_ID}/assets" fi done