From c745e2e4adc329e652e57d50f49d90595bcc4ebb Mon Sep 17 00:00:00 2001 From: Oleg Shparber Date: Wed, 3 Jun 2026 01:11:45 +0300 Subject: [PATCH] ci(github): add Fedora COPR publishing (#1904) --- .github/workflows/publish-copr.yaml | 199 ++++++++++++++++++++++++++++ pkg/copr/README.md | 58 ++++++++ pkg/copr/zeal.spec | 84 ++++++++++++ 3 files changed, 341 insertions(+) create mode 100644 .github/workflows/publish-copr.yaml create mode 100644 pkg/copr/README.md create mode 100644 pkg/copr/zeal.spec diff --git a/.github/workflows/publish-copr.yaml b/.github/workflows/publish-copr.yaml new file mode 100644 index 00000000..684bf79d --- /dev/null +++ b/.github/workflows/publish-copr.yaml @@ -0,0 +1,199 @@ +name: Publish COPR Packages + +on: + release: + types: [published] + schedule: + # Skipped when main has not advanced since the last build (see "Resolve version"). + - cron: "2 4 * * *" + workflow_dispatch: + inputs: + version: + description: "Release version to upload (without 'v'). Leave blank to build a nightly snapshot from main." + required: false + +permissions: {} + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +env: + COPR_PROJECT_STABLE: "@zealdocs/zeal" + COPR_PROJECT_NIGHTLY: "@zealdocs/zeal-nightly" + +jobs: + publish: + name: Publish + if: github.repository == 'zealdocs/zeal' + runs-on: ubuntu-latest + # Build the source RPM on Fedora so the Fedora spec macros (%cmake, %autosetup, + # %{?dist}) and rpmbuild/copr-cli are all native. The first step installs git + # so the checkout below can use it. + container: + image: fedora:latest + permissions: + contents: read + + steps: + - name: Install packaging tools + run: dnf -y install git rpm-build copr-cli jq + + - name: Determine mode and target + id: mode + # Untrusted event/input values go through env and are validated before use + # to block shell injection. + env: + DISPATCH_VERSION: ${{ inputs.version }} + RELEASE_TAG: ${{ github.event.release.tag_name }} + run: | + set -euo pipefail + if [ "${{ github.event_name }}" = "release" ]; then + raw="$RELEASE_TAG"; stable=true + elif [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "$DISPATCH_VERSION" ]; then + raw="$DISPATCH_VERSION"; stable=true + else + stable=false + fi + + if [ "$stable" = true ]; then + version="${raw#v}" # tolerate an optional leading v + # RPM forbids '-' in Version: (it delimits version from release), so + # reject it here along with anything else outside an RPM version. + case "$version" in + ''|*[!0-9A-Za-z.+~]*) + echo "::error::Unexpected version string: '${version}'"; exit 1 ;; + esac + { + echo "mode=stable" + echo "ref=v${version}" + echo "version=${version}" + echo "project=${COPR_PROJECT_STABLE}" + } >> "$GITHUB_OUTPUT" + echo "Stable release: ${version}" + else + # Version is derived from the checkout below, so it is left empty here. + { + echo "mode=nightly" + echo "ref=main" + echo "version=" + echo "project=${COPR_PROJECT_NIGHTLY}" + } >> "$GITHUB_OUTPUT" + echo "Nightly snapshot from main" + fi + + - name: Checkout + uses: actions/checkout@v6 + with: + ref: ${{ steps.mode.outputs.ref }} + fetch-depth: 0 + fetch-tags: true + persist-credentials: false + + - name: Configure COPR credentials + env: + COPR_CONFIG: ${{ secrets.COPR_API_TOKEN }} + run: | + set -euo pipefail + if [ -z "$COPR_CONFIG" ]; then + echo "::error::COPR_API_TOKEN secret is empty; copy the token block from https://copr.fedorainfracloud.org/api/." + exit 1 + fi + mkdir -p "${HOME}/.config" + printf '%s' "$COPR_CONFIG" > "${HOME}/.config/copr" + chmod 600 "${HOME}/.config/copr" + + - name: Resolve version + id: resolve + env: + MODE: ${{ steps.mode.outputs.mode }} + VERSION: ${{ steps.mode.outputs.version }} + PROJECT: ${{ steps.mode.outputs.project }} + run: | + set -euo pipefail + # The checkout populates the workspace as a different owner than the + # container's root, which trips git's ownership guard. + git config --global --add safe.directory "$PWD" + + if [ "$MODE" = "stable" ]; then + version="$VERSION" + else + # Snapshot version from main: 0.8.2-15-gabcdef -> 0.8.2^15.gabcdef, which + # sorts above the last release and below the next. + version=$(git describe --tags --long | sed 's/^v//; s/-\([0-9]*\)-g/^\1.g/') + + # Skip when COPR's latest build already has this version (the version + # embeds the commit). An empty result never skips, so a failed or missing + # build is retried. Confirm the JSON path against copr-cli; a wrong path + # just falls back to always building. + last=$(copr-cli get-package "$PROJECT" --name zeal --with-latest-build \ + --output-format json 2>/dev/null | jq -r '.latest_build.source_package.version // empty' || true) + if [ -n "$last" ] && [ "$version" = "$last" ]; then + echo "main has not advanced since the last build (${last}); nothing to do." + echo "proceed=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + fi + + echo "Building version: ${version}" + echo "version=${version}" >> "$GITHUB_OUTPUT" + echo "proceed=true" >> "$GITHUB_OUTPUT" + + - name: Build source RPM + if: steps.resolve.outputs.proceed == 'true' + env: + VERSION: ${{ steps.resolve.outputs.version }} + PACKAGER: Zeal Release + run: | + set -euo pipefail + + if [ ! -f pkg/copr/zeal.spec ]; then + echo "::error::pkg/copr/zeal.spec missing in this checkout." + exit 1 + fi + + mkdir -p "${HOME}/rpmbuild/SOURCES" "${HOME}/rpmbuild/SPECS" + + # Tarball name and prefix must match the spec's Source0. + git archive --prefix="zeal-${VERSION}/" HEAD \ + | gzip -9 > "${HOME}/rpmbuild/SOURCES/zeal-${VERSION}.tar.gz" + + # Replace the spec's %changelog with a single dated entry. + spec="${HOME}/rpmbuild/SPECS/zeal.spec" + sed '/^%changelog/q' pkg/copr/zeal.spec > "$spec" + { + printf '* %s %s - %s-1\n' "$(LC_ALL=C date '+%a %b %d %Y')" "$PACKAGER" "$VERSION" + printf -- '- Build of %s.\n' "$VERSION" + } >> "$spec" + + # Pass the bundled cpp-httplib version (from its header) to the spec's + # bundled() Provides. Omitted if not found, leaving it unversioned. + httplib_version="" + if [ -f src/contrib/cpp-httplib/httplib.h ]; then + httplib_version=$(sed -n 's/^#define CPPHTTPLIB_VERSION "\([^"]*\)".*/\1/p' \ + src/contrib/cpp-httplib/httplib.h | head -n1) + fi + + defines=(--define "_topdir ${HOME}/rpmbuild" --define "zeal_version ${VERSION}") + if [ -n "$httplib_version" ]; then + defines+=(--define "httplib_version ${httplib_version}") + fi + + rpmbuild -bs "$spec" "${defines[@]}" + + srpm=$(find "${HOME}/rpmbuild/SRPMS" -name "zeal-${VERSION}-1*.src.rpm" | head -n1) + if [ -z "$srpm" ]; then + echo "::error::Source RPM was not produced." + exit 1 + fi + echo "SRPM=${srpm}" >> "$GITHUB_ENV" + + - name: Submit build to COPR + if: steps.resolve.outputs.proceed == 'true' + # No --nowait: wait so a failing chroot fails the workflow. COPR rebuilds + # for every enabled chroot. + env: + PROJECT: ${{ steps.mode.outputs.project }} + run: | + set -euo pipefail + copr-cli build "$PROJECT" "$SRPM" diff --git a/pkg/copr/README.md b/pkg/copr/README.md new file mode 100644 index 00000000..f63a6e78 --- /dev/null +++ b/pkg/copr/README.md @@ -0,0 +1,58 @@ +# Fedora COPR packaging + +RPM packaging for Zeal and the workflow that publishes it to +[Fedora COPR](https://copr.fedorainfracloud.org/). COPR builds binaries from a +source RPM, once per enabled chroot. + +`publish-copr.yaml` runs two flows: + +| Flow | COPR project | Trigger | +| ------- | ------------------------ | -------------------------------------------- | +| Release | `@zealdocs/zeal` | GitHub release, or manual run with a version | +| Nightly | `@zealdocs/zeal-nightly` | daily cron, or manual run with no version | + +Both build the source RPM in the workflow and submit it with `copr-cli`. The +nightly run skips when `main` has not advanced since its last build. + +## Files + +`zeal.spec` is the RPM spec. `%build` and `%install` are `%cmake` macros, since +CMake installs the binary, desktop file, metainfo, and icons under the prefix. +toml++ comes from `tomlplusplus-devel`; cpp-httplib stays bundled in `src/contrib`. + +`Version:` is a placeholder. The workflow sets it with `--define "zeal_version "`: +the tag for releases, a `git describe` snapshot for nightlies (which sorts above +the last release and below the next). + +## Chroots + +Chroots live on the COPR projects, not in the repo. Targets are the supported +Fedora releases plus rawhide, and the EPEL releases that ship `qt6-qtwebengine` +(RHEL, AlmaLinux, Rocky, CentOS Stream; EPEL 8 lacks it). "Follow Fedora branching" +adds new Fedora chroots automatically; add new EPEL releases by hand. + +Support windows: [Fedora](https://endoflife.date/fedora), +[CentOS Stream](https://endoflife.date/centos-stream), +[RHEL](https://endoflife.date/rhel). + +## Setup + +1. Create `@zealdocs/zeal` and `@zealdocs/zeal-nightly`, enable the chroots, and turn + on "Follow Fedora branching". Enable "Generate AppStream metadata" on the + release project. +2. Add the repository secret `COPR_API_TOKEN`: the `[copr-cli]` block from + . Both flows use it; refresh it when it + expires. + +## Local testing + +```sh +VERSION=$(git describe --tags --abbrev=0 | sed 's/^v//') +mkdir -p ~/rpmbuild/SOURCES ~/rpmbuild/SPECS +git archive --prefix="zeal-$VERSION/" "v$VERSION" | gzip -9 \ + > ~/rpmbuild/SOURCES/zeal-$VERSION.tar.gz +cp pkg/copr/zeal.spec ~/rpmbuild/SPECS/ +rpmbuild -bs ~/rpmbuild/SPECS/zeal.spec \ + --define "_topdir $HOME/rpmbuild" --define "zeal_version $VERSION" +mock ~/rpmbuild/SRPMS/zeal-$VERSION-1*.src.rpm +``` diff --git a/pkg/copr/zeal.spec b/pkg/copr/zeal.spec new file mode 100644 index 00000000..c03b898d --- /dev/null +++ b/pkg/copr/zeal.spec @@ -0,0 +1,84 @@ +# RPM spec for Zeal, built on Fedora COPR. +# +# The Version tag is a placeholder overridden by the release automation, which +# passes --define "zeal_version " when building the source RPM (see +# .github/workflows/publish-copr.yaml). A plain build with no override produces +# 0.0.0, matching the AUR PKGBUILD convention. CMake already installs the binary, +# .desktop file, AppStream metainfo, and icons under the prefix on Linux +# (see assets/freedesktop/CMakeLists.txt), so %%cmake_install needs no extra glue. +# toml++ is taken from the distro (tomlplusplus-devel). cpp-httplib stays bundled +# in src/contrib because the system versions span too wide a range to rely on; its +# bundled() Provides version is injected by the workflow via +# --define "httplib_version ", read from the bundled header. + +Name: zeal +Version: %{?zeal_version}%{!?zeal_version:0.0.0} +Release: 1%{?dist} +Summary: Simple offline documentation browser + +License: GPL-3.0-or-later +URL: https://zealdocs.org +# URL form is metadata for release builds only; it does not resolve for nightly +# snapshot versions (e.g. 0.8.2^15.gabcdef). Every build supplies the tarball +# locally, so Source0 is never fetched. +Source0: https://github.com/zealdocs/zeal/archive/refs/tags/v%{version}/%{name}-%{version}.tar.gz + +BuildRequires: cmake >= 3.25.1 +BuildRequires: extra-cmake-modules +BuildRequires: gcc-c++ +BuildRequires: ninja-build + +BuildRequires: qt6-qtbase-devel +BuildRequires: qt6-qtsvg-devel +BuildRequires: qt6-qtwebchannel-devel +BuildRequires: qt6-qtwebengine-devel + +BuildRequires: libarchive-devel +BuildRequires: tomlplusplus-devel +BuildRequires: sqlite-devel +BuildRequires: mesa-libGL-devel +BuildRequires: vulkan-headers +BuildRequires: libX11-devel +BuildRequires: libxcb-devel +BuildRequires: xcb-util-keysyms-devel +BuildRequires: libxkbcommon-devel + +Requires: hicolor-icon-theme + +%if "%{?httplib_version}" != "" +Provides: bundled(cpp-httplib) = %{httplib_version} +%else +Provides: bundled(cpp-httplib) +%endif + +%description +Zeal is a simple offline documentation browser inspired by Dash. It offers +access to over 200 docsets covering various libraries and APIs. + + * Quickly search documentation using a customisable global hotkey from any + place in your workspace. + * Search in multiple sets of documentation at once. + * Don't be dependent on your internet connection. + * Integrate Zeal with Emacs, Sublime Text, or Vim. + +%prep +%autosetup -n %{name}-%{version} + +%build +%cmake -GNinja +%cmake_build + +%install +%cmake_install + +%files +%license COPYING +%doc README.md +%{_bindir}/zeal +%{_datadir}/applications/org.zealdocs.zeal.desktop +%{_datadir}/metainfo/org.zealdocs.zeal.appdata.xml +%{_datadir}/icons/hicolor/*/apps/zeal* + +%changelog +* Mon Jun 01 2026 Zeal Release - 0.0.0-1 +- Placeholder entry; the release automation regenerates this for each upload.