diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 6bcb11f..3668751 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -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" diff --git a/AGENTS.md b/AGENTS.md index 658db27..dd4f626 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,10 @@ ## Agent skills +## Commit messages + +Do not add `Co-authored-by: CommandCodeBot ` or other +CommandCodeBot attribution trailers to commits. + ### Issue tracker Issues are tracked in Gitea using the authenticated `tea` CLI. See `docs/agents/issue-tracker.md`. diff --git a/packaging/release-footer.md b/packaging/release-footer.md index 2e5294a..063ae6e 100644 --- a/packaging/release-footer.md +++ b/packaging/release-footer.md @@ -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 diff --git a/scripts/extract_changelog.py b/scripts/extract_changelog.py index 9ec55d4..5ff956a 100755 --- a/scripts/extract_changelog.py +++ b/scripts/extract_changelog.py @@ -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) diff --git a/tests/test_changelog.py b/tests/test_changelog.py index 18351f6..b117c3f 100644 --- a/tests/test_changelog.py +++ b/tests/test_changelog.py @@ -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