Build the .deb and .rpm in CI, and publish them to the forge

5f0f476 · Gabriel Morell · 2026-10-15 02:18

5 files +356 -4
Message
{commit_body(@commit)}

Files changed

added .github/workflows/rust-ci.yml
+352 −0
@@ -0,0 +1,352 @@
1 +name: Rust CI
2 +
3 +on:
4 + push:
5 + branches: [ master ]
6 + tags: [ 'v*' ]
7 + pull_request:
8 + branches: [ master ]
9 +
10 +env:
11 + CARGO_TERM_COLOR: always
12 +
13 +jobs:
14 + build:
15 + strategy:
16 + matrix:
17 + include:
18 + - runner: arc-runner-set
19 + arch: x86_64
20 + - runner: ubuntu-24.04-arm
21 + arch: aarch64
22 + runs-on: ${{ matrix.runner }}
23 + permissions:
24 + contents: read
25 + packages: write
26 +
27 + steps:
28 + - uses: actions/checkout@v4
29 +
30 + - name: Install system dependencies (Linux)
31 + if: runner.os == 'Linux'
32 + run: |
33 + # Nothing here links a C library -- rumqttc speaks rustls and zbus
34 + # is pure Rust -- so a linker and pkg-config are all a bare
35 + # self-hosted container is missing.
36 + if command -v apt-get &> /dev/null; then
37 + sudo apt-get update
38 + sudo apt-get install -y build-essential pkg-config
39 + elif command -v dnf &> /dev/null; then
40 + sudo dnf install -y gcc pkgconfig
41 + elif command -v yum &> /dev/null; then
42 + sudo yum install -y gcc pkgconfig
43 + else
44 + echo "Package manager not found. Please install a C toolchain and pkg-config manually."
45 + exit 1
46 + fi
47 +
48 + - name: Install Rust
49 + uses: dtolnay/rust-toolchain@stable
50 + with:
51 + toolchain: "1.92"
52 + components: rustfmt, clippy
53 +
54 + - name: Setup Rust cache
55 + uses: Swatinem/rust-cache@v2
56 +
57 + - name: Build
58 + run: cargo build --verbose
59 +
60 + - name: Run tests
61 + run: cargo test --verbose
62 +
63 + - name: Check formatting
64 + run: cargo fmt -- --check
65 +
66 + - name: Run clippy
67 + run: cargo clippy -- -D warnings
68 +
69 + - name: Check
70 + run: cargo check
71 +
72 + - name: Build release artifact
73 + run: cargo build --release --verbose
74 +
75 + - name: Strip binary
76 + run: strip target/release/apollos-dbus
77 +
78 + - name: Install cargo-deb
79 + run: cargo install cargo-deb
80 +
81 + - name: Install cargo-generate-rpm
82 + run: cargo install cargo-generate-rpm
83 +
84 + # Every CI build has to carry a distinct version, or the second
85 + # one publishes the same filename with different bytes and the
86 + # registry correctly refuses it with a 409 — and no machine that
87 + # installed the first ever sees the second.
88 + #
89 + # `+` rather than `-`: RPM forbids `-` in a version outright, and
90 + # in both ecosystems `0.1.2+dev.<stamp>` sorts *after* 0.1.2,
91 + # which is what you want from a repository you added in order to
92 + # run the latest build. A tag publishes the bare Cargo version.
93 + #
94 + # One stamp for the whole job, so the .deb and the .rpm from a
95 + # single run agree about what they are.
96 + - name: Stamp the package version
97 + id: version
98 + run: |
99 + set -euo pipefail
100 + base=$(grep -m1 '^version = ' Cargo.toml | cut -d'"' -f2)
101 +
102 + if [ "${GITHUB_REF_TYPE:-}" = "tag" ]; then
103 + version="$base"
104 + else
105 + version="$base+dev.$(date -u +%s)"
106 + fi
107 +
108 + echo "version: $version"
109 + echo "version=$version" >> "$GITHUB_OUTPUT"
110 +
111 + # Rewritten after the release build, so the binary is cached
112 + # on the unstamped manifest and only the packaging sees this.
113 + sed -i "0,/^version = .*/s//version = \"$version\"/" Cargo.toml
114 +
115 + - name: Build test Debian package
116 + run: cargo deb --no-build --no-strip
117 +
118 + - name: Build RPM package
119 + run: cargo generate-rpm
120 +
121 + # `vars.CI_FORGE_URL` is what the forge injects into a run; fall
122 + # back to what the runner already knows, and say so plainly if
123 + # neither resolves rather than handing curl an empty URL.
124 + - name: Resolve the forge URL
125 + env:
126 + CI_FORGE_URL: ${{ vars.CI_FORGE_URL }}
127 + run: |
128 + set -euo pipefail
129 + forge_url=${CI_FORGE_URL:-${GITHUB_SERVER_URL:-}}
130 +
131 + if [ -z "$forge_url" ]; then
132 + echo "No forge URL available." >&2
133 + echo "Set a CI_FORGE_URL variable on this repository or its org." >&2
134 + exit 1
135 + fi
136 +
137 + echo "FORGE_URL=$forge_url" >> "$GITHUB_ENV"
138 +
139 + # `${{ github.token }}` is minted for this job and authorized to
140 + # publish to THIS repository's packages and nowhere else, so
141 + # there is no secret to create first.
142 + #
143 + # Pull requests don't publish: a PR build is a check, and a
144 + # registry that carries every proposed change is not a registry
145 + # anyone can install from.
146 + - name: Publish the .deb and the .rpm to the forge
147 + if: github.event_name != 'pull_request'
148 + env:
149 + REPOSITORY: ${{ github.repository }}
150 + TOKEN: ${{ github.token }}
151 + run: |
152 + set -euo pipefail
153 +
154 + for deb in target/debian/*.deb; do
155 + echo "publishing $deb"
156 + curl --fail-with-body -sS \
157 + -u "gitgud-actions:$TOKEN" \
158 + -H "Content-Type: application/vnd.debian.binary-package" \
159 + --data-binary "@$deb" \
160 + "$FORGE_URL/pkg/deb/$REPOSITORY/upload?dist=stable&component=main"
161 + echo
162 + done
163 +
164 + for rpm in target/generate-rpm/*.rpm; do
165 + echo "publishing $rpm"
166 + curl --fail-with-body -sS \
167 + -u "gitgud-actions:$TOKEN" \
168 + -H "Content-Type: application/x-rpm" \
169 + --data-binary "@$rpm" \
170 + "$FORGE_URL/pkg/rpm/$REPOSITORY/upload"
171 + echo
172 + done
173 +
174 + - name: How to install it
175 + if: github.event_name != 'pull_request'
176 + env:
177 + REPOSITORY: ${{ github.repository }}
178 + OWNER: ${{ github.repository_owner }}
179 + run: |
180 + cat <<EOF
181 + # Debian / Ubuntu
182 + curl -fsSL $FORGE_URL/pkg/_keys/deb/$OWNER | sudo tee /etc/apt/keyrings/$OWNER.asc >/dev/null
183 + echo "deb [signed-by=/etc/apt/keyrings/$OWNER.asc] $FORGE_URL/pkg/deb/$OWNER/_all stable main" | sudo tee /etc/apt/sources.list.d/$OWNER.list
184 + sudo apt update && sudo apt install apollos-dbus
185 +
186 + # Fedora / RHEL / openSUSE — /etc/yum.repos.d/$OWNER.repo
187 + [$OWNER]
188 + name=$OWNER
189 + baseurl=$FORGE_URL/pkg/rpm/$OWNER/_all
190 + enabled=1
191 + repo_gpgcheck=1
192 + gpgcheck=0
193 + gpgkey=$FORGE_URL/pkg/_keys/rpm/$OWNER
194 + EOF
195 +
196 + - name: Upload test artifact
197 + uses: actions/upload-artifact@v4
198 + # Artifact storage is a GitHub feature; this forge has none, so
199 + # these steps have nothing to upload to. They used to run and
200 + # fail with `continue-on-error`, which is a red step on every
201 + # single run — and a build that is always a bit red teaches
202 + # everyone to stop reading it. Skipping says the same thing
203 + # honestly and leaves the badge meaningful.
204 + #
205 + # The packages are published to the forge's registries instead,
206 + # which is the thing anyone actually installs from.
207 + if: ${{ startsWith(github.server_url, 'https://github.com') }}
208 + with:
209 + name: apollos-dbus-test-${{ matrix.arch }}-linux
210 + path: target/release/apollos-dbus
211 +
212 + - name: Upload test Debian package
213 + uses: actions/upload-artifact@v4
214 + if: ${{ startsWith(github.server_url, 'https://github.com') }}
215 + with:
216 + name: apollos-dbus-test-${{ matrix.arch }}-linux.deb
217 + path: target/debian/*.deb
218 +
219 + - name: Upload test RPM package
220 + uses: actions/upload-artifact@v4
221 + if: ${{ startsWith(github.server_url, 'https://github.com') }}
222 + with:
223 + name: apollos-dbus-test-${{ matrix.arch }}-linux.rpm
224 + path: target/generate-rpm/*.rpm
225 +
226 + # A D-Bus service with a systemd user unit: Linux is the only platform
227 + # it means anything on, so no macOS or Windows builds.
228 + release:
229 + name: Release
230 + if: startsWith(github.ref, 'refs/tags/v')
231 + needs: build
232 + permissions:
233 + contents: write
234 + packages: write
235 + strategy:
236 + matrix:
237 + include:
238 + - os: ubuntu-latest
239 + runner: arc-runner-set
240 + name: apollos-dbus-x86_64-linux-gnu.tar.gz
241 + rust: "1.92"
242 + - os: ubuntu-24.04-arm
243 + runner: ubuntu-24.04-arm
244 + name: apollos-dbus-aarch64-linux-gnu.tar.gz
245 + rust: "1.92"
246 + runs-on: ${{ matrix.runner }}
247 +
248 + steps:
249 + - uses: actions/checkout@v4
250 +
251 + - name: Install system dependencies
252 + run: |
253 + # Nothing here links a C library -- rumqttc speaks rustls and zbus
254 + # is pure Rust -- so a linker and pkg-config are all a bare
255 + # self-hosted container is missing.
256 + if command -v apt-get &> /dev/null; then
257 + sudo apt-get update
258 + sudo apt-get install -y build-essential pkg-config
259 + elif command -v dnf &> /dev/null; then
260 + sudo dnf install -y gcc pkgconfig
261 + elif command -v yum &> /dev/null; then
262 + sudo yum install -y gcc pkgconfig
263 + else
264 + echo "Package manager not found. Please install a C toolchain and pkg-config manually."
265 + exit 1
266 + fi
267 +
268 + - name: Install Rust
269 + uses: dtolnay/rust-toolchain@stable
270 + with:
271 + toolchain: ${{ matrix.rust }}
272 +
273 + - name: Setup Rust cache
274 + uses: Swatinem/rust-cache@v2
275 + with:
276 + key: ${{ matrix.os }}-${{ matrix.rust }}
277 +
278 + - name: Build release binary
279 + run: cargo build --release --verbose
280 +
281 + - name: Strip binary
282 + run: strip target/release/apollos-dbus
283 +
284 + - name: Create release archive
285 + run: tar -czf ${{ matrix.name }} -C target/release apollos-dbus
286 +
287 + - name: Install cargo-deb
288 + run: cargo install cargo-deb
289 +
290 + - name: Build Debian package
291 + run: cargo deb --no-build --no-strip
292 +
293 + - name: Install cargo-generate-rpm
294 + run: cargo install cargo-generate-rpm
295 +
296 + - name: Build RPM package
297 + run: cargo generate-rpm
298 +
299 + # Artifact storage is a GitHub feature; see the build job.
300 + - name: Upload release archive
301 + uses: actions/upload-artifact@v4
302 + if: ${{ startsWith(github.server_url, 'https://github.com') }}
303 + with:
304 + name: ${{ matrix.name }}
305 + path: ${{ matrix.name }}
306 +
307 + - name: Upload Debian package
308 + uses: actions/upload-artifact@v4
309 + if: ${{ startsWith(github.server_url, 'https://github.com') }}
310 + with:
311 + name: apollos-dbus-${{ matrix.os }}.deb
312 + path: target/debian/*.deb
313 +
314 + - name: Upload RPM package
315 + uses: actions/upload-artifact@v4
316 + if: ${{ startsWith(github.server_url, 'https://github.com') }}
317 + with:
318 + name: apollos-dbus-${{ matrix.os }}.rpm
319 + path: target/generate-rpm/*.rpm
320 +
321 + create-release:
322 + name: Create Release
323 + runs-on: arc-runner-set
324 + # Makes a *GitHub* Release out of artifacts a GitHub runner uploaded;
325 + # off GitHub there is nothing to download. Tagged builds still publish
326 + # their packages to the forge from the build job.
327 + if: >-
328 + startsWith(github.ref, 'refs/tags/v') &&
329 + startsWith(github.server_url, 'https://github.com')
330 + needs: release
331 + permissions:
332 + contents: write
333 + steps:
334 + - uses: actions/checkout@v4
335 +
336 + - name: Download all artifacts
337 + uses: actions/download-artifact@v4
338 + with:
339 + path: artifacts
340 +
341 + - name: Create GitHub Release
342 + uses: softprops/action-gh-release@v1
343 + with:
344 + files: |
345 + artifacts/*/*.tar.gz
346 + artifacts/*/*.deb
347 + artifacts/*/*.rpm
348 + draft: false
349 + prerelease: ${{ contains(github.ref_name, '-') }}
350 + generate_release_notes: true
351 + env:
352 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
modified Cargo.lock
+3 −1
@@ -89,7 +89,9 @@ dependencies = [
89 89
90 90 [[package]]
91 91 name = "apollos-types"
92 −version = "0.2.0"
92 +version = "0.3.0"
93 +source = "registry+https://github.com/rust-lang/crates.io-index"
94 +checksum = "deb1aabd55ffe8ffa02e43bd8e35bd4e7fcd148ee43b7e4b795906897c565385"
93 95 dependencies = [
94 96 "serde",
95 97 "serde_json",
modified Cargo.toml
+1 −3
@@ -8,9 +8,7 @@ license = "GPL-3.0-only"
8 8
9 9 [dependencies]
10 10 anyhow = "1"
11 −# The sibling checkout until the release carrying route names, colours and
12 −# free bikes is on crates.io; 0.2.0 there predates them.
13 −apollos-types = { path = "../apollos-types", version = "0.2" }
11 +apollos-types = "0.3"
14 12 clap = { version = "4.5", features = ["derive", "env"] }
15 13 dirs = "6"
16 14 gethostname = "1"

Parents: d13c936