Package the Linux desktop head as an RPM, and publish a nightly one
ci / build and test (push) Successful in 3m1s
ci / android head (push) Successful in 3m39s
ci / desktop nightly (push) Successful in 1m7s
ci / api image (push) Successful in 22s

The third desktop platform shipped as nothing: a checkout somebody built.
VelopackUpdateChannel already named the two platforms it serves rather than
negating the one it does not, so that a Linux package would be an addition
there rather than something that had silently half-happened. This is it, and
it is deliberately not a Velopack channel.

There is no updater in the package at all. On this platform dnf is already an
updater the user trusts, audits and controls, and a second one answering to
this project instead is the capability ADR 0013 rule 4 spends a page keeping
unsettable. The cost is real and is written down rather than glossed: nothing
tells a Linux user that a newer build exists, because there is no repository
to tell them.

Rule 3 is untouched. The build job packages the release-shaped RPM on every
main build and every tag and uploads nothing, exactly as it does for Windows
and macOS; the desktop job packages the nightly-shaped one and puts it on the
rolling nightly-desktop release beside the Windows feed. Two RPMs with
different names owning different paths cannot replace one another, so rule 9's
separations come almost free here — the profile split still belongs to the
build, through DodoChannel.

vpk verifies that what it packs actually calls VelopackApp.Build().Run(), and
nothing here does that for us, so scripts/ci-linux-rpm.sh makes its own
assertions: it asks the finished package what it requires, what it provides,
what it holds and what modes those carry. Two of those checks exist because
the first realistic payload tripped them.

  - .NET's libmscordbi.so links libmscordaccore.so. With bundled provides
    excluded — which they must be, or this package would advertise libcoreclr
    to the whole system — that produced a dependency no host on earth can
    satisfy. The requires exclusion is computed from the payload rather than
    listed, because a list of Microsoft's internal file names goes stale in
    the direction that yields an uninstallable package.

  - cp -a inherits the publish directory's modes, and off the wrong filesystem
    that meant a world-writable /opt/DodoSSH: any local account able to
    replace the SSH client every other account runs. For an application
    holding a team's credentials in plaintext that is the trust model walked
    around rather than attacked. Every mode is now set absolutely.

Verified as far as a runner can: both packages install side by side on Fedora
44, resolve webkit2gtk4.1 2.52.5 — the backend MainWindow records as measured
— keep separate launchers, icons and profiles, and dnf remove takes one tree
and leaves the other. 0.0.1~alpha.0.143 sorts below both 0.0.1 and the next
height. What no runner can answer is Phase 19, which is new and unwalked.

RHEL 9 is deliberately not claimed. It ships WebKitGTK's 4.0 line under
libwebkit2gtk-4.0.so.37, and an or-branch for it was written and then removed:
nobody has run the head there, and dnf refusing with "nothing provides
libwebkit2gtk-4.1.so.0" is a better answer than an install that draws a blank
terminal. Same sentence, and the same reason, as the missing Intel Mac.

x86_64 only, for that reason and not a different one.
This commit is contained in:
jaap-jan committed 2026-08-22 16:43:38 +02:00
1 parent 1970e20e56
commit 1bf79c7c26
12 files changed
+1015 -5

No files matched your search

+127 -1
View File
@@ -462,6 +462,55 @@ jobs:
echo "Bundled DodoSSH ${{ steps.winpack.outputs.packVersion }} for osx-arm64."
# ◆ AND THE THIRD DESKTOP PLATFORM, WHICH IS THE ONE THIS RUNNER ACTUALLY IS.
#
# Same argument as the two steps above and with none of their cross-compiling: this job runs on
# Linux, so the linux-x64 publish here is a native one. What it proves that the others cannot is
# the linux-x64 restore graph — libsodium, SkiaSharp and HarfBuzzSharp all ship native assets per
# RID, and a package that resolves for win-x64 and has no linux-x64 asset fails here.
#
# The runner is still Alpine and the package targets glibc distributions, which is fine and is not
# glossed over: scripts/ci-linux-rpm.sh explains why rpmbuild composing an RPM for one libc on a
# host running another is correct rather than lucky.
#
# ◆ IT IS ALSO WHERE THE PACKAGING ITSELF IS PROVED, WHICH ON THIS PLATFORM IS NOT A FORMALITY.
#
# The Windows and macOS steps hand a directory to vpk and vpk decides what the package needs.
# There is no vpk here — Velopack's Linux target is an AppImage and this is a distribution
# package, updated by dnf rather than by the application — so the dependency metadata is
# generated by rpm from the published tree, and scripts/ci-linux-rpm.sh interrogates the result
# rather than trusting an exit code. Every check in there is a way a package can be built
# successfully and then refuse to install on every host in the world; one of them was written
# because .NET's own libmscordbi.so links libmscordaccore.so, which is exactly that.
#
# Release-shaped rather than nightly-shaped — package name dodossh, tree at /opt/DodoSSH — so
# that what is proved on every main build is the packaging the release channel would use. The
# desktop job below builds the nightly-shaped one and is the only job that publishes either.
#
# ◆ NOTHING IS UPLOADED, FOR THE REASON THE WINDOWS STEP GIVES.
#
# RUNNER_TEMP, dying with the job. An RPM is if anything the easiest of the three to pass on
# casually — one file, no installer to click through, `dnf install` and done — which is a reason
# to keep it off a downloads page rather than a reason to put it on one. ADR 0013 rule 3.
- name: publish and package the linux desktop client
if: github.event_name != 'pull_request'
run: |
set -euo pipefail
# RestoreLockedMode=false for the RID, exactly as the two publishes above do — see the long
# note on the win-x64 one. This checkout is thrown away, so the lock files it rewrites go
# nowhere.
dotnet publish src/DodoSSH.Client.App/DodoSSH.Client.App.csproj \
--configuration Release --runtime linux-x64 --self-contained true \
-p:RestoreLockedMode=false \
--output "$RUNNER_TEMP/linux-x64"
scripts/ci-linux-rpm.sh \
--payload "$RUNNER_TEMP/linux-x64" \
--version '${{ steps.winpack.outputs.packVersion }}' \
--channel release \
--out "$RUNNER_TEMP/linux-rpm"
# This includes the end-to-end suite, which starts PostgreSQL, Keycloak and an OpenSSH
# server through Testcontainers and runs the API as a child process — so it needs a
# Docker daemon and gets one here. That is why the tests run on ubuntu rather than
@@ -1048,6 +1097,45 @@ jobs:
ls -la "$RUNNER_TEMP/nightly-releases"
# ◆ AND THE LINUX NIGHTLY, WHICH IS A PACKAGE RATHER THAN A FEED.
#
# Everything above this step is Velopack: an installer, an index file and a client that polls it.
# This one is an RPM, and the difference is not a gap in the work. VelopackUpdateChannel refuses
# every platform that is not Windows or macOS — deliberately, and its comment says why — so a
# Linux build has no self-updater to give a channel to. What it has instead is dnf, which is a
# mechanism the user already trusts and already controls, and build/linux/dodossh.spec argues
# that at length.
#
# So there is no rolling *feed* here, only a rolling *file*: the newest nightly RPM sits on the
# same release as the Windows nightly and somebody who wants it downloads it. That is a worse
# deal than the Windows nightly gets and it is the honest one — the alternative is a package
# repository, which is a deployment this project does not have.
#
# DodoChannel=nightly for the same reason the win-x64 publish above sets it: it is what
# ClientPaths reads to suffix the profile directory, so the nightly RPM and a release RPM
# installed beside it share no cache, no outbox and no device key. The package name, the tree
# under /opt and the desktop entry are the packaging half of that separation and are the
# script's `--channel nightly` — see the note there for why it is four things and not one.
- name: publish and package the linux nightly
id: linuxpack
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
set -euo pipefail
dotnet publish src/DodoSSH.Client.App/DodoSSH.Client.App.csproj \
--configuration Release --runtime linux-x64 --self-contained true \
-p:RestoreLockedMode=false \
-p:DodoChannel=nightly \
-p:MinVerVersionOverride="$VERSION" \
--output "$RUNNER_TEMP/nightly-linux-x64"
scripts/ci-linux-rpm.sh \
--payload "$RUNNER_TEMP/nightly-linux-x64" \
--version "$VERSION" \
--channel nightly \
--out "$RUNNER_TEMP/nightly-rpm"
# ◆ PUBLISHING IT, AND WHAT THAT CAPABILITY IS.
#
# Whoever can write a release here can put a build on every nightly desktop, because Velopack fetches
@@ -1145,7 +1233,45 @@ jobs:
fi
done
echo "Published nightly desktop $VERSION as $TAG"
# ◆ THE RPM GOES ONTO THE RELEASE vpk JUST MADE, RATHER THAN ONTO ONE OF ITS OWN.
#
# One rolling release carrying both heads, because they are one nightly: the same commit,
# the same version, and one page a person looks at to find out what is on main. A second tag
# would double the delete-and-recreate dance this step already does, and would let the two
# drift apart on a run where one of them failed.
#
# It has to happen here, after `vpk upload`, and not in a step before it. vpk deletes and
# recreates this release, so an RPM uploaded first would be attached to something that no
# longer exists by the time the job ends — silently, because the delete succeeds.
#
# curl rather than vpk, unlike the assets above, for the reason the android job gives about
# its APK: this is one file the release simply carries. There is no index to write and no
# format that belongs to a tool.
release="$(printf '%s' "$published" | release_id)"
if [ -z "$release" ]; then
echo "The release was published and its id could not be read back:" >&2
printf '%s\n' "$published" >&2
exit 1
fi
rpm='${{ steps.linuxpack.outputs.rpm }}'
rpmName='${{ steps.linuxpack.outputs.rpmName }}'
echo "Uploading $rpmName ($(wc -c < "$rpm") bytes)"
curl -fsS -X POST -H "$auth" \
-F "attachment=@$rpm" \
"$api/releases/$release/assets?name=$rpmName" >/dev/null
# Asked for back, exactly as the Velopack assets above are and for the same history: the
# android job once created a release, uploaded nothing, and reported success for every
# upload it never made.
withRpm="$(curl -fsS -H "$auth" "$api/releases/$release")"
if ! printf '%s' "$withRpm" | grep -qF "\"name\":\"$rpmName\""; then
echo "The release was created but $rpmName is not on it:" >&2
printf '%s\n' "$withRpm" >&2
exit 1
fi
echo "Published nightly desktop $VERSION as $TAG, with $rpmName"
image:
name: api image