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 Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
This commit is contained in:
co-authored by
CommandCodeBot
parent
d93238b0f3
commit
2d4cb16a00
@@ -8,6 +8,12 @@ on:
|
||||
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:
|
||||
@@ -62,6 +68,9 @@ jobs:
|
||||
- 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 }}
|
||||
@@ -87,6 +96,26 @@ jobs:
|
||||
- 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
|
||||
@@ -116,6 +145,7 @@ jobs:
|
||||
gpg --batch --yes --delete-secret-keys "${FINGERPRINT}"
|
||||
gpg --batch --yes --delete-keys "${FINGERPRINT}"
|
||||
fi
|
||||
rm -f ~/.ssh/id_xbps
|
||||
|
||||
- name: Determine version
|
||||
id: version
|
||||
@@ -159,6 +189,54 @@ jobs:
|
||||
*) 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 }}
|
||||
@@ -174,6 +252,18 @@ jobs:
|
||||
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}" \
|
||||
@@ -182,11 +272,11 @@ jobs:
|
||||
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}")"
|
||||
--body-file "${RELEASE_BODY_WITH_FORMATS}" --existing-release "${EXISTING_RELEASE}")"
|
||||
;;
|
||||
404)
|
||||
REQUEST="$(python3 scripts/release_request.py --version "${VERSION}" \
|
||||
--body-file "${RELEASE_BODY}")"
|
||||
--body-file "${RELEASE_BODY_WITH_FORMATS}")"
|
||||
;;
|
||||
*)
|
||||
echo "::error::release lookup failed with HTTP ${EXISTING}"
|
||||
@@ -214,10 +304,21 @@ jobs:
|
||||
"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
|
||||
# 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"
|
||||
|
||||
@@ -6,6 +6,13 @@ Install Fenris from its package channel after following the [package setup instr
|
||||
sudo apt update && sudo apt install fenris # Debian / Ubuntu
|
||||
sudo dnf install fenris # Fedora
|
||||
sudo zypper install fenris # openSUSE Tumbleweed
|
||||
sudo xbps-install fenris # Void Linux
|
||||
```
|
||||
|
||||
For Void Linux, configure the XBPS repository first:
|
||||
|
||||
```bash
|
||||
sudo xbps-install -S https://git.bongbetic.com/xavierk/Fenris-xbps/raw/branch/stable/x86_64
|
||||
```
|
||||
|
||||
## Verify downloads
|
||||
|
||||
@@ -68,6 +68,20 @@ def assemble_release_body(section: str, footer: str) -> str:
|
||||
return f"{section}{separator}{footer}"
|
||||
|
||||
|
||||
def format_availability_section(
|
||||
available: list[str], withheld: list[str]
|
||||
) -> str:
|
||||
"""Generate a package formats section for release notes."""
|
||||
if not available and not withheld:
|
||||
return ""
|
||||
lines = ["\n## Package formats\n"]
|
||||
if available:
|
||||
lines.append(f"Available: {', '.join(available)}")
|
||||
if withheld:
|
||||
lines.append(f"Withheld: {', '.join(withheld)}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("changelog", type=Path)
|
||||
@@ -77,6 +91,18 @@ def main(argv: list[str] | None = None) -> int:
|
||||
type=Path,
|
||||
help="append this standing release guidance after the extracted section",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--available",
|
||||
action="append",
|
||||
default=[],
|
||||
help="format available for this release (can be repeated)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--withheld",
|
||||
action="append",
|
||||
default=[],
|
||||
help="format withheld from this release (can be repeated)",
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
try:
|
||||
section = extract_changelog(args.changelog, args.version)
|
||||
@@ -88,6 +114,10 @@ def main(argv: list[str] | None = None) -> int:
|
||||
f"cannot read release footer {args.footer}: {error.strerror}"
|
||||
) from error
|
||||
section = assemble_release_body(section, footer)
|
||||
if args.available or args.withheld:
|
||||
formats = format_availability_section(args.available, args.withheld)
|
||||
if formats:
|
||||
section = f"{section}\n{formats}"
|
||||
sys.stdout.write(section)
|
||||
except ChangelogError as error:
|
||||
print(f"::error::{error}", file=sys.stderr)
|
||||
|
||||
@@ -209,3 +209,86 @@ def test_release_request_command_reports_create_or_patch_decisions(tmp_path):
|
||||
"path": "/releases/17",
|
||||
"payload": {"body": "## [1.4.0] - 2026-09-10\n"},
|
||||
}
|
||||
|
||||
|
||||
def test_format_availability_section_with_both():
|
||||
extractor = _extractor_module()
|
||||
result = extractor.format_availability_section(
|
||||
available=["Debian/Ubuntu (deb)", "Fedora/openSUSE (rpm)"],
|
||||
withheld=["Void Linux (xbps) — pending host acceptance"],
|
||||
)
|
||||
assert "Available: Debian/Ubuntu (deb), Fedora/openSUSE (rpm)" in result
|
||||
assert "Withheld: Void Linux (xbps) — pending host acceptance" in result
|
||||
assert "## Package formats" in result
|
||||
|
||||
|
||||
def test_format_availability_section_available_only():
|
||||
extractor = _extractor_module()
|
||||
result = extractor.format_availability_section(
|
||||
available=["Debian/Ubuntu (deb)", "Fedora/openSUSE (rpm)", "Void Linux (xbps)"],
|
||||
withheld=[],
|
||||
)
|
||||
assert "Available: Debian/Ubuntu (deb), Fedora/openSUSE (rpm), Void Linux (xbps)" in result
|
||||
assert "Withheld" not in result
|
||||
|
||||
|
||||
def test_format_availability_section_empty():
|
||||
extractor = _extractor_module()
|
||||
result = extractor.format_availability_section(available=[], withheld=[])
|
||||
assert result == ""
|
||||
|
||||
|
||||
def test_command_includes_format_availability(tmp_path):
|
||||
changelog = tmp_path / "CHANGELOG.md"
|
||||
changelog.write_text(
|
||||
"# Changelog\n\n## [Unreleased]\n\n## [1.4.0] - 2026-09-10\n\n"
|
||||
"### Added\n\n- Show a release summary.\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(EXTRACTOR_PATH),
|
||||
str(changelog),
|
||||
"1.4.0",
|
||||
"--available",
|
||||
"Debian/Ubuntu (deb)",
|
||||
"--available",
|
||||
"Fedora/openSUSE (rpm)",
|
||||
"--withheld",
|
||||
"Void Linux (xbps)",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
assert result.returncode == 0
|
||||
assert "## Package formats" in result.stdout
|
||||
assert "Available: Debian/Ubuntu (deb), Fedora/openSUSE (rpm)" in result.stdout
|
||||
assert "Withheld: Void Linux (xbps)" in result.stdout
|
||||
|
||||
|
||||
def test_command_without_format_args_has_no_formats_section(tmp_path):
|
||||
changelog = tmp_path / "CHANGELOG.md"
|
||||
changelog.write_text(
|
||||
"# Changelog\n\n## [Unreleased]\n\n## [1.4.0] - 2026-09-10\n\n"
|
||||
"### Added\n\n- Show a release summary.\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(EXTRACTOR_PATH),
|
||||
str(changelog),
|
||||
"1.4.0",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
assert result.returncode == 0
|
||||
assert "## Package formats" not in result.stdout
|
||||
|
||||
Reference in New Issue
Block a user