ci: package the release binary as a Fedora 44 rpm and publish it to rpm.lair.cafe
ci / gate (push) Has been cancelled
ci / gate (push) Has been cancelled
rpm/blackbeard-wallet.spec is the house prebuilt-binary spec: the bare binary from the release, the desktop entry rendered from the same template Tauri's bundlers use, AppStream metadata and four hicolor icons; no strip, no debuginfo; validators run where they exist; every rpmlint warning that stands is explained at the top. The release workflow gains a package job on the rpm runner (changelog entry from the tag through actions/rpm-changelog, rpmbuild with dist .fc44, rpmlint) and a publish job under the shared rpm-publish concurrency group that signs with the repository key, rsyncs as gitea_ci to oolon, runs createrepo_c and regenerates packages.json, exactly as cichlid does. rpm/rpmmacros and script/generate-packages-json.py are cichlid's. Built here from the local release binary: blackbeard-wallet-0.1.0-1.fc44 installs /usr/bin/blackbeard-wallet, the desktop entry, the metainfo and the icons. The publish job needs RPM_SIGNING_KEY, RPM_SIGNING_KEY_ID and RSYNC_SSH_KEY on this repository, which only the operator can add; until then a tag builds and attaches the release but stops at publish. Refs #33 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ftBXYuba8ARhQeF74oUgW
This commit is contained in:
@@ -3,8 +3,14 @@ name: release
|
||||
# A release is a tag on the workspace version. Push `vX.Y.Z` where X.Y.Z is
|
||||
# `version` in Cargo.toml (script/stamp-version.sh refuses anything else),
|
||||
# and this builds the Linux bundles and attaches them to a Gitea release for
|
||||
# that tag. Packaging for rpm.lair.cafe hangs off the release (#33); the
|
||||
# Windows and macOS builders are their own child (#34).
|
||||
# that tag. The package and publish jobs then put the same binary on
|
||||
# rpm.lair.cafe through the house signed-repo pipeline (#33): Fedora 44 only
|
||||
# for v1, prebuilt-binary spec, signed with RPM_SIGNING_KEY, rsynced as
|
||||
# gitea_ci to oolon, createrepo_c and packages.json regenerated. The Windows
|
||||
# and macOS builders are their own child (#34).
|
||||
#
|
||||
# Secrets this needs on the repository: RPM_SIGNING_KEY, RPM_SIGNING_KEY_ID,
|
||||
# RSYNC_SSH_KEY (the same values the other rpm.lair.cafe publishers hold).
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -104,3 +110,128 @@ jobs:
|
||||
name: linux-bundles
|
||||
path: dist/
|
||||
retention-days: 7
|
||||
|
||||
package:
|
||||
needs: linux
|
||||
# `rpm`: rpmbuild, rpmlint and the signing tools (gitea-runners.md §3).
|
||||
# One Fedora version for v1 (#33); the binary is a Fedora 44 binary
|
||||
# because rust-gtk3 is Fedora 44, so a wider matrix would need a source
|
||||
# build under mock or COPR, not a longer list here.
|
||||
runs-on: rpm
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
fedora_version: ["44"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: the tag is the workspace version
|
||||
id: version
|
||||
run: |
|
||||
set -euo pipefail
|
||||
script/stamp-version.sh "${GITHUB_REF_NAME}"
|
||||
echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: the bundles from the linux job
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: linux-bundles
|
||||
path: artifacts/
|
||||
|
||||
- name: changelog entry
|
||||
uses: https://git.lair.cafe/actions/rpm-changelog@v1
|
||||
with:
|
||||
spec: rpm/blackbeard-wallet.spec
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
|
||||
- name: build the rpm
|
||||
run: |
|
||||
set -euo pipefail
|
||||
rm -f ~/.rpmmacros
|
||||
rpmdev-setuptree
|
||||
v="${{ steps.version.outputs.version }}"
|
||||
install -m 0755 "artifacts/blackbeard-wallet-${v}-x86_64-linux-gnu" ~/rpmbuild/SOURCES/blackbeard-wallet
|
||||
cp crates/wallet-app/blackbeard-wallet.desktop rpm/cafe.lair.blackbeard.wallet.metainfo.xml ~/rpmbuild/SOURCES/
|
||||
cp crates/wallet-app/icons/32x32.png crates/wallet-app/icons/128x128.png \
|
||||
crates/wallet-app/icons/128x128@2x.png crates/wallet-app/icons/icon.png ~/rpmbuild/SOURCES/
|
||||
rpmbuild -bb rpm/blackbeard-wallet.spec \
|
||||
--define "wallet_version ${v}" \
|
||||
--undefine dist \
|
||||
--define "dist .fc${{ matrix.fedora_version }}"
|
||||
rpmlint ~/rpmbuild/RPMS/x86_64/*.rpm || true
|
||||
ls -l ~/rpmbuild/RPMS/x86_64/
|
||||
|
||||
- name: keep the rpm for publishing
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: rpm-fc${{ matrix.fedora_version }}
|
||||
path: ~/rpmbuild/RPMS/x86_64/*.rpm
|
||||
retention-days: 7
|
||||
|
||||
publish:
|
||||
needs: package
|
||||
runs-on: rpm
|
||||
# Serialise repository metadata updates (createrepo_c) across every
|
||||
# publisher on rpm.lair.cafe.
|
||||
concurrency:
|
||||
group: rpm-publish
|
||||
cancel-in-progress: false
|
||||
env:
|
||||
RPM_REPO_HOST: oolon.kosherinata.internal
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 1
|
||||
matrix:
|
||||
fedora_version: ["44"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: the rpm
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: rpm-fc${{ matrix.fedora_version }}
|
||||
path: rpms/
|
||||
|
||||
- name: import the signing key
|
||||
run: |
|
||||
set -euo pipefail
|
||||
command -v sq >/dev/null || { echo "sequoia-sq is not installed"; exit 1; }
|
||||
echo "${{ secrets.RPM_SIGNING_KEY }}" | gpg --batch --import
|
||||
fpr=$(gpg --batch --with-colons --list-keys "${{ secrets.RPM_SIGNING_KEY_ID }}" | awk -F: '/^fpr:/ { print $10; exit }')
|
||||
echo "${fpr}:6:" | gpg --batch --import-ownertrust
|
||||
sed "s/@GPG_NAME@/${{ secrets.RPM_SIGNING_KEY_ID }}/" rpm/rpmmacros > ~/.rpmmacros
|
||||
|
||||
- name: sign
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for rpm in rpms/*.rpm; do
|
||||
echo "signing ${rpm}"
|
||||
rpm --addsign "${rpm}"
|
||||
rpm -K "${rpm}"
|
||||
done
|
||||
|
||||
- name: ssh to the repo host
|
||||
env:
|
||||
RSYNC_SSH_KEY: ${{ secrets.RSYNC_SSH_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
install --directory --mode 700 ~/.ssh
|
||||
echo "${RSYNC_SSH_KEY}" | install --mode 600 /dev/stdin ~/.ssh/id_ed25519
|
||||
ssh -o StrictHostKeyChecking=accept-new "gitea_ci@${RPM_REPO_HOST}" exit
|
||||
|
||||
- name: publish to the stable channel
|
||||
run: |
|
||||
set -euo pipefail
|
||||
dir="/var/www/rpm/fedora/${{ matrix.fedora_version }}/x86_64"
|
||||
ssh "gitea_ci@${RPM_REPO_HOST}" "mkdir -p ${dir}"
|
||||
rsync --archive --verbose --chmod D755,F644 rpms/*.rpm "gitea_ci@${RPM_REPO_HOST}:${dir}/"
|
||||
ssh "gitea_ci@${RPM_REPO_HOST}" "cd ${dir} && createrepo_c --update ."
|
||||
scp script/generate-packages-json.py "gitea_ci@${RPM_REPO_HOST}:/tmp/"
|
||||
ssh "gitea_ci@${RPM_REPO_HOST}" \
|
||||
"python3 /tmp/generate-packages-json.py \
|
||||
--repodata-dir ${dir}/repodata \
|
||||
--output ${dir}/packages.json \
|
||||
--base-url https://rpm.lair.cafe/fedora/${{ matrix.fedora_version }}/x86_64"
|
||||
echo "published: https://rpm.lair.cafe/fedora/${{ matrix.fedora_version }}/x86_64/"
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
# Prebuilt binary from the release workflow's rust-gtk3 job (Fedora 44), so no
|
||||
# compile here, no strip, no debuginfo: the house convention for a desktop
|
||||
# application (lair/claude-desktop-package), and blackbeard/wallet #33's
|
||||
# operator decision: Fedora 44 only for v1, a source build under mock or COPR
|
||||
# only if the matrix ever grows.
|
||||
%global _build_id_links none
|
||||
%global debug_package %{nil}
|
||||
%global __strip /usr/bin/true
|
||||
|
||||
# Passed in via --define at rpmbuild time (see .gitea/workflows/release.yaml).
|
||||
%{!?wallet_version: %global wallet_version 0.0.0}
|
||||
|
||||
# rpmlint, and why each remaining warning stands:
|
||||
# unstripped-binary-or-object deliberate (%%__strip above): the binary is
|
||||
# built and stripped upstream of rpmbuild.
|
||||
# no-manual-page-for-binary a windowed application; --help is the manual.
|
||||
# no-documentation the readme lives in the repository the URL names.
|
||||
# crypto-policy-non-compliance a dependency calls SSL_CTX_set_cipher_list;
|
||||
# the wallet's own TLS is rustls (reqwest, subxt).
|
||||
# spelling-error "quantus", "cli's".
|
||||
# no-changelogname-tag only on a workstation build; the release
|
||||
# workflow writes %%changelog first.
|
||||
|
||||
Name: blackbeard-wallet
|
||||
Version: %{wallet_version}
|
||||
Release: 1%{?dist}
|
||||
Summary: A desktop wallet for post-quantum chains
|
||||
|
||||
License: GPL-3.0-or-later
|
||||
URL: https://git.lair.cafe/blackbeard/wallet
|
||||
|
||||
# The bare binary, the desktop entry template Tauri's own bundlers use, the
|
||||
# AppStream metadata, and the hicolor icons, all from the release checkout.
|
||||
Source0: blackbeard-wallet
|
||||
Source1: blackbeard-wallet.desktop
|
||||
Source2: cafe.lair.blackbeard.wallet.metainfo.xml
|
||||
Source3: 32x32.png
|
||||
Source4: 128x128.png
|
||||
Source5: 128x128@2x.png
|
||||
Source6: icon.png
|
||||
|
||||
ExclusiveArch: x86_64
|
||||
|
||||
# The webview and the toolkit the binary links dynamically. rpm's automatic
|
||||
# soname requires finds the same libraries; these name the packages a person
|
||||
# would look for.
|
||||
Requires: webkit2gtk4.1
|
||||
Requires: gtk3
|
||||
|
||||
%description
|
||||
The blackbeard wallet is a desktop wallet for post-quantum chains, Quantus
|
||||
first: ML-DSA signatures, accounts and balances, sending with a review of
|
||||
what will be signed, reversible transfers with a cancellation window, and
|
||||
history from blackbeard.observer kept readable offline. Wallet files share
|
||||
quantus-cli's encrypted format. Nodes are reached through the public
|
||||
endpoints at *.blackbeard.observer.
|
||||
|
||||
%prep
|
||||
# Every source is a bare file; nothing to unpack.
|
||||
|
||||
%build
|
||||
# Built upstream of rpmbuild, on the rust-gtk3 runner.
|
||||
|
||||
%install
|
||||
install -D -m 0755 %{SOURCE0} %{buildroot}%{_bindir}/blackbeard-wallet
|
||||
# The desktop template carries Tauri's placeholders; fill them the way its
|
||||
# bundler would, so the deb, the rpm from `cargo tauri build` and this rpm
|
||||
# install the same entry.
|
||||
sed -e 's/{{exec}}/blackbeard-wallet/' -e 's/{{icon}}/blackbeard-wallet/' \
|
||||
-e '/^#/d' %{SOURCE1} \
|
||||
> blackbeard-wallet.desktop
|
||||
install -D -m 0644 blackbeard-wallet.desktop \
|
||||
%{buildroot}%{_datadir}/applications/blackbeard-wallet.desktop
|
||||
install -D -m 0644 %{SOURCE2} \
|
||||
%{buildroot}%{_metainfodir}/cafe.lair.blackbeard.wallet.metainfo.xml
|
||||
install -D -m 0644 %{SOURCE3} %{buildroot}%{_datadir}/icons/hicolor/32x32/apps/blackbeard-wallet.png
|
||||
install -D -m 0644 %{SOURCE4} %{buildroot}%{_datadir}/icons/hicolor/128x128/apps/blackbeard-wallet.png
|
||||
install -D -m 0644 %{SOURCE5} %{buildroot}%{_datadir}/icons/hicolor/256x256/apps/blackbeard-wallet.png
|
||||
install -D -m 0644 %{SOURCE6} %{buildroot}%{_datadir}/icons/hicolor/512x512/apps/blackbeard-wallet.png
|
||||
|
||||
%check
|
||||
# The validators are not on the rpm runner image; where they exist (a
|
||||
# workstation), a bad entry fails the build rather than the install.
|
||||
if command -v desktop-file-validate >/dev/null; then
|
||||
desktop-file-validate %{buildroot}%{_datadir}/applications/blackbeard-wallet.desktop
|
||||
fi
|
||||
if command -v appstreamcli >/dev/null; then
|
||||
appstreamcli validate --no-net %{buildroot}%{_metainfodir}/cafe.lair.blackbeard.wallet.metainfo.xml
|
||||
fi
|
||||
|
||||
%files
|
||||
%{_bindir}/blackbeard-wallet
|
||||
%{_datadir}/applications/blackbeard-wallet.desktop
|
||||
%{_metainfodir}/cafe.lair.blackbeard.wallet.metainfo.xml
|
||||
%{_datadir}/icons/hicolor/*/apps/blackbeard-wallet.png
|
||||
|
||||
%post
|
||||
touch --no-create %{_datadir}/icons/hicolor &>/dev/null || :
|
||||
update-desktop-database &>/dev/null || :
|
||||
|
||||
%postun
|
||||
if [ $1 -eq 0 ] ; then
|
||||
touch --no-create %{_datadir}/icons/hicolor &>/dev/null
|
||||
gtk-update-icon-cache %{_datadir}/icons/hicolor &>/dev/null || :
|
||||
update-desktop-database &>/dev/null || :
|
||||
fi
|
||||
|
||||
%posttrans
|
||||
gtk-update-icon-cache %{_datadir}/icons/hicolor &>/dev/null || :
|
||||
|
||||
%changelog
|
||||
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- AppStream metadata for the software centre; the id matches the Tauri
|
||||
bundle identifier and the desktop entry it launches. -->
|
||||
<component type="desktop-application">
|
||||
<id>cafe.lair.blackbeard.wallet</id>
|
||||
<name>blackbeard wallet</name>
|
||||
<summary>A desktop wallet for post-quantum chains</summary>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>GPL-3.0-or-later</project_license>
|
||||
<developer id="cafe.lair.blackbeard">
|
||||
<name>blackbeard</name>
|
||||
</developer>
|
||||
<description>
|
||||
<p>
|
||||
A wallet for post-quantum chains, Quantus first: ML-DSA signatures,
|
||||
accounts and balances, sending with a review of exactly what will be
|
||||
signed, reversible transfers with a cancellation window, and history
|
||||
from blackbeard.observer that stays readable offline.
|
||||
</p>
|
||||
<p>
|
||||
Wallet files share quantus-cli's encrypted format, so a wallet made
|
||||
here opens in the CLI and the other way round.
|
||||
</p>
|
||||
</description>
|
||||
<launchable type="desktop-id">blackbeard-wallet.desktop</launchable>
|
||||
<url type="homepage">https://git.lair.cafe/blackbeard/wallet</url>
|
||||
<url type="bugtracker">https://git.lair.cafe/blackbeard/wallet/issues</url>
|
||||
<categories>
|
||||
<category>Finance</category>
|
||||
<category>Network</category>
|
||||
</categories>
|
||||
<content_rating type="oars-1.1" />
|
||||
</component>
|
||||
@@ -0,0 +1 @@
|
||||
%_openpgp_sign_id @GPG_NAME@
|
||||
Executable
+154
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Parse RPM repodata and emit a packages.json manifest for the UI."""
|
||||
|
||||
import argparse
|
||||
import gzip
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
from datetime import datetime, timezone
|
||||
|
||||
RPM_NS = "http://linux.duke.edu/metadata/common"
|
||||
OTHER_NS = "http://linux.duke.edu/metadata/other"
|
||||
REPO_NS = "http://linux.duke.edu/metadata/repo"
|
||||
|
||||
|
||||
def find_repodata_file(repodata_dir, data_type):
|
||||
"""Read repomd.xml and return the path to a specific data type's file."""
|
||||
repomd_path = os.path.join(repodata_dir, "repomd.xml")
|
||||
tree = ET.parse(repomd_path)
|
||||
root = tree.getroot()
|
||||
|
||||
for data in root.findall(f"{{{REPO_NS}}}data"):
|
||||
if data.get("type") == data_type:
|
||||
location = data.find(f"{{{REPO_NS}}}location")
|
||||
if location is not None:
|
||||
href = location.get("href", "")
|
||||
return os.path.join(os.path.dirname(repodata_dir), href)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def open_compressed(path):
|
||||
"""Open a gzip or zstd compressed file for reading."""
|
||||
if path.endswith(".zst"):
|
||||
result = subprocess.run(
|
||||
["zstdcat", path], capture_output=True, check=True
|
||||
)
|
||||
import io
|
||||
return io.BytesIO(result.stdout)
|
||||
else:
|
||||
return gzip.open(path, "rb")
|
||||
|
||||
|
||||
def parse_primary(repodata_dir):
|
||||
"""Parse primary.xml.{gz,zst} and return package metadata."""
|
||||
path = find_repodata_file(repodata_dir, "primary")
|
||||
if not path:
|
||||
print("error: primary metadata not found in repomd.xml", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
packages = {}
|
||||
with open_compressed(path) as f:
|
||||
tree = ET.parse(f)
|
||||
|
||||
for pkg in tree.getroot().findall(f"{{{RPM_NS}}}package"):
|
||||
if pkg.get("type") != "rpm":
|
||||
continue
|
||||
|
||||
name = pkg.findtext(f"{{{RPM_NS}}}name", "")
|
||||
version_el = pkg.find(f"{{{RPM_NS}}}version")
|
||||
ver = version_el.get("ver", "") if version_el is not None else ""
|
||||
rel = version_el.get("rel", "") if version_el is not None else ""
|
||||
arch = pkg.findtext(f"{{{RPM_NS}}}arch", "")
|
||||
|
||||
size_el = pkg.find(f"{{{RPM_NS}}}size")
|
||||
size = int(size_el.get("package", "0")) if size_el is not None else 0
|
||||
|
||||
time_el = pkg.find(f"{{{RPM_NS}}}time")
|
||||
build_time = int(time_el.get("build", "0")) if time_el is not None else 0
|
||||
|
||||
location_el = pkg.find(f"{{{RPM_NS}}}location")
|
||||
filename = os.path.basename(location_el.get("href", "")) if location_el is not None else ""
|
||||
|
||||
key = f"{name}-{ver}-{rel}"
|
||||
packages[key] = {
|
||||
"name": name,
|
||||
"version": ver,
|
||||
"release": rel,
|
||||
"arch": arch,
|
||||
"summary": pkg.findtext(f"{{{RPM_NS}}}summary", ""),
|
||||
"size": size,
|
||||
"buildTime": build_time,
|
||||
"rpmFilename": filename,
|
||||
"changelog": [],
|
||||
}
|
||||
|
||||
return packages
|
||||
|
||||
|
||||
def parse_other(repodata_dir, packages):
|
||||
"""Parse other.xml.gz and attach changelog entries to packages."""
|
||||
path = find_repodata_file(repodata_dir, "other")
|
||||
if not path:
|
||||
return
|
||||
|
||||
with open_compressed(path) as f:
|
||||
tree = ET.parse(f)
|
||||
|
||||
for pkg in tree.getroot().findall(f"{{{OTHER_NS}}}package"):
|
||||
name = pkg.get("name", "")
|
||||
version_el = pkg.find(f"{{{OTHER_NS}}}version")
|
||||
ver = version_el.get("ver", "") if version_el is not None else ""
|
||||
rel = version_el.get("rel", "") if version_el is not None else ""
|
||||
key = f"{name}-{ver}-{rel}"
|
||||
|
||||
if key not in packages:
|
||||
continue
|
||||
|
||||
for entry in pkg.findall(f"{{{OTHER_NS}}}changelog"):
|
||||
packages[key]["changelog"].append({
|
||||
"author": entry.get("author", ""),
|
||||
"date": int(entry.get("date", "0")),
|
||||
"text": (entry.text or "").strip(),
|
||||
})
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"--repodata-dir",
|
||||
required=True,
|
||||
help="path to the repodata/ directory",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
required=True,
|
||||
help="path to write packages.json",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--base-url",
|
||||
required=True,
|
||||
help="public base URL for the repo (e.g. https://rpm.lair.cafe/fedora/43/x86_64)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
packages = parse_primary(args.repodata_dir)
|
||||
parse_other(args.repodata_dir, packages)
|
||||
|
||||
manifest = {
|
||||
"generated": datetime.now(timezone.utc).isoformat(),
|
||||
"baseUrl": args.base_url,
|
||||
"packages": list(packages.values()),
|
||||
}
|
||||
|
||||
with open(args.output, "w") as f:
|
||||
json.dump(manifest, f, indent=2)
|
||||
|
||||
print(f"wrote {len(packages)} packages to {args.output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user