# 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: 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.GITEA_PACKAGE_TOKEN }} run: | set -euo pipefail if [ -z "${GITEA_PUBLISH_TOKEN}" ]; then echo "::error::GITEA_PACKAGE_TOKEN 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 curl --fail --user "xavierk:${GITEA_PUBLISH_TOKEN}" -X PUT \ -T "dist/${DEB}" \ "https://git.bongbetic.com/api/packages/xavierk/debian/pool/${CODENAME}/main/upload" done - name: Upload RPM to registry env: GITEA_PUBLISH_TOKEN: ${{ secrets.GITEA_PACKAGE_TOKEN }} run: | set -euo pipefail VERSION=${{ steps.version.outputs.version }} RPM="fenris-${VERSION}-1.x86_64.rpm" curl --fail --user "xavierk:${GITEA_PUBLISH_TOKEN}" -X PUT \ -T "dist/${RPM}" \ "https://git.bongbetic.com/api/packages/xavierk/rpm/fenris/upload" - name: Create Gitea release env: GITEA_PUBLISH_TOKEN: ${{ secrets.GITEA_PACKAGE_TOKEN }} run: | VERSION=${{ steps.version.outputs.version }} # Check if release already exists (idempotent re-runs) EXISTING=$(curl -s -o /dev/null -w '%{http_code}' \ -H "Authorization: token ${GITEA_PUBLISH_TOKEN}" \ "https://git.bongbetic.com/api/v1/repos/xavierk/Fenris/releases/tags/v${VERSION}") if [ "$EXISTING" = "200" ]; then echo "Release v${VERSION} already exists, skipping creation" else curl --fail -X POST \ -H "Authorization: token ${GITEA_PUBLISH_TOKEN}" \ -H "Content-Type: application/json" \ -d "{\"tag_name\":\"v${VERSION}\",\"name\":\"v${VERSION}\"}" \ "https://git.bongbetic.com/api/v1/repos/xavierk/Fenris/releases" fi - name: Attach artifacts to release env: GITEA_PUBLISH_TOKEN: ${{ secrets.GITEA_PACKAGE_TOKEN }} run: | VERSION=${{ steps.version.outputs.version }} # Get release ID for this tag RELEASE_ID=$(curl -s \ -H "Authorization: token ${GITEA_PUBLISH_TOKEN}" \ "https://git.bongbetic.com/api/v1/repos/xavierk/Fenris/releases/tags/v${VERSION}" \ | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])") # Attach deb, rpm, and clearsigned checksums for FILE in "dist/fenris_${VERSION}_amd64.deb" \ "dist/fenris-${VERSION}-1.x86_64.rpm" \ "dist/SHA256SUMS.asc"; do curl --fail -X POST \ -H "Authorization: token ${GITEA_PUBLISH_TOKEN}" \ -F "attachment=@${FILE}" \ "https://git.bongbetic.com/api/v1/repos/xavierk/Fenris/releases/${RELEASE_ID}/assets" done - name: Upload build artifacts uses: actions/upload-artifact@v4 with: name: fenris-packages path: dist/