Package for the forge's apt and rpm repositories

ed0aa58 · gmorell · 2026-11-05 18:39

5 files +258 -0

Files changed

added .forgejo/workflows/package.yml
+124 −0
@@ -0,0 +1,124 @@
1 +name: package
2 +on:
3 + push:
4 + branches: [master]
5 +
6 +# A newer push supersedes a run still waiting; one in progress finishes,
7 +# so the repository never holds a half-published commit.
8 +concurrency:
9 + group: ${{ github.workflow }}-${{ github.ref }}
10 + cancel-in-progress: false
11 +
12 +# The plasmoid packaged for the forge's own apt and rpm repositories —
13 +# the same shape as gitgud's `cli` job. This file and packaging/build.sh
14 +# are identical in every neiam plasmoid repo.
15 +#
16 +# Everything a plasmoid repo publishes lands in the namespace-wide
17 +# `_all` index too, so one apt line or one .repo file installs any of
18 +# them, beside `gg`:
19 +#
20 +# curl -fsSL $FORGE/pkg/_keys/deb/neiam | sudo tee /etc/apt/keyrings/neiam.asc >/dev/null
21 +# echo "deb [signed-by=/etc/apt/keyrings/neiam.asc] $FORGE/pkg/deb/neiam/_all $(lsb_release -cs) main" \
22 +# | sudo tee /etc/apt/sources.list.d/neiam.list
23 +jobs:
24 + package:
25 + runs-on: self-hosted
26 + steps:
27 + - uses: actions/checkout@v4
28 +
29 + # Same fallback as gitgud's build.yml: CI_FORGE_URL where the
30 + # forge injects it, the runner's own idea of the server otherwise.
31 + - name: Resolve the forge URL
32 + env:
33 + CI_FORGE_URL: ${{ vars.CI_FORGE_URL }}
34 + run: |
35 + set -euo pipefail
36 + forge_url=${CI_FORGE_URL:-${GITHUB_SERVER_URL:-}}
37 +
38 + if [ -z "$forge_url" ]; then
39 + echo "No forge URL available." >&2
40 + echo "Set a CI_FORGE_URL variable on this repository or its org." >&2
41 + exit 1
42 + fi
43 +
44 + echo "FORGE_URL=$forge_url" >> "$GITHUB_ENV"
45 +
46 + # A plasmoid is QML only: one arch-independent package, built once
47 + # per suite for the .deb (the +suite suffix gives each its own
48 + # filename in the pool) and once for the .rpm, which has no suites.
49 + # The suites match what `gg` publishes, so one `_all` line with
50 + # $(lsb_release -cs) finds both.
51 + - name: Build the .deb and .rpm
52 + env:
53 + REPOSITORY: ${{ github.repository }}
54 + SUITES: trixie forky
55 + run: |
56 + set -euo pipefail
57 + command -v jq >/dev/null || { echo "packaging/build.sh needs jq on the runner" >&2; exit 1; }
58 +
59 + # The maintainer address is this forge's own host, as for gitgud.
60 + host=${FORGE_URL#*://}
61 + export MAINTAINER="GitGud CI <noreply@${host%%/*}>"
62 + export HOMEPAGE="$FORGE_URL/$REPOSITORY"
63 +
64 + rm -rf dist
65 + for suite in $SUITES; do
66 + echo "deb $suite $(packaging/build.sh deb "$suite")" >> packages.txt
67 + done
68 + echo "rpm - $(packaging/build.sh rpm)" >> packages.txt
69 + mv packages.txt dist/
70 + cat dist/packages.txt
71 +
72 + # `${{ github.token }}` is minted for this job and may publish to
73 + # this repository's packages and nowhere else — no secret to set up.
74 + - name: Publish
75 + env:
76 + REPOSITORY: ${{ github.repository }}
77 + TOKEN: ${{ github.token }}
78 + run: |
79 + set -euo pipefail
80 + while read -r format suite package; do
81 + echo "publishing $package"
82 + case $format in
83 + deb)
84 + type=application/vnd.debian.binary-package
85 + url="$FORGE_URL/pkg/deb/$REPOSITORY/upload?dist=$suite&component=main" ;;
86 + rpm)
87 + type=application/octet-stream
88 + url="$FORGE_URL/pkg/rpm/$REPOSITORY/upload" ;;
89 + esac
90 + curl --fail-with-body -sS \
91 + -u "gitgud-actions:$TOKEN" \
92 + -H "Content-Type: $type" \
93 + --data-binary "@$package" \
94 + "$url"
95 + echo
96 + done < dist/packages.txt
97 +
98 + - name: How to install it
99 + env:
100 + OWNER: ${{ github.repository_owner }}
101 + run: |
102 + set -euo pipefail
103 + name=$(jq -r '.KPlugin.Id | "plasma-applet-" + (split(".") | last)' metadata.json)
104 + cat <<EOF
105 + # Debian — \$(lsb_release -cs) is trixie or forky
106 + curl -fsSL $FORGE_URL/pkg/_keys/deb/$OWNER | sudo tee /etc/apt/keyrings/$OWNER.asc >/dev/null
107 + echo "deb [signed-by=/etc/apt/keyrings/$OWNER.asc] $FORGE_URL/pkg/deb/$OWNER/_all \$(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/$OWNER.list
108 + sudo apt update && sudo apt install $name
109 +
110 + # Fedora / openSUSE — /etc/yum.repos.d/$OWNER.repo
111 + [$OWNER]
112 + name=$OWNER
113 + baseurl=$FORGE_URL/pkg/rpm/$OWNER/_all
114 + enabled=1
115 + repo_gpgcheck=1
116 + # CI publishes unsigned packages; the forge signs the index.
117 + gpgcheck=0
118 + gpgkey=$FORGE_URL/pkg/_keys/rpm/$OWNER
119 +
120 + sudo dnf install $name # or: sudo zypper install $name
121 +
122 + # A copy installed with kpackagetool6 shadows this one:
123 + kpackagetool6 -t Plasma/Applet -r $(jq -r .KPlugin.Id metadata.json)
124 + EOF
added packaging/build.sh
+134 −0
@@ -0,0 +1,134 @@
1 +#!/usr/bin/env bash
2 +#
3 +# Package this plasmoid as a .deb or .rpm.
4 +#
5 +# packaging/build.sh <deb|rpm> [suite]
6 +#
7 +# Prints the path of the package it built. Everything comes from
8 +# metadata.json, so this file is identical in every neiam plasmoid repo
9 +# — copy it, don't fork it.
10 +#
11 +# A plasmoid is QML and nothing else: there is nothing to compile, so
12 +# one `Architecture: all` / `noarch` package installs on any Plasma 6
13 +# system, and the "build" is laying files out under
14 +# /usr/share/plasma/plasmoids/<Id>/.
15 +#
16 +# suite deb only — appended as `+<suite>`, because the same
17 +# package published to two suites needs two filenames to
18 +# sit in the forge's pool
19 +# MAINTAINER control field (default: the first author in metadata.json)
20 +# HOMEPAGE control field (default: KPlugin.Website)
21 +# NFPM_IMAGE container used when nfpm isn't on PATH
22 +#
23 +# Builds with nfpm if it's installed, otherwise runs it in docker or
24 +# podman — so the runner needs neither dpkg-deb nor rpmbuild.
25 +set -euo pipefail
26 +
27 +format=${1:?usage: build.sh <deb|rpm> [suite]}
28 +suite=${2:-}
29 +case $format in deb | rpm) ;; *) echo "unknown format: $format" >&2; exit 2 ;; esac
30 +if [ "$format" = rpm ] && [ -n "$suite" ]; then
31 + echo "rpm has no suites; drop the second argument" >&2
32 + exit 2
33 +fi
34 +
35 +root=$(cd -- "$(dirname -- "$0")/.." && pwd)
36 +m() { jq -er "$1" "$root/metadata.json"; }
37 +
38 +id=$(m .KPlugin.Id)
39 +# Named from the plugin Id, not the repo: a repo name like
40 +# `lambent_ex-plasmoid` isn't a legal Debian package name.
41 +pkg=plasma-applet-${id##*.}
42 +version=$(m .KPlugin.Version)
43 +
44 +commit=$(git -C "$root" rev-parse HEAD)
45 +# The COMMIT's date, not the clock: the same commit always builds the
46 +# same version, and the .deb and .rpm of one run agree on it.
47 +stamp=$(git -C "$root" show -s --format=%cd --date=format:%Y%m%d%H%M%S HEAD)
48 +SOURCE_DATE_EPOCH=$(git -C "$root" show -s --format=%ct HEAD)
49 +export SOURCE_DATE_EPOCH
50 +
51 +maintainer=${MAINTAINER:-"$(m '.KPlugin.Authors[0].Name') <$(m '.KPlugin.Authors[0].Email')>"}
52 +homepage=${HOMEPAGE:-$(m .KPlugin.Website)}
53 +
54 +# plasma-workspace brings Plasma, Kirigami and Qt Quick with it. These
55 +# are the QML modules it doesn't, added only where a plasmoid imports
56 +# them. rpm gets a file dependency instead: /usr/bin/plasmashell is the
57 +# same path on Fedora and openSUSE though the package names differ.
58 +deb_depends=(plasma-workspace)
59 +uses() { grep -rqE "^[[:space:]]*import $1([[:space:]]|$)" "$root/contents"; }
60 +uses 'QtQuick\.Dialogs' && deb_depends+=(qml6-module-qtquick-dialogs)
61 +uses 'org\.kde\.notification' && deb_depends+=(qml6-module-org-kde-notifications)
62 +uses 'org\.kde\.kcmutils' && deb_depends+=(qml6-module-org-kde-kcmutils)
63 +
64 +out=$root/dist
65 +mkdir -p "$out"
66 +config=$out/nfpm-$format${suite:+-$suite}.yaml
67 +
68 +# Every scalar goes through jq's @json, so a description with a colon
69 +# or a quote is still valid YAML.
70 +y() { jq -rn --arg v "$1" '$v|@json'; }
71 +
72 +cat > "$config" <<EOF
73 +name: $(y "$pkg")
74 +arch: all
75 +platform: linux
76 +version: $(y "$version")
77 +# '~' sorts before the release: 0.5.0~ci… upgrades cleanly to 0.5.0.
78 +prerelease: $(y "ci${stamp}.g${commit:0:12}")
79 +version_metadata: $(y "$suite")
80 +release: "1"
81 +maintainer: $(y "$maintainer")
82 +homepage: $(y "$homepage")
83 +license: GPL-3.0
84 +section: kde
85 +# First line is the synopsis (and the forge's package card); the rest
86 +# is the extended description.
87 +description: $(y "$(m .KPlugin.Name) — $(m .KPlugin.Description)
88 +A Plasma 6 widget, installed system-wide as $id.
89 +Built from commit $commit.")
90 +contents:
91 + - src: ./metadata.json
92 + dst: /usr/share/plasma/plasmoids/$id/metadata.json
93 + file_info: { mode: 0644 }
94 + - src: ./contents/
95 + dst: /usr/share/plasma/plasmoids/$id/contents/
96 + type: tree
97 + - src: ./LICENSE.md
98 + dst: /usr/share/doc/$pkg/copyright
99 + file_info: { mode: 0644 }
100 +overrides:
101 + deb:
102 + depends: $(jq -cn '$ARGS.positional' --args "${deb_depends[@]}")
103 + rpm:
104 + depends: ["/usr/bin/plasmashell"]
105 +EOF
106 +
107 +# nfpm names the file itself; find what it made by listing before and
108 +# after rather than reconstructing its naming rules here.
109 +before=$(ls -1 "$out" | sort)
110 +
111 +rel_config=${config#"$root/"}
112 +if command -v nfpm >/dev/null 2>&1; then
113 + (cd "$root" && nfpm package -f "$rel_config" -p "$format" -t dist/) >&2
114 +else
115 + image=${NFPM_IMAGE:-docker.io/goreleaser/nfpm:v2.41.0}
116 + if command -v docker >/dev/null 2>&1; then
117 + run=(docker run --rm --user "$(id -u):$(id -g)" -v "$root:/w")
118 + elif command -v podman >/dev/null 2>&1; then
119 + # keep-id: files come out owned by the caller, not a subuid.
120 + run=(podman run --rm --userns=keep-id -v "$root:/w:z")
121 + else
122 + echo "need nfpm, docker or podman" >&2
123 + exit 1
124 + fi
125 + "${run[@]}" -e SOURCE_DATE_EPOCH -w /w "$image" \
126 + package -f "$rel_config" -p "$format" -t dist/ >&2
127 +fi
128 +
129 +made=$(comm -13 <(echo "$before") <(ls -1 "$out" | sort) | grep "\.$format\$" || true)
130 +if [ "$(echo "$made" | grep -c .)" != 1 ]; then
131 + # Same version already in dist/ from an earlier run: nfpm overwrote it.
132 + made=$(ls -1t "$out" | grep "\.$format\$" | head -1)
133 +fi
134 +echo "$out/$made"

Parents: 50de478