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
332 lines
14 KiB
YAML
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
|