Merge branch 'claude/android-release'
ci / build and test (push) Successful in 2m4s
ci / android head (push) Successful in 3m24s
ci / api image (push) Successful in 22s

This commit is contained in:
2026-08-05 21:57:05 +02:00
4 changed files with 224 additions and 25 deletions
+160 -16
View File
@@ -147,13 +147,25 @@ jobs:
# green, the package is cut, and the symptom arrives weeks later as clients that never update.
#
# -getProperty evaluates without building, so this costs a second and runs before the build.
#
# ◆ -t:MinVer IS NOT OPTIONAL, AND WITHOUT IT THIS STEP FAILED EVERY TAG BUILD.
#
# -getProperty on its own evaluates the project and runs no targets, and MinVer sets Version
# from inside a target — so what came back was the SDK's default 1.0.0, for any tag, on any
# commit, in a clean checkout with every tag present. The step would have failed the first
# release ever cut and blamed fetch-depth for it, which is the one explanation that is not it.
# Naming a target makes -getProperty report the value after that target has run.
#
# Never exercised, because no v* tag exists yet: this whole step is `if:` a tag ref. It is the
# shape of bug a guard is prone to — the guard is the thing nothing tests, and it only speaks
# on the one build nobody is watching, which is the reason this step exists at all.
- name: the tag and the version agree
if: startsWith(github.ref, 'refs/tags/v')
run: |
set -euo pipefail
tag="${GITHUB_REF#refs/tags/v}"
declared="$(dotnet msbuild src/DodoSSH.Client.App/DodoSSH.Client.App.csproj \
-getProperty:Version -nologo | tr -d '[:space:]')"
-getProperty:Version -t:MinVer -nologo | tr -d '[:space:]')"
if [ "$tag" != "$declared" ]; then
echo "The tag says v$tag and MinVer computed $declared." >&2
@@ -214,8 +226,7 @@ jobs:
# The one build shape nothing else here exercises: a self-contained RID-specific publish. Its
# failure mode is a restore graph or a native asset that resolves for net10.0 and not for
# net10.0/win-x64, which nobody would see until a person was halfway through cutting a release
# on a Windows machine. vpk can pack a Windows package from Linux; only signing needs Windows,
# and this repository signs nothing yet, so proving the publish here is worth the minutes.
# on a Windows machine.
#
# RestoreLockedMode=false for this command only, and it is not a loosened gate. The committed
# lock files are deliberately RID-free: declaring win-x64 on the desktop head writes a
@@ -224,20 +235,144 @@ jobs:
# under locked mode, so the image job would fail NU1004. The gate is the locked solution
# restore at the top of this job, which is unchanged.
#
# It rewrites the lock files as it goes; nothing after this step reads them, and the runner's
# It rewrites the lock files as it goes; nothing after it reads them, and the runner's
# checkout is thrown away. The release script does the same thing and puts them back, because
# there the tree is somebody's working copy.
#
# After the tests rather than before them, so a red suite does not first spend a hundred
# megabytes pulling a win-x64 runtime pack. main and tags only, for the same reason: a break
# found by the person about to release is found early enough.
- name: the windows publish still resolves
#
# The output directory is read by the packaging step below, so the two names have to agree.
- name: publish the windows desktop client
if: github.event_name != 'pull_request'
run: >
dotnet publish src/DodoSSH.Client.App/DodoSSH.Client.App.csproj
--configuration Release --runtime win-x64 --self-contained true
-p:RestoreLockedMode=false
--output "$RUNNER_TEMP/win-x64-check"
--output "$RUNNER_TEMP/win-x64"
# ◆ THE WINDOWS INSTALLER IS BUILT HERE, ON LINUX, AND IS DELIBERATELY THROWN AWAY.
#
# Packaging rather than only publishing, for the same reason the android job packages an APK
# nobody installs: the failures a release is most exposed to are the ones only the packager
# finds. vpk opens the published binaries and verifies that the main executable really calls
# VelopackApp.Build().Run() — so a refactor that drops that call, which compiles, tests green
# and produces an application that silently never updates itself, fails here instead of
# shipping. A publish alone cannot see that, and the person who would otherwise see it first is
# the one midway through a release.
#
# ◆ NOT WITH scripts/release-windows.ps1. That script is a person's release procedure and holds
# things a runner must not have and must not skip — it refuses a dirty tree, insists HEAD is
# tagged, downloads the previous release to build deltas against, and in its second phase asks
# for the forge token. Calling it from here would mean either weakening it with CI switches or
# having CI meet conditions that only make sense at a desk. So the packaging arguments below
# are written out again in shell, and the duplication is accepted and stated: the pack id, the
# title, the authors, the channel and the icon are a contract with VelopackUpdateChannel and
# with every installed client. Change one, change the other.
#
# ◆ AND CROSS-COMPILING NEEDS THE OS DIRECTIVE, WHICH IS EASY TO MISS.
#
# vpk picks its target from the host: on Linux, plain `vpk pack` builds a Linux release and
# refuses a Windows RID outright — "the target rid must be Linux (actually was Windows)". The
# bracketed [win] before the verb is what turns it into a cross-compile, and it must be quoted
# or the shell reads it as a glob matching any one of w, i and n. With it, the runner writes
# DodoSSH.Desktop-win-Setup.exe, the portable zip, the full .nupkg and releases.win.json,
# exactly as a Windows machine does. Only signing needs Windows tooling, and nothing here is
# signed — see ADR 0013 rule 8 for the trigger that changes.
#
# ◆ NOTHING IS UPLOADED, AND NOTHING IS EVEN OFFERED FOR DOWNLOAD.
#
# The output goes to RUNNER_TEMP and dies with the job. That is the whole point rather than an
# omission: ADR 0013 rule 3 puts the capability to ship somebody a build on a machine which is
# not a runner, because Velopack clients apply what their feed serves without verifying a
# signature. Publishing a CI-built package as a workflow artefact would not cross that line by
# itself, and it would put an installer nobody has run in a place that invites passing it on —
# so it is not done either. What reaches users is built, installed and walked through Phase 16
# of docs/manual-checks.md by a person first.
- name: package the windows desktop client
if: github.event_name != 'pull_request'
run: |
set -euo pipefail
# vpk comes from the tool manifest, at the version committed there, which is the same one
# the release script uses. Restored here rather than in a step of its own because this is
# the only job that needs a tool at all.
dotnet tool restore
# The same value the release script packs with, read the same way. A version vpk disagrees
# with the assemblies about is a client that compares wrongly, so it is read from MSBuild
# rather than reconstructed from the ref — a main build has no tag to reconstruct from
# anyway, and MinVer's answer there carries the prerelease height that tells two of them
# apart.
#
# -t:MinVer for the reason spelled out on the tag check above: without a target named,
# -getProperty answers 1.0.0 and every package would carry the same version forever, which
# Velopack reads as "no update available".
version="$(dotnet msbuild src/DodoSSH.Client.App/DodoSSH.Client.App.csproj \
-getProperty:Version -t:MinVer -nologo | tr -d '[:space:]')"
if [ -z "$version" ]; then
echo "Could not read the version from MSBuild." >&2
exit 1
fi
# ◆ AND A FLOOR UNDER IT, BECAUSE MINVER'S PRE-FIRST-TAG ANSWER IS ONE vpk REFUSES.
#
# Until a v* tag exists MinVer answers 0.0.0-alpha.0.N — correct, documented, and rejected
# outright by vpk: "Invalid package version '0.0.0-alpha.0.143', it must be >= 0.0.1". So
# today, and on every main build until the first release, packing the true version cannot
# work at all.
#
# The patch digit is lifted and the prerelease part kept, so the stand-in still sorts below
# anything real and still differs between commits. Sound only because this package is
# thrown away: what is being proved here is that the desktop client packages, and no client
# will ever see this number. The release script is not given a floor and must not be — its
# version is the one users compare against, and if MinVer ever answered 0.0.0 there the
# right outcome is the refusal.
packVersion="$version"
case "$packVersion" in
0.0.0*)
packVersion="0.0.1${packVersion#0.0.0}"
echo "MinVer says $version, which vpk will not pack; packaging as $packVersion."
echo "This disappears the moment a v* tag exists, and never reaches a release."
;;
esac
releases="$RUNNER_TEMP/win-releases"
# --skip-updates because vpk otherwise asks nuget.org whether a newer vpk exists on every
# run, which is a network call whose answer this build must not act on: the version that
# packs is the one in the tool manifest.
#
# No `vpk download`, where the release script has one. That fetches the previous release so
# deltas can be built against it, which needs the feed and produces an artefact nobody
# applies. A full package proves the packaging.
dotnet vpk '[win]' pack \
--skip-updates \
--packId DodoSSH.Desktop \
--packVersion "$packVersion" \
--packDir "$RUNNER_TEMP/win-x64" \
--packTitle DodoSSH \
--packAuthors DodoTech \
--mainExe DodoSSH.exe \
--icon src/DodoSSH.Client.App/Assets/dodossh.ico \
--runtime win-x64 \
--channel win \
--outputDir "$releases"
# Asked for rather than assumed. vpk fails loudly and this costs a line, but the artefact
# whose existence is the entire question — a Windows setup stub built on a machine that is
# not Windows — is worth naming rather than inferring from an exit code.
setup="$releases/DodoSSH.Desktop-win-Setup.exe"
if [ ! -s "$setup" ]; then
echo "vpk reported success and there is no setup executable at $setup." >&2
ls -la "$releases" >&2 || true
exit 1
fi
ls -la "$releases"
echo "Packaged DodoSSH $packVersion for win-x64, from a build MinVer calls $version."
# 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
@@ -932,18 +1067,27 @@ jobs:
if: always() && github.event_name != 'pull_request'
run: docker logout registry-docker.dodotech.cloud
# There is no job here that publishes the desktop client, and there is not going to be one. Two
# independent reasons, and both need saying because someone will fix one and think they are done.
# There is no job here that publishes the desktop client, and there is not going to be one. It is built
# and packaged here — the two steps at the end of the build job — and what is withheld is only the
# upload.
#
# The smaller one is mechanical: vpk stamps and embeds the Setup.exe and Update.exe stubs with Windows
# tooling, and every job in this file is runs-on: [linux]. A Windows runner would answer that.
# ◆ ONE REASON, WHERE THIS ONCE CLAIMED TWO, AND THE SECOND WAS NOT TRUE.
#
# The larger one is that a Windows runner would not answer the other. Velopack clients fetch from the
# release feed and do not verify a package signature when they apply it, so whoever can write a release
# on this repository can publish an update that every installed client downloads and runs. That is the
# same capability as the signing key, reached through a different door — and docs/adr/0011 rule 1 puts
# that capability on a machine which is not a runner, because a workflow secret is held by everyone who
# can change a workflow file. See docs/adr/0013-desktop-distribution-and-updates.md.
# It used to say that vpk stamps and embeds the Setup.exe and Update.exe stubs with Windows tooling and
# so could not run on a Linux runner, and offered that as the smaller of two reasons. It was wrong, and
# worth recording as wrong because it is the kind of claim that discourages anybody from trying: vpk
# cross-compiles when told to, `vpk [win] pack` on this runner builds the setup stub, the portable zip
# and the .nupkg, and only signing needs Windows — which this repository does not do yet. That is what
# the packaging step now does on every main and tag build.
#
# The reason that stands is the one that was never mechanical. Velopack clients fetch from the release
# feed and do not verify a package signature when they apply it, so whoever can write a release on this
# repository can publish an update that every installed client downloads and runs. That is the same
# capability as the signing key, reached through a different door — and docs/adr/0011 rule 1 puts that
# capability on a machine which is not a runner, because a workflow secret is held by everyone who can
# change a workflow file. A Windows runner would never have answered it, which is why fixing the
# mechanical half changes nothing about the division of labour.
# See docs/adr/0013-desktop-distribution-and-updates.md.
#
# What cuts a release is scripts/release-windows.ps1, run by a person. What this file does is prove the
# thing still builds and packages, which is the same division of labour the android job above already