Package the Windows client in CI, on the runner that could not

The build job published a win-x64 tree and stopped there, so the half of a
release that fails in ways a compile cannot see was proved by nobody until a
person was midway through cutting one. It now packs as well: vpk opens the
published binaries and verifies the main executable really calls
VelopackApp.Build().Run(), which is the check worth having — a refactor that
drops that call compiles, tests green, and produces an application that silently
never updates itself.

The file said this was impossible on Linux, and also said it was fine, in
comments forty lines apart. The claim that vpk needs Windows tooling to stamp the
Setup.exe stub is the one that was wrong: vpk cross-compiles when told to, and
the telling is a bracketed directive before the verb rather than a flag. Plain
`vpk pack --runtime win-x64` on a Linux host refuses outright and says so in the
message that names the fix. `[win]` must be quoted, or the shell reads it as a
glob matching any one of w, i and n. Only signing needs Windows, and nothing here
is signed yet.

Fixing that does not move ADR 0013 rule 3 an inch, which is why the two reasons
were recorded separately in the first place. What may not live on a runner is the
token, not the build: Velopack clients apply what their feed serves without
verifying a signature, so whoever can write a release can ship an update every
install runs. The packages go to RUNNER_TEMP and die with the job. They are not
offered as workflow artefacts either — an installer nobody has run should not sit
somewhere that invites passing it on.

Written out as shell rather than by calling scripts/release-windows.ps1. That
script is a person's 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 for deltas, and asks for the forge token. Calling it would mean either
weakening it with CI switches or having CI satisfy conditions that only make
sense at a desk. The constants the two now share — pack id, title, authors,
channel, icon — are a contract with VelopackUpdateChannel and with every
installed client, and both sides say so.

Two things fell out of running the steps rather than reading them, and both were
in code nothing had ever executed:

dotnet msbuild -getProperty:Version answers 1.0.0. Without a target named it
evaluates the project and runs nothing, and MinVer computes inside a target — so
the read comes back as the SDK default on a full checkout with every tag present.
That line is the tag check in this file, which is `if:` a tag ref, and there are
no tags yet: the first release ever cut would have been refused by its own guard,
which would then have blamed fetch-depth. release-windows.ps1 had the same line
and would have demanded HEAD be tagged v1.0.0. Both now pass -t:MinVer.

And MinVer answers 0.0.0-alpha.0.N until that first tag exists, which vpk rejects
outright as below 0.0.1 — so packing the true version could not have worked on
any build made today. The patch digit is lifted for the throwaway package only.
The release script gets no such floor and must not: its version is the one users
compare against, and there the refusal is the right outcome.

Verified by extracting both steps from this file and running them against a real
clone in a dotnet SDK container: Setup.exe, the portable zip, the .nupkg and
releases.win.json, from a machine that is not Windows.
This commit is contained in:
2026-08-05 21:56:45 +02:00
parent 5cb361ea13
commit 3d3d0bc95f
4 changed files with 224 additions and 25 deletions
+27 -2
View File
@@ -568,12 +568,37 @@ Packaging the desktop client had broken the server's image build, and nothing bu
have caught it. Found by running `docker build` locally rather than by reading the lock files.
So the RID stays out of the committed state, and the two commands that need one — the release script's
publish and the `windows publish still resolves` step in `ci.yml` — pass `-p:RestoreLockedMode=false` for
themselves alone. That restore rewrites the lock files as a side effect, which does not matter on a runner
publish and the `publish the windows desktop client` step in `ci.yml` — pass `-p:RestoreLockedMode=false`
for themselves alone. That restore rewrites the lock files as a side effect, which does not matter on a runner
whose checkout is discarded and does matter on a developer's machine, so the release script runs
`git checkout -- '*packages.lock.json'` afterwards. `-p:RestorePackagesWithLockFile=false` is not an
alternative: it fails NU1005 whenever a lock file already exists.
**`vpk` picks its target from the host, and cross-compiling is a bracketed directive rather than a flag.**
The CI job packages the Windows desktop client on a Linux runner, and plain `vpk pack --runtime win-x64`
there refuses outright:
```
To build packages for Linux, the target rid must be Linux (actually was Windows). If your real intention
was to cross-compile a release for Windows then you should provide an OS directive: eg. 'vpk [win] pack ...'
```
The directive goes before the verb — `dotnet vpk '[win]' pack …` — and must be quoted in a POSIX shell,
where `[win]` is a glob matching any one of `w`, `i` and `n`. With it, a Linux runner logs
`Directive enabled for cross-compiling from Linux (current os) to Windows` and writes
`DodoSSH.Desktop-win-Setup.exe`, the portable zip, the `.nupkg` and `releases.win.json` — the same set a
Windows machine produces. `--runtime win-x64` is still required: the directive says which OS is being
targeted, not which RID. Only signing needs Windows tooling, which is why `ci.yml` can package and
`scripts/release-windows.ps1` will still be the thing that signs when there is a certificate to sign with.
**`dotnet msbuild -getProperty:Version` answers `1.0.0`, and MinVer is not to blame.** `-getProperty`
without a target *evaluates* the project and runs nothing, while MinVer computes the version inside a
target — so the read comes back as the SDK default on a full checkout with every tag present, which looks
exactly like a version that was never configured. `-t:MinVer` makes `-getProperty` report the value after
that target has run, and both readers of it — the tag check in `ci.yml` and `scripts/release-windows.ps1`
— pass it. Neither had, and neither had ever run: the CI check is `if:` a tag ref and there are no tags
yet, so the first release would have been refused by its own guard, which would have blamed `fetch-depth`.
**The Android head's lock file is outside the solution, so nothing checks it until the android job runs
— and the android job was broken for an unrelated reason for the whole of the release that went stale.**
`DodoSSH.Client.Android` is deliberately not in `DodoSSH.slnx` (it needs a workload the other two jobs