Files
Fenris/.gitea/workflows/release.yml
T
xavierk 1c3037c2f8 Implement independent format gates for release workflow (issue #86)
Add XBPS build and sign steps to CI workflow
Add XBPS publication as independent gate (requires manual trigger)
Track format availability in release notes
Update release-footer.md with XBPS install instructions
Add --available/--withheld arguments to extract_changelog.py
Attach XBPS artifacts to Gitea release
Clean up XBPS signing key material after use
2026-09-15 10:50:38 +05:30

332 lines
14 KiB
YAML

# 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:
inputs:
publish_xbps:
description: 'Publish XBPS package to distribution repository (requires host acceptance)'
required: false
default: false
type: boolean
# 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: Build XBPS package
run: make package-xbps
- 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: Import XBPS signing key
id: import-xbps-key
env:
XBPS_SIGNING_KEY: ${{ secrets.XBPS_SIGNING_KEY }}
run: |
set -euo pipefail
if [ -z "${XBPS_SIGNING_KEY}" ]; then
echo "::warning::XBPS_SIGNING_KEY secret not configured; XBPS signing skipped"
echo "xbps_signed=false" >> "$GITHUB_OUTPUT"
exit 0
fi
mkdir -p ~/.ssh
printf '%s\n' "${XBPS_SIGNING_KEY}" > ~/.ssh/id_xbps
chmod 600 ~/.ssh/id_xbps
echo "xbps_signed=true" >> "$GITHUB_OUTPUT"
- name: Sign XBPS package
if: steps.import-xbps-key.outputs.xbps_signed == 'true'
run: make sign-xbps
- 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
rm -f ~/.ssh/id_xbps
- 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: Publish XBPS to distribution repository
if: github.event.inputs.publish_xbps == 'true'
run: |
set -euo pipefail
VERSION=${{ steps.version.outputs.version }}
XBPS_FILE="fenris-${VERSION}_1.x86_64.xbps"
if [ ! -f "${XBPS_FILE}" ]; then
echo "::error::XBPS package not found: ${XBPS_FILE}"
exit 1
fi
if [ ! -f "${XBPS_FILE}.sig2" ]; then
echo "::error::XBPS signature not found: ${XBPS_FILE}.sig2"
exit 1
fi
bash scripts/xbps-publish.sh --publish
- name: Track format availability
id: formats
run: |
set -euo pipefail
VERSION=${{ steps.version.outputs.version }}
DEB_EXISTS=$([ -f "dist/fenris_${VERSION}_amd64.deb" ] && echo "true" || echo "false")
RPM_EXISTS=$([ -f "dist/fenris-${VERSION}-1.x86_64.rpm" ] && echo "true" || echo "false")
XBPS_EXISTS=$([ -f "fenris-${VERSION}_1.x86_64.xbps" ] && echo "true" || echo "false")
XBPS_PUBLISHED=$([ "${{ github.event.inputs.publish_xbps }}" = "true" ] && echo "true" || echo "false")
echo "deb_available=${DEB_EXISTS}" >> "$GITHUB_OUTPUT"
echo "rpm_available=${RPM_EXISTS}" >> "$GITHUB_OUTPUT"
echo "xbps_available=${XBPS_EXISTS}" >> "$GITHUB_OUTPUT"
echo "xbps_published=${XBPS_PUBLISHED}" >> "$GITHUB_OUTPUT"
# Build format availability summary for release notes
AVAILABLE_FORMATS=""
WITHHELD_FORMATS=""
if [ "${DEB_EXISTS}" = "true" ]; then
AVAILABLE_FORMATS="${AVAILABLE_FORMATS}Debian/Ubuntu (deb), "
fi
if [ "${RPM_EXISTS}" = "true" ]; then
AVAILABLE_FORMATS="${AVAILABLE_FORMATS}Fedora/openSUSE (rpm), "
fi
if [ "${XBPS_EXISTS}" = "true" ] && [ "${XBPS_PUBLISHED}" = "true" ]; then
AVAILABLE_FORMATS="${AVAILABLE_FORMATS}Void Linux (xbps)"
elif [ "${XBPS_EXISTS}" = "true" ]; then
WITHHELD_FORMATS="Void Linux (xbps) — pending host acceptance"
fi
# Remove trailing comma and space
AVAILABLE_FORMATS=$(echo "${AVAILABLE_FORMATS}" | sed 's/, $//')
echo "available_formats=${AVAILABLE_FORMATS}" >> "$GITHUB_OUTPUT"
echo "withheld_formats=${WITHHELD_FORMATS}" >> "$GITHUB_OUTPUT"
- 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
# Append format availability to release notes
AVAILABLE_FORMATS="${{ steps.formats.outputs.available_formats }}"
WITHHELD_FORMATS="${{ steps.formats.outputs.withheld_formats }}"
RELEASE_BODY_WITH_FORMATS="${RUNNER_TEMP}/release-body-formats.md"
cp "${RELEASE_BODY}" "${RELEASE_BODY_WITH_FORMATS}"
echo "" >> "${RELEASE_BODY_WITH_FORMATS}"
echo "## Package formats" >> "${RELEASE_BODY_WITH_FORMATS}"
echo "" >> "${RELEASE_BODY_WITH_FORMATS}"
echo "Available: ${AVAILABLE_FORMATS}" >> "${RELEASE_BODY_WITH_FORMATS}"
if [ -n "${WITHHELD_FORMATS}" ]; then
echo "Withheld: ${WITHHELD_FORMATS}" >> "${RELEASE_BODY_WITH_FORMATS}"
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_WITH_FORMATS}" --existing-release "${EXISTING_RELEASE}")"
;;
404)
REQUEST="$(python3 scripts/release_request.py --version "${VERSION}" \
--body-file "${RELEASE_BODY_WITH_FORMATS}")"
;;
*)
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'])")"
RELEASE_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${RELEASE_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, clearsigned checksums, and XBPS artifacts once.
ARTIFACTS=(
"dist/fenris_${VERSION}_amd64.deb"
"dist/fenris-${VERSION}-1.x86_64.rpm"
"dist/SHA256SUMS.asc"
)
# Add XBPS artifacts if they exist
XBPS_FILE="fenris-${VERSION}_1.x86_64.xbps"
if [ -f "${XBPS_FILE}" ]; then
ARTIFACTS+=("${XBPS_FILE}")
if [ -f "${XBPS_FILE}.sig2" ]; then
ARTIFACTS+=("${XBPS_FILE}.sig2")
fi
fi
for FILE in "${ARTIFACTS[@]}"; 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