The optional XBPS publisher signs repository metadata after package signing. Remove the runner key only after publication and release asset upload.
351 lines
16 KiB
YAML
351 lines
16 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
|
|
# Ubuntu does not package the XBPS build tools. Use Void's static
|
|
# toolchain, pinned and checksum-verified before it reaches PATH.
|
|
XBPS_STATIC_VERSION=0.60.4_1
|
|
XBPS_STATIC_ARCHIVE="xbps-static-static-${XBPS_STATIC_VERSION}.x86_64-musl.tar.xz"
|
|
XBPS_STATIC_SHA256=603b3c55e9cabd5af79b461b929b14e1556a443c97b5714d188681c2172d9e28
|
|
curl --fail --silent --show-error --location \
|
|
"https://repo-default.voidlinux.org/static/${XBPS_STATIC_ARCHIVE}" \
|
|
-o "/tmp/${XBPS_STATIC_ARCHIVE}"
|
|
echo "${XBPS_STATIC_SHA256} /tmp/${XBPS_STATIC_ARCHIVE}" | sha256sum --check --strict
|
|
mkdir -p /tmp/xbps-static
|
|
tar -xJf "/tmp/${XBPS_STATIC_ARCHIVE}" -C /tmp/xbps-static
|
|
export PATH="/tmp/xbps-static/usr/bin:${PATH}"
|
|
echo "/tmp/xbps-static/usr/bin" >> "$GITHUB_PATH"
|
|
xbps-create --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
|
|
|
|
- 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
|
|
if [ -f "${XBPS_FILE}.sig2" ]; then
|
|
ARTIFACTS+=("${XBPS_FILE}")
|
|
ARTIFACTS+=("${XBPS_FILE}.sig2")
|
|
else
|
|
echo "::warning::Unsigned XBPS artifact omitted from release assets"
|
|
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
|
|
|
|
- name: Remove XBPS signing key
|
|
if: always()
|
|
run: rm -f ~/.ssh/id_xbps
|