Public Access
Merge branch 'claude/android-release'
This commit is contained in:
+276
-3
@@ -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.
|
||||
#
|
||||
|
||||
Reference in New Issue
Block a user