diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5e95817..8a9b780 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -787,6 +787,278 @@ jobs: echo "Published nightly $VERSION with$names" + desktop: + name: desktop nightly + # Gated on the tests, like the image job and for a stronger version of its reason: this one is + # installed by people and replaces itself afterwards. Nothing anybody runs should come out of a + # commit whose suite was red. + needs: [build] + runs-on: [linux] + # main only, and the whole job rather than its last step. A v* tag belongs to the release channel, + # which no runner may publish — ADR 0013 rule 3 — and the desktop head is already built and packed + # on tags by the build job above, so there is nothing here a tag build would gain. + if: github.ref == 'refs/heads/main' + # Writes the rolling nightly release at the end of the job. Job-scoped, so no other job in this file + # gains it; see the publish step for what the capability is and why this channel may hold it. + permissions: + contents: write + steps: + # Duplicated from the build job; see the comment there for why it cannot be factored out. There + # are four copies now, and any change has to be made in all four. + - name: ensure node and git + run: | + set -eu + SUDO="" + [ "$(id -u)" -eq 0 ] || SUDO="sudo" + + missing="" + command -v node >/dev/null 2>&1 || missing="$missing nodejs" + command -v git >/dev/null 2>&1 || missing="$missing git" + + if [ -z "$missing" ]; then + echo "node $(node --version), git $(git --version)" + exit 0 + fi + + echo "Installing:$missing" + if command -v apt-get >/dev/null 2>&1; then + $SUDO apt-get update -qq + $SUDO apt-get install -y --no-install-recommends $missing + elif command -v apk >/dev/null 2>&1; then + $SUDO apk add --no-cache $missing + elif command -v dnf >/dev/null 2>&1; then + $SUDO dnf install -y $missing + else + echo "No apt-get, apk or dnf here, so node cannot be installed from inside the" >&2 + echo "job. Point the runner's container.image at something that ships node." >&2 + exit 1 + fi + echo "node $(node --version), git $(git --version)" + + major="$(node --version | sed 's/^v//; s/\..*//')" + if [ "$major" -lt 20 ]; then + echo "::warning::node $major is older than the runtime these actions target;" \ + "give the runner an image with node 20 or newer if actions misbehave." + fi + + # fetch-depth 0 for the reason the other three jobs give, and here it decides what gets published: + # MinVer's answer is this build's version and the number a nightly client compares against. + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + + - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 + with: + global-json-file: global.json + cache: true + cache-dependency-path: '**/packages.lock.json' + + # Before the version is read, and that ordering is not tidiness. MinVer arrives as a package, so its + # target does not exist until a restore has written obj/*.nuget.g.targets — and `-t:MinVer` on an + # unrestored project fails MSB4057 "the target does not exist", which reads like a typo in this file + # rather than like a missing restore. The build job's tag check is safe because it runs after that + # job's own restore; this job has none, so it needs this one. + # + # Locked, like the solution restore in the build job. The RID-specific restore the publish needs is + # unlocked and asks for that itself, exactly as the build job's publish does. + - name: restore + run: dotnet restore src/DodoSSH.Client.App/DodoSSH.Client.App.csproj --locked-mode + + # ◆ THE VERSION IS DECIDED ONCE HERE AND THEN FORCED ON EVERYTHING. + # + # MinVer's own answer is not usable as it stands: until a v* tag exists it is 0.0.0-alpha.0.N, and + # vpk refuses to pack anything below 0.0.1. The floor is applied to the *whole build* rather than to + # the packaging alone, through MinVerVersionOverride, and that is the part worth understanding. + # + # Packing a version the assemblies disagree with would put one number in the installer and another + # on the preferences screen — the screen a person reads when asked which nightly they are on, and + # the number they would then quote into an issue that nobody can match to a build. MinVer sets both + # Version and InformationalVersion from the override, so the two cannot drift. + # + # Monotonic across the boundary, which is what a feed needs: heights keep rising within a floored + # version, and the first real tag moves the whole number up past every floored one. + - name: the nightly version + id: version + run: | + set -euo pipefail + + # -t:MinVer for the reason the tag check in the build job spells out: without a target named, + # -getProperty answers the SDK default and every nightly would claim the same version forever. + 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 + + case "$version" in + 0.0.0*) + floored="0.0.1${version#0.0.0}" + echo "MinVer says $version, which vpk will not pack; this nightly is $floored." + version="$floored" + ;; + esac + + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "$version" + + # DodoChannel=nightly is what makes this a different application rather than the same one built + # again: it puts the channel in the assembly, which is where DesktopChannel reads it to pick the + # feed to poll, whether to accept prereleases, and which profile directory to keep a cache in. + # + # RestoreLockedMode=false for the RID, exactly as the build job's publish does — see the long note + # there. This checkout is thrown away, so the lock files it rewrites go nowhere. + - name: publish the nightly + env: + VERSION: ${{ steps.version.outputs.version }} + run: > + dotnet publish src/DodoSSH.Client.App/DodoSSH.Client.App.csproj + --configuration Release --runtime win-x64 --self-contained true + -p:RestoreLockedMode=false + -p:DodoChannel=nightly + -p:MinVerVersionOverride="$VERSION" + --output "$RUNNER_TEMP/nightly-win-x64" + + # ◆ A DIFFERENT PACK ID, A DIFFERENT CHANNEL, A DIFFERENT TITLE. ALL THREE, AND NONE IS COSMETIC. + # + # packId decides where Velopack installs and what an installed client matches an update against, so + # DodoSSH.Desktop.Nightly is what makes this install *beside* the release build rather than over it, + # and what stops either feed's package being applied to the other's install. + # + # channel decides the name of the index file on the feed — releases.win-nightly.json — and it is a + # contract with VelopackUpdateChannel.NightlyChannel. Disagree on this word and the channel answers + # nothing, forever, with no error anywhere. + # + # title is what a person reads in the Start menu and in Add/Remove Programs, and it is the only one + # of the three they will ever see. Two entries both called DodoSSH would be the whole benefit of + # installing side by side, thrown away at the last step. + # + # No `vpk download` and so no deltas: this channel deletes its previous release on every push, so + # there would be nothing on the feed for a delta to be applied against. A nightly update is a full + # download, which is the honest cost of a rolling channel that keeps exactly one build. + - name: package the nightly + env: + VERSION: ${{ steps.version.outputs.version }} + run: | + set -euo pipefail + + dotnet tool restore + + dotnet vpk '[win]' pack \ + --skip-updates \ + --packId DodoSSH.Desktop.Nightly \ + --packVersion "$VERSION" \ + --packDir "$RUNNER_TEMP/nightly-win-x64" \ + --packTitle 'DodoSSH Nightly' \ + --packAuthors DodoTech \ + --mainExe DodoSSH.exe \ + --icon src/DodoSSH.Client.App/Assets/dodossh.ico \ + --runtime win-x64 \ + --channel win-nightly \ + --outputDir "$RUNNER_TEMP/nightly-releases" + + ls -la "$RUNNER_TEMP/nightly-releases" + + # ◆ PUBLISHING IT, AND WHAT THAT CAPABILITY IS. + # + # Whoever can write a release here can put a build on every nightly desktop, because Velopack fetches + # from this feed and applies what it finds without verifying a signature. That is the same capability + # as a signing key reached through a different door, and ADR 0013 rule 3 keeps it off runners. + # + # It is acceptable here for the reason the android nightly gives, and only for that reason: this is + # not that channel. A nightly is a separate application with its own pack id, its own install + # directory and its own profile, and it cannot update the build anybody is trusting with their + # credentials — the release channel reads a different index and refuses prereleases, so it cannot + # even see this one. Anybody installing a nightly is trusting everyone who can write to this + # repository, which is a thing to know rather than a thing to discover; the README says so. + # + # ◆ DELETED AND RECREATED RATHER THAN ADDED TO. + # + # A rolling channel has to keep exactly one build, and every asset here is version-named, so merging + # would grow the release by a hundred and twenty megabytes per push until the forge said no. There is + # no atomic form of this in the API, so the shape with the fewest states is to remove both the + # release and its tag and make them again. The window where no nightly exists is a few seconds, and + # the client's answer to it is the same as to an unreachable forge: the timer swallows it and tries + # later; a pressed CHECK NOW says so. + - name: publish the nightly release + env: + FORGE: https://git.dodotech.cloud + REPO: DodoTech-Public/DodoSSH + TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ steps.version.outputs.version }} + TAG: nightly-desktop + run: | + set -euo pipefail + + if [ -z "${TOKEN:-}" ]; then + echo "No token, so the nightly was built and not published." >&2 + echo "GITHUB_TOKEN is provided by the runner; an empty one means Actions is configured" >&2 + echo "without it, and the job's contents: write permission is what asks for it." >&2 + exit 1 + fi + + api="$FORGE/api/v1/repos/$REPO" + auth="Authorization: token $TOKEN" + + # The first "id" with digits after it, for the reason the android job records at length: a + # greedy .* walks past the release's own id to the author's, which is -2, and yields nothing. + release_id() { + grep -oE '"id":[0-9]+' | head -1 | cut -d: -f2 + } + + existing="$(curl -fsS -H "$auth" "$api/releases/tags/$TAG" 2>/dev/null || true)" + if [ -n "$existing" ]; then + id="$(printf '%s' "$existing" | release_id)" + if [ -n "$id" ]; then + echo "Removing the previous $TAG release $id" + curl -fsS -X DELETE -H "$auth" "$api/releases/$id" >/dev/null || true + else + echo "A $TAG release exists and its id could not be read:" >&2 + printf '%s\n' "$existing" >&2 + exit 1 + fi + fi + curl -fsS -X DELETE -H "$auth" "$api/tags/$TAG" >/dev/null 2>&1 || true + + # vpk rather than curl, unlike the android job above, and the difference is what is being + # uploaded. An APK is one file the client is told about by a manifest this repository writes; a + # Velopack release is a set of files plus an index whose format is Velopack's own. Writing that + # index by hand would be reimplementing the tool that is already here. + # + # --pre is load-bearing twice over. It keeps this out of the release channel, which refuses + # prereleases — and it keeps it out of `releases/latest`, which is what the *phone's* release + # channel reads: a desktop nightly published as a stable release would become the newest release + # in this repository and every phone on the release channel would start failing its update check + # against a release carrying no android manifest. + dotnet vpk upload gitea \ + --skip-updates \ + --repoUrl "$FORGE/$REPO" \ + --token "$TOKEN" \ + --outputDir "$RUNNER_TEMP/nightly-releases" \ + --channel win-nightly \ + --tag "$TAG" \ + --releaseName "Nightly desktop $VERSION" \ + --targetCommitish "$GITHUB_SHA" \ + --pre \ + --publish + + # Asked for back rather than assumed, and the android job's history is why: it once created a + # release, uploaded nothing, and reported success for every upload it never made. A nightly + # desktop feed that exists and carries no index is a client that checks, finds nothing, and + # reports itself up to date forever. + published="$(curl -fsS -H "$auth" "$api/releases/tags/$TAG")" + + for name in releases.win-nightly.json DodoSSH.Desktop.Nightly-win-nightly-Setup.exe; do + if ! printf '%s' "$published" | grep -qF "\"name\":\"$name\""; then + echo "The release was created but $name is not on it:" >&2 + printf '%s\n' "$published" >&2 + exit 1 + fi + done + + echo "Published nightly desktop $VERSION as $TAG" + image: name: api image # Gated on the tests rather than parallel with them, which costs a few minutes of wall @@ -1067,9 +1339,10 @@ 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. It is built -# and packaged here — the two steps at the end of the build job — and what is withheld is only the -# upload. +# No job here publishes the desktop *release* channel, and there is not going to be one. The desktop +# nightly job above publishes a different application — its own pack id, its own Velopack channel, its own +# install directory and profile — and the distance between those two sentences is the whole design. What +# the build job does for the release channel is prove it still builds and packs; the upload is withheld. # # ◆ ONE REASON, WHERE THIS ONCE CLAIMED TWO, AND THE SECOND WAS NOT TRUE. # diff --git a/README.md b/README.md index d180afc..a4be75d 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,23 @@ this machine's device key from your account. Cutting a release is `scripts/release-windows.ps1`, run by a person on a Windows machine. Deliberately not a CI job; ADR 0013 decision 3 explains why, and it is not only that the runners are Linux. +### The nightly desktop build + +There is a second Windows build, published by CI from `main` on every push, on the `nightly-desktop` +release. It installs **beside** the release build rather than over it — its own entry called *DodoSSH +Nightly*, its own install directory, and its own profile at `%LOCALAPPDATA%\DodoSSH.Nightly` — so trying it +costs nothing you are relying on. It signs in separately and appears to your account as a new device, +because it genuinely is a second installation. + +**Installing it means trusting everyone who can write a release on this repository, including CI.** The +updater applies what the feed serves without verifying a signature, so anyone who can change a workflow +file here can put a build on every nightly machine. That is an acceptable trade for a build you are trying +and not one for a build holding your infrastructure credentials, which is the entire reason the two +channels exist and cannot see each other. The release build refuses prereleases and reads a different feed, +so nothing published here can ever reach it. Same arrangement, and same reasoning, as the phone's nightly — +[ADR 0014](docs/adr/0014-android-updates.md), and [ADR 0013](docs/adr/0013-desktop-distribution-and-updates.md) +decision 9. + ## Running it Three commands, in order. The first is once per machine. diff --git a/docs/adr/0013-desktop-distribution-and-updates.md b/docs/adr/0013-desktop-distribution-and-updates.md index 1b6e5eb..f87a922 100644 --- a/docs/adr/0013-desktop-distribution-and-updates.md +++ b/docs/adr/0013-desktop-distribution-and-updates.md @@ -228,6 +228,56 @@ changes. token, and it puts a compellable third party in the signing path — which is ADR 0011 rule 3's shape one layer down, declined there for reasons that do not stop applying because the vendor changed. +### 9. There is a second desktop channel, published by CI, and it is a second application + +[ADR 0014](0014-android-updates.md) gave the phone a nightly channel and rule 3 above gives the desktop +none, which left the two heads with different answers to the same question — *how does somebody try what +is on main?* — for no reason other than the order the work happened in. This is the desktop's answer, and +it is the phone's arrangement with one difference that changes how much of it has to be built. + +**Android gets the separation from the platform. Windows does not.** Two Android channels cannot replace +one another because the installer refuses a package signed by a different key; a mistake there is loud. +Velopack applies what its feed serves and verifies no signature, so on this head the separation is entirely +construction: + +| | release | nightly | +| --- | --- | --- | +| pack id | `DodoSSH.Desktop` | `DodoSSH.Desktop.Nightly` | +| Velopack channel | `win` | `win-nightly` | +| feed | newest non-prerelease release | the rolling `nightly-desktop` prerelease | +| profile | `%LOCALAPPDATA%\DodoSSH` | `%LOCALAPPDATA%\DodoSSH.Nightly` | +| cut by | a person, from a `v*` tag | CI, on every push to main | + +Four separations rather than one, because each closes a different door. The pack id decides the install +directory and what an installed client matches an update against, so it is what makes a nightly install +*beside* rather than *over*. The channel names the index file on the feed, so neither build ever reads the +other's — and the release channel additionally refuses prereleases, which is belt and braces in the one +direction that matters: an unsigned CI build must never reach a machine somebody is trusting with their +credentials. The profile is the one that is easy to skip and would hurt most: the cache schema is migrated +on every launch, before unlock, so a shared profile means a nightly quietly upgrading a database the +release build then opens. + +**The prerelease flag is also how the two heads stay out of each other's way.** The phone's release channel +reads `releases/latest`, which skips prereleases. A desktop nightly published as a stable release would +become the newest release in this repository, and every phone on the release channel would start failing +its check against a release carrying no Android manifest. The nightly is published with `--pre` for its own +sake and for that one. + +**What this costs, stated where somebody will read it before installing:** whoever can write a release on +this repository can put a build on every nightly desktop, because the client applies what the feed serves +without verifying a signature. That set includes CI and therefore everyone who can change a workflow file. +It is the same trade [ADR 0014](0014-android-updates.md) accepted for the phone's nightly, and it is +acceptable for the same reason and only that reason: this is not the channel anybody's real credentials are +on. Rule 3 is untouched — the release channel still has no job, no token and no runner. + +**The version gets a floor, on this channel only.** MinVer answers `0.0.0-alpha.0.N` until the first `v*` +tag and `vpk` refuses to pack anything below `0.0.1`, so the nightly job lifts the patch digit and keeps +the prerelease height. It is applied through `MinVerVersionOverride`, which moves the assembly version too +— packing a number the assemblies disagree with would put one version in the installer and another on the +preferences screen, which is the screen somebody reads when asked which nightly they are running. Ordering +across the floor holds: heights rise within a floored version, and the first real tag moves past all of +them. The release script gets no floor and must not have one; there, `0.0.0` should be refused. + ## Consequences **The desktop gets what ADR 0011 had to refuse the phone.** Discovery is still manual — somebody has to be diff --git a/docs/adr/0014-android-updates.md b/docs/adr/0014-android-updates.md index 0b8bb78..1b13ebe 100644 --- a/docs/adr/0014-android-updates.md +++ b/docs/adr/0014-android-updates.md @@ -116,6 +116,15 @@ is the entire reason there are two. other is an uninstall and a fresh enrolment. There is no migration and there will not be one — the two caches are encrypted under keys held by two package identities the platform keeps apart. +◆ **The desktop has since taken this arrangement, and it had to build what Android is given.** +[ADR 0013](0013-desktop-distribution-and-updates.md) decision 9 is this ADR applied to Windows: a nightly +CI publishes from main, installing beside the release build rather than over it. The difference worth +carrying back here is that every separation this ADR gets from the platform — different package identity, +signature-checked updates, a per-app data directory — is on Windows a thing somebody had to choose and can +therefore undo. The paragraph above about the attack surface applies there word for word, with one line +removed: on the desktop the packages are not signed at all, so the feed is the only thing standing between +a nightly install and an arbitrary build. + **ADR 0011's "no automatic update" consequence is now wrong for the release channel and remains true for reach.** Discovery, MDM deployment and the sideloading permission are all unchanged. What changed is only that an installed copy can now learn a newer one exists. diff --git a/docs/manual-checks.md b/docs/manual-checks.md index dfb98f2..590641b 100644 --- a/docs/manual-checks.md +++ b/docs/manual-checks.md @@ -2005,6 +2005,29 @@ is wrong. Reinstalling then asks for the passphrase rather than for a server. lost their offline unlock and any change that was still in the outbox. That is the failure ADR 0013 decision 2 exists to prevent, and it is why 16.1 checks the same thing from the other end. +### 16.10 The nightly installs beside the release build and not over it · **the one CI cannot check** + +Needs both: a release build installed by walking 16.1, and `DodoSSH.Desktop.Nightly-win-nightly-Setup.exe` +from the `nightly-desktop` release. Install the nightly second, and then start **both**. + +**Pass:** two entries in the Start menu, one *DodoSSH* and one *DodoSSH Nightly*; two directories, +`%LOCALAPPDATA%\DodoSSH.Desktop` and `%LOCALAPPDATA%\DodoSSH.Desktop.Nightly`; two profiles, +`%LOCALAPPDATA%\DodoSSH` and `%LOCALAPPDATA%\DodoSSH.Nightly`, each with its own `cache.db` and +`device.key`. The nightly's titlebar says **DodoSSH Nightly** and the release build's says **DodoSSH** — +which is the only difference visible while somebody is typing a passphrase into one of them. Signing in to +the nightly leaves the release build signed in and untouched, and the account shows a second device. + +**Failure means:** if there is one profile directory, the two builds are sharing a cache and a device key, +and the next nightly carrying a schema migration will upgrade the database the release build opens. If +there is one install directory, the pack ids collide and the nightly has replaced the release build +outright — which is the thing ADR 0013 decision 9 is constructed to make impossible, so it means one of the +four separations has been undone. + +**And the direction that matters most:** on the release build, PREFERENCES → UPDATES → CHECK NOW must not +offer a nightly, ever, however many have been published since. It reads a different index and refuses +prereleases; if a nightly version is ever offered there, stop and treat it as a release-channel incident +rather than as a bug in the nightly. + --- ## Phase 17 — Installing and updating the phone diff --git a/docs/platform-flags.md b/docs/platform-flags.md index ca5bfa3..5cbde70 100644 --- a/docs/platform-flags.md +++ b/docs/platform-flags.md @@ -595,9 +595,23 @@ targeted, not which RID. Only signing needs Windows tooling, which is why `ci.ym 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`. +that target has run, and every reader of it — the tag check in `ci.yml`, the desktop nightly job, and +`scripts/release-windows.ps1` — passes it. Neither of the first two 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`. + +**And naming that target requires a restore first, which is a second failure wearing a very different +face.** MinVer arrives as a package, so its target is imported from `obj/*.nuget.g.targets` and does not +exist at all on a clean checkout: + +``` +error MSB4057: The target "MinVer" does not exist in the project. +``` + +That reads like a typo in the workflow rather than like a missing restore, and it does not reproduce on any +machine that has built the project before — which is every developer machine and no fresh runner. The +build job's tag check is safe because it runs after that job's own restore; the desktop nightly job and the +release script each restore before reading, deliberately and with a comment saying why. **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.** diff --git a/scripts/release-windows.ps1 b/scripts/release-windows.ps1 index fcc5577..377a668 100644 --- a/scripts/release-windows.ps1 +++ b/scripts/release-windows.ps1 @@ -71,6 +71,11 @@ $RepoUrl = 'https://git.dodotech.cloud/DodoTech-Public/DodoSSH' # A contract with VelopackUpdateChannel.ReleaseChannel. It is Velopack's Windows default, so leaving it # unsaid on both sides would work too — but unsaid here and stated there is how a feed goes quiet with no # error at all: the client checks, finds nothing, and reports itself up to date forever. +# +# The nightly channel is the same three values with different contents — DodoSSH.Desktop.Nightly, DodoSSH +# Nightly, win-nightly — and they live in the `desktop nightly` job in .github/workflows/ci.yml rather than +# here, because that build is CI's and this script is a person's. Nothing shares them, deliberately: this +# file must keep working if that job is deleted. See ADR 0013 decision 9 for what the separation buys. $Channel = 'win' $RepoRoot = Split-Path -Parent $PSScriptRoot @@ -100,11 +105,20 @@ Push-Location $RepoRoot try { # ---- What is being released ----------------------------------------------------------------------- - # -t:MinVer, and it is load-bearing. -getProperty on its own evaluates the project and runs no - # targets, while MinVer sets Version from inside one — so this read answered the SDK's default - # 1.0.0 regardless of the tag, and the check below would then have refused to build anything not - # tagged v1.0.0. CI's tag check had the same line and the same fault; both are fixed, and both - # say so, because this is the version that ends up in the package a client compares against. + # Restored before the version is read, and both halves of that sentence are load-bearing. + # + # -t:MinVer, because -getProperty on its own evaluates the project and runs no targets, while MinVer + # sets Version from inside one — so this read answered the SDK's default 1.0.0 regardless of the tag, + # and the check below would then have refused to build anything not tagged v1.0.0. CI's tag check had + # the same line and the same fault. + # + # And a restore first, because naming a target that arrives with a package fails MSB4057 on a clean + # clone, where obj/ has no MinVer targets to import yet. It costs seconds on a machine that has built + # before, which is every machine except the one this would otherwise fail on. + Write-Step 'Restoring the desktop head, so the version can be read' + & dotnet restore $Project --locked-mode + if ($LASTEXITCODE -ne 0) { Stop-With 'Restore failed.' } + $version = (& dotnet msbuild $Project -getProperty:Version -t:MinVer -nologo) -replace '\s', '' if ([string]::IsNullOrWhiteSpace($version)) { Stop-With 'Could not read the version from MSBuild.' diff --git a/src/DodoSSH.Client.App/App.axaml.cs b/src/DodoSSH.Client.App/App.axaml.cs index 1b70943..aecc675 100644 --- a/src/DodoSSH.Client.App/App.axaml.cs +++ b/src/DodoSSH.Client.App/App.axaml.cs @@ -92,7 +92,10 @@ internal sealed partial class DodoSshApp : Application private static void Compose(IClassicDesktopStyleApplicationLifetime desktop) { - var paths = ClientPaths.Default; + // ForChannel rather than Default, so a nightly keeps its cache, outbox and device key somewhere + // the release build never opens. See ClientPaths.ForChannel for why sharing them is the failure + // worth spending a directory on. + var paths = ClientPaths.ForChannel(DesktopChannel.Name); var caches = ClientCacheFactory.ForFile(paths.CacheFile); // Known hosts live in the vault, so trust survives a restart and follows the user to every device. diff --git a/src/DodoSSH.Client.App/DodoSSH.Client.App.csproj b/src/DodoSSH.Client.App/DodoSSH.Client.App.csproj index 6b6cfc2..4efba22 100644 --- a/src/DodoSSH.Client.App/DodoSSH.Client.App.csproj +++ b/src/DodoSSH.Client.App/DodoSSH.Client.App.csproj @@ -42,6 +42,30 @@ --> Assets/dodossh.ico + + release + + + + - +