Files
mtgc/scripts/rpm-publish.sh
Garrett Mills 8d510b8ffd Publish desktop releases to a self-hosted dnf repo
scripts/rpm-publish.sh builds the RPM, signs it and the repo metadata
with a dedicated GPG key, and adds it to a dnf repo in a given directory,
keeping the newest three versions for downgrades. The directory holds
nothing secret, so it can be served as is.

Both publish scripts now share the version scheme in
scripts/lib/release-version.sh: <major>.<minor>.<commit count>, plain
enough for RPM, MSI and semver alike. Android's versionCode stays the
commit count.

Fixes the publish scripts failing under set -u when the repo URL
variable isn't set, which is every run after the first. tauri.conf.json
now declares the license and homepage for the package metadata.
2026-10-13 22:05:17 -05:00

202 lines
6.4 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# rpm-publish.sh — build a signed RPM of the desktop app and add it to a dnf repo.
#
# Usage:
# RPM_REPO_URL=https://rpm.example.com/mtgc \
# ./scripts/rpm-publish.sh --init-key ~/rpm-mtgc # first time
# ./scripts/rpm-publish.sh ~/rpm-mtgc # every release after
#
# Or through npm: `npm run rpm:publish -- ~/rpm-mtgc`.
#
# Options:
# --init-key generate the package signing key (once, ever)
# --allow-dirty publish uncommitted changes; the RPM release becomes 0.dirty
#
# Environment:
# MTGC_RPM_DIR repo directory, if not given as an argument
# RPM_REPO_URL public address of the repo directory; needed the first time,
# and changes the address in mtgc.repo if set later
# MTGC_RPM_GNUPGHOME where --init-key keeps the key (default ~/.local/share/mtgc/gnupg)
#
# The whole repo directory is public — the signing key lives elsewhere — so
# serve it as is. Needs rpm-sign and createrepo_c (`sudo dnf install rpm-sign
# createrepo_c`) and the desktop setup from ./scripts/devel-setup.sh.
#
set -euo pipefail
usage() {
sed -n '3,25p' "$0" | sed 's/^# \{0,1\}//'
exit "${1:-0}"
}
info() { printf '\033[1;34m==>\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m[warn]\033[0m %s\n' "$*"; }
err() { printf '\033[1;31m[err]\033[0m %s\n' "$*" >&2; }
PACKAGE="mtgc"
GNUPGHOME="${MTGC_RPM_GNUPGHOME:-$HOME/.local/share/mtgc/gnupg}"
KEEP=3 # versions kept in the repo, so a bad release can be downgraded from
init_key=false
allow_dirty=false
dir="${MTGC_RPM_DIR:-}"
while [ $# -gt 0 ]; do
case "$1" in
--init-key) init_key=true ;;
--allow-dirty) allow_dirty=true ;;
-h | --help) usage ;;
-*) usage 1 ;;
*) dir="$1" ;;
esac
shift
done
[ -n "$dir" ] || usage 1
mkdir -p "$dir"
dir="$(cd "$dir" && pwd)"
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$repo_root"
# --- 1. Tools ---------------------------------------------------------------
for tool in rpmsign createrepo_c gpg; do
if ! command -v "$tool" >/dev/null 2>&1; then
err "$tool not found. Install them: sudo dnf install rpm-sign createrepo_c"
exit 1
fi
done
# --- 2. Package signing key -------------------------------------------------
#
# A key of its own in its own keyring, with no passphrase so publishing runs
# unattended. Every machine trusts it once, when it adds the repo; lose it and
# each one has to import a new key before it takes another update.
gpg_() { gpg --homedir "$GNUPGHOME" --batch "$@"; }
key_fpr() { gpg_ --list-secret-keys --with-colons 2>/dev/null | awk -F: '/^fpr/ { print $10; exit }'; }
if $init_key; then
if [ -n "$(key_fpr)" ]; then
err "a signing key already exists in $GNUPGHOME; not making another"
exit 1
fi
info "generating the package signing key in $GNUPGHOME"
mkdir -p "$GNUPGHOME"
chmod 700 "$GNUPGHOME"
gpg_ --pinentry-mode loopback --passphrase '' \
--quick-gen-key "mtgc packages" rsa4096 sign never 2>/dev/null
warn "Back up $GNUPGHOME."
warn "Without it, every machine has to trust a new key before it takes another update."
fi
fpr="$(key_fpr)"
if [ -z "$fpr" ]; then
err "no package signing key in $GNUPGHOME. Run once with --init-key."
exit 1
fi
# --- 3. Version -------------------------------------------------------------
source scripts/lib/release-version.sh
release=1
if $release_dirty; then
if ! $allow_dirty; then
err "the working tree has uncommitted changes; commit them or pass --allow-dirty"
exit 1
fi
release=0.dirty
fi
# --- 4. The repo's address --------------------------------------------------
repo_url="${RPM_REPO_URL:-}"
repo_url="${repo_url%/}"
case "$repo_url" in
"" | http://* | https://*) ;;
*) err "RPM_REPO_URL must be an http(s):// address"; exit 1 ;;
esac
if [ -z "$repo_url" ] && [ ! -f "$dir/$PACKAGE.repo" ]; then
err "no repo in $dir yet; set RPM_REPO_URL to the address $dir will be served at"
exit 1
fi
if [ -n "$repo_url" ]; then
cat >"$dir/$PACKAGE.repo" <<EOF
[$PACKAGE]
name=mtgc
baseurl=$repo_url
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=$repo_url/RPM-GPG-KEY-$PACKAGE
EOF
fi
gpg_ --armor --export "$fpr" >"$dir/RPM-GPG-KEY-$PACKAGE"
# --- 5. Only ever move forward ----------------------------------------------
newest=0
for rpm_file in "$dir/$PACKAGE"-*.rpm; do
[ -e "$rpm_file" ] || continue
patch="$(rpm -qp --qf '%{VERSION}' "$rpm_file" 2>/dev/null)"
patch="${patch##*.}"
[ "$patch" -gt "$newest" ] && newest="$patch"
done
if [ "$release_count" -le "$newest" ]; then
err "version $release_version is not newer than the repo's x.y.$newest; commit something first"
exit 1
fi
# --- 6. Build and sign ------------------------------------------------------
info "building $release_version-$release"
outputs="target/release/bundle/rpm"
rm -rf "$outputs"
cargo tauri build --bundles rpm --ci \
--config "{\"version\":\"$release_version\",\"bundle\":{\"linux\":{\"rpm\":{\"release\":\"$release\"}}}}"
built="$(find "$outputs" -name '*.rpm' | head -1)"
if [ -z "$built" ]; then
err "the build produced no RPM under $outputs"
exit 1
fi
rpmsign --addsign --define "_gpg_path $GNUPGHOME" --define "_gpg_name $fpr" "$built" >/dev/null
# --- 7. Publish -------------------------------------------------------------
update_metadata() {
createrepo_c --quiet --update "$dir" &&
gpg_ --yes --local-user "$fpr" --armor --detach-sign "$dir/repodata/repomd.xml"
}
published="$dir/$(basename "$built")"
cp "$built" "$published"
info "updating the repo metadata"
if ! update_metadata; then
# Take the RPM back out, or the next run would think it's already published.
rm -f "$published"
update_metadata || true
err "updating the repo metadata failed; nothing was published"
exit 1
fi
# Drop all but the newest few versions.
mapfile -t old < <(find "$dir" -maxdepth 1 -name "$PACKAGE-*.rpm" | sort -V | head -n -"$KEEP")
if [ "${#old[@]}" -gt 0 ]; then
rm -f "${old[@]}"
update_metadata
fi
# --- 8. How to use it -------------------------------------------------------
info "published $release_version-$release"
repo_file_url="$(sed -n 's/^baseurl=//p' "$dir/$PACKAGE.repo")/$PACKAGE.repo"
cat <<EOF
Serve $dir at $(sed -n 's/^baseurl=//p' "$dir/$PACKAGE.repo"). On each machine, once:
sudo dnf config-manager addrepo --from-repofile=$repo_file_url
sudo dnf install $PACKAGE
After that, \`sudo dnf upgrade\` picks up new releases.
EOF