Public Access
The android job got past restore and died in the .NET Android SDK's own tooling resolution:
warning : An error occurred trying to start process
'.../packs/Microsoft.Android.Sdk.Linux/36.1.69/tools/Linux/aapt2' … No such file or directory
error XA0111: Unsupported version of AAPT2 found at path '.../tools/Linux'
The error names the wrong problem. Nothing was found, so nothing had a version, and XA0111 points at
an Aapt2ToolPath in the project file that has never been set. The warning above it is the real message
and it is only a warning.
The pack was incomplete, and on this runner it would have stayed that way: act's host executor keeps
/usr/share/dotnet between runs, and `dotnet workload install` reads the installed-workload records and
does nothing when android is listed, whatever is on disk. So two changes, each of which stands alone.
The build and the packaging step are now given -p:Aapt2ToolPath pointing at build-tools, which this job
installs itself and can therefore vouch for. That is already the aapt2 the packaging step shells out to
for `dump badging`, so this makes one tool of what were two, and the manifest the feed publishes is now
read by the binary that wrote it. Checked rather than assumed: build-tools 36.0.0 answers aapt2 2.20 and
.NET for Android 36.1.43 builds and packages this head against it with no complaint. The version is
named once, in the step's env, because three things now depend on it agreeing with itself.
And the workload step probes for the pack file that went missing and repairs the workload when it is
absent. The probe is a witness rather than the point — the build no longer touches that binary — but a
5 MB file near the end of a 130 MB package is what a truncated extraction loses first, and r8.jar and
manifestmerger.jar are what it loses next.
960 lines
50 KiB
YAML
960 lines
50 KiB
YAML
name: ci
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
# Release tags run the whole workflow, not only the image job that gates on it. A tag
|
|
# is the one build nobody is watching, so it is the last place to take the tests on
|
|
# trust.
|
|
tags: ['v*']
|
|
pull_request:
|
|
branches: [main]
|
|
|
|
# Actions are pinned to commit SHAs, not tags: a tag can be moved to point at new code,
|
|
# which would let a compromised action run with this workflow's permissions.
|
|
permissions:
|
|
contents: read
|
|
|
|
concurrency:
|
|
group: ci-${{ github.workflow }}-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
env:
|
|
DOTNET_NOLOGO: true
|
|
DOTNET_CLI_TELEMETRY_OPTOUT: true
|
|
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
|
|
CI: true
|
|
|
|
jobs:
|
|
build:
|
|
name: build and test
|
|
runs-on: [linux]
|
|
steps:
|
|
# act_runner runs every `uses:` action with node inside the job container, and this
|
|
# runner's image has none — the run died on the first line of actions/checkout with
|
|
# "Cannot find: node in PATH". A `run:` step is shell rather than node, so this one
|
|
# can go first and unblock the rest.
|
|
#
|
|
# This is a workaround and the real fix is one line of the runner's own config.yaml:
|
|
# point container.image at an image that ships node, the way Gitea's default
|
|
# catthehacker/ubuntu:act-latest does. Kept anyway, because a pipeline that depends
|
|
# on a runner being configured correctly somewhere else fails confusingly when it is
|
|
# not, and because it costs nothing on a runner that is.
|
|
#
|
|
# git as well as node, and said in the step name rather than smuggled in: checkout
|
|
# shells out to git the moment node has loaded it, so an image thin enough to lack
|
|
# one usually lacks the other, and learning that costs a whole second CI round trip.
|
|
#
|
|
# Repeated verbatim in all three jobs, which is not laziness. It cannot be a local
|
|
# composite action — that would need the checkout it exists to unblock — and YAML
|
|
# anchors, which would deduplicate it, are rejected by GitHub's parser and would make
|
|
# this file portable to nothing. Change one copy, change all three.
|
|
- 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)"
|
|
|
|
# Warned about rather than failed on. Distributions pin their nodejs package to
|
|
# the release they shipped with — Ubuntu 24.04 still serves 18, which is past end
|
|
# of life and older than the runtime these actions declare. It generally runs
|
|
# them anyway, since act_runner uses whichever node is on PATH regardless of what
|
|
# the action asked for, so this is a note for when one of them misbehaves in a
|
|
# way that makes no sense, not a reason to stop a build that is probably fine.
|
|
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, and it is load-bearing rather than tidy. MinVer derives the version from the
|
|
# nearest v* tag, and checkout's default shallow clone has no tags at all — so it would not fail,
|
|
# it would quietly answer 0.0.0-alpha.0.N and every build would ship that. Velopack decides
|
|
# whether an installed client is out of date by comparing versions, which makes a plausible wrong
|
|
# answer here a client that never updates.
|
|
#
|
|
# Repeated in all three jobs, like the node preamble above and for the same reason. Change one
|
|
# copy, change all three.
|
|
- 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'
|
|
|
|
# Locked mode fails if packages.lock.json does not match the project files, so a
|
|
# dependency cannot change without the lock file change being reviewed.
|
|
- name: restore
|
|
run: dotnet restore DodoSSH.slnx --locked-mode
|
|
|
|
# The version comes from the tag, so on a tag build there are two ways to say the same number
|
|
# and they can disagree — a moved tag, a tag on the wrong commit, or a checkout that somehow
|
|
# still lost its history. What makes that worth a step of its own is that the disagreement is
|
|
# silent everywhere else: MinVer answers 0.0.0-alpha.0.N rather than failing, the build goes
|
|
# 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.
|
|
- 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:]')"
|
|
|
|
if [ "$tag" != "$declared" ]; then
|
|
echo "The tag says v$tag and MinVer computed $declared." >&2
|
|
echo >&2
|
|
echo "These come from the same place, so a mismatch means the checkout did not see the" >&2
|
|
echo "tag it is building — most likely fetch-depth, which must stay 0 in every job here." >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "v$declared"
|
|
|
|
# No `dotnet format --verify-no-changes` step. It re-analysed the whole solution before
|
|
# the build did, for minutes, to check something the build already checks: IDE0055 is an
|
|
# error in .editorconfig and TreatWarningsAsErrors is on, so a misformatted file fails
|
|
# the build step below on its own. What the separate step added was the ability to say
|
|
# so a few minutes earlier, and it cost more than that on every run.
|
|
|
|
# Avalonia's headless renderer is still Skia, and libSkiaSharp.so — which the layout
|
|
# test project copies into its own output — links against libfontconfig. Without that
|
|
# one library every test in DodoSSH.Client.App.Layout.Tests dies inside
|
|
# HeadlessUnitTestSession before it measures anything, and 69 tests fail for a reason
|
|
# none of their names or assertions mention.
|
|
#
|
|
# The library, and not fonts. Verified in a container where fc-list returns zero and
|
|
# the suite passes anyway: the application carries Inter itself, so nothing here needs
|
|
# a typeface installed — only the thing that would have gone looking for one.
|
|
- name: ensure skia's native dependency
|
|
run: |
|
|
set -eu
|
|
SUDO=""
|
|
[ "$(id -u)" -eq 0 ] || SUDO="sudo"
|
|
|
|
if ldconfig -p 2>/dev/null | grep -q 'libfontconfig\.so\.1'; then
|
|
echo "libfontconfig present"
|
|
exit 0
|
|
fi
|
|
|
|
echo "Installing fontconfig"
|
|
if command -v apt-get >/dev/null 2>&1; then
|
|
$SUDO apt-get update -qq
|
|
$SUDO apt-get install -y --no-install-recommends libfontconfig1
|
|
elif command -v apk >/dev/null 2>&1; then
|
|
$SUDO apk add --no-cache fontconfig
|
|
elif command -v dnf >/dev/null 2>&1; then
|
|
$SUDO dnf install -y fontconfig
|
|
else
|
|
echo "No package manager here, so Skia cannot be given its dependency and the" >&2
|
|
echo "layout suite will fail to start. Add fontconfig to the runner's image." >&2
|
|
exit 1
|
|
fi
|
|
|
|
- name: build
|
|
run: dotnet build DodoSSH.slnx --no-restore --configuration Release
|
|
|
|
- name: test
|
|
run: dotnet test DodoSSH.slnx --no-build --configuration Release
|
|
|
|
# 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.
|
|
#
|
|
# 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
|
|
# net10.0/win-x64 target into every project it references transitively, which includes
|
|
# DodoSSH.Contracts and DodoSSH.Crypto — and the API's Dockerfile restores those with no RID
|
|
# 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
|
|
# 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
|
|
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"
|
|
|
|
# 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
|
|
# macOS, whose runners have no daemon at all. Expect the Keycloak image pull to
|
|
# dominate a cold run.
|
|
|
|
# A failing run says only that tests failed and names a log file on a machine nobody
|
|
# has a shell on. Every diagnostic thing — exception type, message, stack — is inside
|
|
# that file, so a red build was a filename and a guess. This prints it.
|
|
#
|
|
# head rather than tail, and that is the whole trick: when a suite fails wholesale it
|
|
# writes one stack per test and they are all the same stack. The first is the one that
|
|
# explains it, and the last two hundred lines are the same sentence repeated.
|
|
- name: what actually failed
|
|
if: failure()
|
|
run: |
|
|
set +e
|
|
echo "=== distro ==="
|
|
cat /etc/os-release 2>/dev/null | head -3
|
|
id
|
|
|
|
echo "=== what Skia needs, and whether it is here ==="
|
|
# ldd against the copy the test project carries. Its unresolved rows are the
|
|
# answer whenever the layout suite dies in HeadlessUnitTestSession, and asking
|
|
# here beats inferring it from a managed TypeInitializationException.
|
|
skia="$(find tests -name 'libSkiaSharp.so' 2>/dev/null | head -1)"
|
|
if [ -n "$skia" ]; then
|
|
echo "$skia"
|
|
ldd "$skia" 2>&1 | grep -Ei 'not found|fontconfig|freetype' || echo " all resolved"
|
|
else
|
|
echo " libSkiaSharp.so was not in the test output at all"
|
|
fi
|
|
ldconfig -p 2>/dev/null | grep -ci fontconfig | sed 's/^/fontconfig entries in ldconfig: /'
|
|
|
|
echo "=== docker, for the Testcontainers suites ==="
|
|
docker version --format '{{.Server.Version}}' 2>&1 | head -2
|
|
|
|
echo "=== test logs ==="
|
|
find tests -path '*/TestResults/*.log' 2>/dev/null | while read -r log; do
|
|
echo "----- $log"
|
|
head -n 120 "$log"
|
|
done
|
|
exit 0
|
|
android:
|
|
name: android head
|
|
runs-on: [linux]
|
|
# Writes the nightly release at the end of the job; see that step for what the capability is and why
|
|
# it is acceptable here and nowhere else. Job-scoped, so nothing else in this file gains it.
|
|
permissions:
|
|
contents: write
|
|
steps:
|
|
# Duplicated from the build job; see the comment there for why it cannot be factored
|
|
# out. Any change here has to be made in all three.
|
|
- 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)"
|
|
|
|
# Warned about rather than failed on. Distributions pin their nodejs package to
|
|
# the release they shipped with — Ubuntu 24.04 still serves 18, which is past end
|
|
# of life and older than the runtime these actions declare. It generally runs
|
|
# them anyway, since act_runner uses whichever node is on PATH regardless of what
|
|
# the action asked for, so this is a note for when one of them misbehaves in a
|
|
# way that makes no sense, not a reason to stop a build that is probably fine.
|
|
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, and it is load-bearing rather than tidy. MinVer derives the version from the
|
|
# nearest v* tag, and checkout's default shallow clone has no tags at all — so it would not fail,
|
|
# it would quietly answer 0.0.0-alpha.0.N and every build would ship that. Velopack decides
|
|
# whether an installed client is out of date by comparing versions, which makes a plausible wrong
|
|
# answer here a client that never updates.
|
|
#
|
|
# Repeated in all three jobs, like the node preamble above and for the same reason. Change one
|
|
# copy, change all three.
|
|
- 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'
|
|
|
|
# A job of its own, because DodoSSH.Client.Android is deliberately not in DodoSSH.slnx.
|
|
# Adding it there would make the android workload and a full Android SDK a prerequisite of
|
|
# `dotnet build DodoSSH.slnx` for everyone — including the build job above, which needs
|
|
# neither and would grow several minutes for a head it does not compile.
|
|
#
|
|
# The cost of keeping it out is that nothing in the main job would notice this head
|
|
# breaking, which for a project sharing view models with the desktop one is a matter of
|
|
# when rather than whether. This job is that notice.
|
|
# ============ the toolchain this job needs and a bare runner does not have ============
|
|
#
|
|
# This job assumed an image with a JDK and an Android SDK already on it, which is what a GitHub
|
|
# ubuntu-latest runner is and what this project's own runner is not. Every step below installs one
|
|
# of the things that assumption was making, and each is a no-op where it is already satisfied.
|
|
#
|
|
# The order matters once: the SDK's licence acceptance and every sdkmanager call are Java programs,
|
|
# so the JDK has to be first.
|
|
- name: ensure a jdk
|
|
run: |
|
|
set -eu
|
|
SUDO=""
|
|
[ "$(id -u)" -eq 0 ] || SUDO="sudo"
|
|
|
|
# An existing one is used whatever its provenance — the runner image's, or a previous run's.
|
|
# 17 is the floor: .NET for Android 36 refuses to start javac below it, and says so in a
|
|
# message that names a path rather than a version.
|
|
if command -v javac >/dev/null 2>&1; then
|
|
have="$(javac -version 2>&1 | sed 's/javac //; s/\..*//')"
|
|
if [ "${have:-0}" -ge 17 ]; then
|
|
echo "javac $(javac -version 2>&1)"
|
|
exit 0
|
|
fi
|
|
echo "javac $have is below 17; installing a newer one"
|
|
fi
|
|
|
|
if command -v apt-get >/dev/null 2>&1; then
|
|
$SUDO apt-get update -qq
|
|
$SUDO apt-get install -y --no-install-recommends openjdk-17-jdk-headless
|
|
elif command -v apk >/dev/null 2>&1; then
|
|
$SUDO apk add --no-cache openjdk17-jdk
|
|
elif command -v dnf >/dev/null 2>&1; then
|
|
$SUDO dnf install -y java-17-openjdk-devel
|
|
else
|
|
echo "No apt-get, apk or dnf here, so a JDK cannot be installed from inside the job." >&2
|
|
echo "Add one to the runner's image, or point JAVA_HOME at one." >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Exported for every later step. dirname twice: `which javac` is .../bin/javac and JAVA_HOME is
|
|
# the directory above bin. readlink -f because the packaged javac is a symlink into
|
|
# /usr/lib/jvm, and the link is what the alternatives system points at rather than the real
|
|
# home the Android SDK wants.
|
|
home="$(dirname "$(dirname "$(readlink -f "$(command -v javac)")")")"
|
|
echo "JAVA_HOME=$home" >> "$GITHUB_ENV"
|
|
echo "javac $(javac -version 2>&1) at $home"
|
|
|
|
# The SDK, from Google's own zip rather than a package. There is no distribution package for it that
|
|
# carries the platform this head needs, and unlike the JDK there is nothing to reuse: the archive is
|
|
# the supported way to get cmdline-tools and it is what every CI image does behind the scenes.
|
|
#
|
|
# Cached by path rather than by an actions/cache, deliberately. This runner is persistent, so the
|
|
# directory survives between runs and the check below turns the whole step into an echo; a cache
|
|
# action would upload and download a quarter of a gigabyte to reproduce a directory that never left.
|
|
- name: ensure the android sdk
|
|
env:
|
|
# Pinned, and the number is the commandline-tools release rather than an API level — they are
|
|
# versioned separately and this one is the current stable. Floating it would make the toolchain
|
|
# a moving part of every build.
|
|
CMDLINE_TOOLS: commandlinetools-linux-11076708_latest.zip
|
|
# Named once here because three things now depend on it being one version: what sdkmanager
|
|
# installs, what the build is told to use for aapt2, and what the packaging step reads the
|
|
# versionName back with.
|
|
BUILD_TOOLS: 36.0.0
|
|
run: |
|
|
set -eu
|
|
SUDO=""
|
|
[ "$(id -u)" -eq 0 ] || SUDO="sudo"
|
|
|
|
for tool in curl unzip; do
|
|
command -v "$tool" >/dev/null 2>&1 && continue
|
|
echo "Installing $tool"
|
|
if command -v apt-get >/dev/null 2>&1; then
|
|
$SUDO apt-get update -qq && $SUDO apt-get install -y --no-install-recommends "$tool"
|
|
elif command -v apk >/dev/null 2>&1; then
|
|
$SUDO apk add --no-cache "$tool"
|
|
elif command -v dnf >/dev/null 2>&1; then
|
|
$SUDO dnf install -y "$tool"
|
|
fi
|
|
done
|
|
|
|
# Under the runner's own tool directory rather than /opt, so it needs no root and survives
|
|
# between runs on a persistent runner. ANDROID_HOME and ANDROID_SDK_ROOT are both exported
|
|
# because the .NET Android SDK reads one and sdkmanager reads the other, and a job that set only
|
|
# one fails halfway through with a message about neither.
|
|
sdk="${ANDROID_HOME:-$HOME/android-sdk}"
|
|
mkdir -p "$sdk"
|
|
|
|
if [ ! -x "$sdk/cmdline-tools/latest/bin/sdkmanager" ]; then
|
|
echo "Fetching $CMDLINE_TOOLS"
|
|
curl -fsSL -o /tmp/cmdline-tools.zip \
|
|
"https://dl.google.com/android/repository/$CMDLINE_TOOLS"
|
|
rm -rf /tmp/cmdline-tools-unpacked
|
|
unzip -q /tmp/cmdline-tools.zip -d /tmp/cmdline-tools-unpacked
|
|
# The archive unpacks to a directory called cmdline-tools, and sdkmanager insists on living
|
|
# at cmdline-tools/<channel>/ — unpacking it in place gives cmdline-tools/cmdline-tools and
|
|
# every later call fails with "Could not determine SDK root".
|
|
mkdir -p "$sdk/cmdline-tools"
|
|
mv /tmp/cmdline-tools-unpacked/cmdline-tools "$sdk/cmdline-tools/latest"
|
|
rm -f /tmp/cmdline-tools.zip
|
|
fi
|
|
|
|
echo "ANDROID_HOME=$sdk" >> "$GITHUB_ENV"
|
|
echo "ANDROID_SDK_ROOT=$sdk" >> "$GITHUB_ENV"
|
|
export ANDROID_HOME="$sdk" ANDROID_SDK_ROOT="$sdk"
|
|
|
|
# yes rather than echo y: there are several licences and each wants its own answer, so a single
|
|
# y accepts the first and leaves the rest pending — which surfaces later as a package that
|
|
# "failed to install" with no reason given. The pipe is allowed to break when sdkmanager exits
|
|
# first, which is what the || true is for and is not hiding a failure: the install below is
|
|
# what reports one.
|
|
yes 2>/dev/null | "$sdk/cmdline-tools/latest/bin/sdkmanager" --licenses >/dev/null 2>&1 || true
|
|
|
|
# API 36 specifically, and it is not a preference: Avalonia.Controls.WebView ships only a
|
|
# net10.0-android36.0 assembly, so anything lower cannot resolve it and the head loses its
|
|
# terminal. See docs/android-port.md. platform-tools comes along because aapt2 and apksigner
|
|
# are what the packaging step actually shells out to.
|
|
"$sdk/cmdline-tools/latest/bin/sdkmanager" \
|
|
"platform-tools" "platforms;android-36" "build-tools;$BUILD_TOOLS"
|
|
|
|
# ============ aapt2, which the build is told to take from here and not from the workload ============
|
|
#
|
|
# .NET for Android ships its own aapt2 inside the workload pack, at
|
|
# packs/Microsoft.Android.Sdk.Linux/<v>/tools/Linux/aapt2, and prefers it when Aapt2ToolPath is
|
|
# unset. On this runner that binary could not be started at all:
|
|
#
|
|
# warning : An error occurred trying to start process '.../tools/Linux/aapt2' … No such file
|
|
# error XA0111: Unsupported version of AAPT2 found at path '.../tools/Linux'
|
|
#
|
|
# The second line is what the build reports and it is misleading — nothing was found, so nothing
|
|
# had a version. This runner is persistent (act's host executor, so /usr/share/dotnet survives
|
|
# between runs), and `dotnet workload install` treats an already-listed workload as satisfied
|
|
# whatever is actually on disk, so a pack left incomplete by an interrupted install stays
|
|
# incomplete for every later run. That is a state no step here can detect and none can repair.
|
|
#
|
|
# So the build is pointed at the aapt2 from build-tools instead: Google's own, installed by the
|
|
# line above, in a directory this job creates and can therefore vouch for. It is also already
|
|
# the aapt2 the packaging step shells out to for `dump badging`, so this makes one tool of what
|
|
# were two — and the manifest the feed publishes is now read by the same binary that wrote it.
|
|
#
|
|
# Verified rather than assumed to be usable, because the failure mode above is precisely a file
|
|
# that exists and will not run, and because a version check here says so in one line instead of
|
|
# as an XA0111 four minutes into a build.
|
|
aapt2="$sdk/build-tools/$BUILD_TOOLS"
|
|
|
|
if ! "$aapt2/aapt2" version; then
|
|
echo "aapt2 at $aapt2 will not run, so there is no usable one on this runner." >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "AAPT2_TOOL_PATH=$aapt2" >> "$GITHUB_ENV"
|
|
echo "Android SDK at $sdk"
|
|
|
|
# After the SDK, because the workload's own first-run checks look for one and are quieter when they
|
|
# find it. --skip-sign-check is for the workload package feed, not for anything this project signs.
|
|
#
|
|
# The repair is here because `install` is not one. It reads the installed-workload records and does
|
|
# nothing when android is listed, whatever is on disk — so a pack left half-extracted by an
|
|
# interrupted run stays half-extracted forever on a runner whose filesystem persists, and every
|
|
# later build fails on whichever file happened not to make it. This runner has already produced
|
|
# exactly that, with a missing tools/Linux/aapt2 (see the SDK step above).
|
|
#
|
|
# tools/Linux/aapt2 is the probe rather than the point: the build no longer uses that binary at all.
|
|
# It is a 5 MB file near the end of a 130 MB package, which makes it a good witness for a truncated
|
|
# extraction — and if it is absent then r8.jar and manifestmerger.jar, which the build does need,
|
|
# are the next things to go. Repairing costs a re-download and only happens when something is
|
|
# already wrong; the alternative is a mystery every few months.
|
|
- name: install the android workload
|
|
run: |
|
|
set -eu
|
|
|
|
dotnet workload install android --skip-sign-check
|
|
|
|
root="$(dirname "$(readlink -f "$(command -v dotnet)")")"
|
|
|
|
# tools/Linux/ and not tools/, because the pack's layout is host-shaped: the Linux pack puts its
|
|
# host binaries under a named directory and the Windows one puts aapt2.exe straight in tools/.
|
|
# This job is linux-only, so the Linux path is the one to look for and a probe that accepted
|
|
# either would quietly pass on a pack for the wrong host.
|
|
if [ -d "$root/packs" ] && ! ls "$root"/packs/Microsoft.Android.Sdk.*/*/tools/Linux/aapt2 >/dev/null 2>&1; then
|
|
echo "::warning::The android workload pack is missing files; repairing it."
|
|
dotnet workload repair
|
|
fi
|
|
|
|
- name: restore
|
|
run: dotnet restore src/DodoSSH.Client.Android/DodoSSH.Client.Android.csproj --locked-mode
|
|
|
|
# Aapt2ToolPath on both this and the packaging step, not on one: it is a directory the workload
|
|
# would otherwise pick for itself, and a build that resolved a different aapt2 than the package
|
|
# step would be a difference nobody could see until one of them failed. See the SDK step.
|
|
- name: build
|
|
run: >
|
|
dotnet build src/DodoSSH.Client.Android/DodoSSH.Client.Android.csproj
|
|
--no-restore --configuration Release
|
|
-p:Aapt2ToolPath="$AAPT2_TOOL_PATH"
|
|
|
|
# Packaging rather than only compiling, because the two failures this head is most exposed to
|
|
# are both link-time: a native library with no android ABI, and a managed assembly that
|
|
# resolves for net10.0 but has nothing to dex. Neither shows up in a compile.
|
|
#
|
|
# ◆ THE NIGHTLY CHANNEL, WHICH IS AN INSTALLABLE APPLICATION AND NOT THE ONE. It has its own package
|
|
# id and is signed by a keystore committed to this repository in the open, so it can neither replace
|
|
# nor be replaced by the release channel — see the csproj, and ADR 0014. The APK this produces is
|
|
# meant to be installed; the release APK is cut from a v* tag by a person running
|
|
# scripts/release-android.ps1, on a machine that holds the key ADR 0011 rule 1 keeps off runners.
|
|
#
|
|
# No RuntimeIdentifier, where this step used to pin android-arm64. That produced the smallest
|
|
# possible build check and the least installable artefact: an arm64-only APK will not run on an
|
|
# x86_64 emulator, which is what most people testing a nightly actually have. Every supported ABI
|
|
# costs size on a package nobody ships to users.
|
|
#
|
|
# versionCode is the commit count, which is monotonic by construction and needs nobody to remember
|
|
# anything. It is not a version and is never displayed; versionName carries MinVer's full answer
|
|
# including the height, which is what tells two nightlies apart.
|
|
- name: package the nightly
|
|
id: nightly
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
code="$(git rev-list --count HEAD)"
|
|
|
|
dotnet build src/DodoSSH.Client.Android/DodoSSH.Client.Android.csproj \
|
|
--no-restore --configuration Release \
|
|
-t:SignAndroidPackage \
|
|
-p:DodoChannel=nightly \
|
|
-p:DodoNightlyVersionCode="$code" \
|
|
-p:Aapt2ToolPath="$AAPT2_TOOL_PATH"
|
|
|
|
apk="$(find src/DodoSSH.Client.Android/bin/Release -name '*-Signed.apk' | head -1)"
|
|
if [ -z "$apk" ]; then
|
|
echo "The package step produced no signed APK." >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Read back out of the APK rather than recomputed, so what the feed advertises is what the
|
|
# bytes say. A versionName derived a second time in shell is a second implementation of the
|
|
# csproj's target, and the two would drift on the first change to either.
|
|
badging="$("$AAPT2_TOOL_PATH/aapt2" dump badging "$apk")"
|
|
name="$(printf '%s' "$badging" | sed -n "s/.*versionName='\([^']*\)'.*/\1/p" | head -1)"
|
|
|
|
staged="$RUNNER_TEMP/android-nightly"
|
|
rm -rf "$staged"
|
|
mkdir -p "$staged"
|
|
cp "$apk" "$staged/DodoSSH-nightly-$name.apk"
|
|
|
|
# The channel manifest, which is what the client reads and the whole reason the feed is
|
|
# machine-readable at all. versionCode is the comparison — it is the number Android itself uses
|
|
# to accept or refuse an install, so comparing anything else would let the client offer an
|
|
# update the platform then rejects. versionName is for the person reading the banner.
|
|
cat > "$staged/android-nightly.json" <<JSON
|
|
{"versionCode":$code,"versionName":"$name","apk":"DodoSSH-nightly-$name.apk"}
|
|
JSON
|
|
|
|
echo "staged=$staged" >> "$GITHUB_OUTPUT"
|
|
echo "version=$name" >> "$GITHUB_OUTPUT"
|
|
ls -la "$staged"
|
|
|
|
# ◆ PUBLISHING IT, AND WHAT THAT CAPABILITY IS.
|
|
#
|
|
# Whoever can write a release here can put a build on every nightly phone, because the client fetches
|
|
# from this feed and Android's only check is that the signature matches — and this channel's key is
|
|
# in the repository for everybody. That is the same capability as the signing key, reached through a
|
|
# different door, and it is exactly what ADR 0011 rule 1 keeps off runners.
|
|
#
|
|
# It is acceptable here for one reason: this is not that channel. A nightly is signed by a key with
|
|
# no secrecy to lose, installs under its own package id, and cannot update the application anybody
|
|
# is trusting with their credentials. The release channel has none of this — no job, no token, no
|
|
# key on a runner — and the two are separate applications so that no mistake here can reach it.
|
|
#
|
|
# main only. A tag build must not touch this: a v* tag is the release channel's, and cutting it is a
|
|
# person's job.
|
|
- name: publish the nightly
|
|
if: github.ref == 'refs/heads/main'
|
|
env:
|
|
FORGE: https://git.dodotech.cloud
|
|
REPO: DodoTech/DodoSSH
|
|
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
STAGED: ${{ steps.nightly.outputs.staged }}
|
|
VERSION: ${{ steps.nightly.outputs.version }}
|
|
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"
|
|
|
|
# Deleted and recreated rather than updated in place. A rolling tag has to move, and moving one
|
|
# through this API is two calls with no atomic form either way — so the shape with the fewest
|
|
# states is to remove both 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: try later.
|
|
existing="$(curl -fsS -H "$auth" "$api/releases/tags/nightly" 2>/dev/null || true)"
|
|
if [ -n "$existing" ]; then
|
|
id="$(printf '%s' "$existing" | sed -n 's/.*"id":\([0-9]*\).*/\1/p' | head -1)"
|
|
[ -n "$id" ] && curl -fsS -X DELETE -H "$auth" "$api/releases/$id" >/dev/null || true
|
|
fi
|
|
curl -fsS -X DELETE -H "$auth" "$api/tags/nightly" >/dev/null 2>&1 || true
|
|
|
|
created="$(curl -fsS -X POST -H "$auth" -H 'Content-Type: application/json' \
|
|
-d "$(printf '{"tag_name":"nightly","target_commitish":"%s","name":"Nightly %s","prerelease":true,"body":"Built from %s by CI, signed with the public nightly key. Installs beside the release build, never over it. See docs/adr/0014-android-updates.md."}' \
|
|
"$GITHUB_SHA" "$VERSION" "$GITHUB_SHA")" \
|
|
"$api/releases")"
|
|
|
|
release="$(printf '%s' "$created" | sed -n 's/.*"id":\([0-9]*\).*/\1/p' | head -1)"
|
|
if [ -z "$release" ]; then
|
|
echo "Gitea accepted the release call and returned no id:" >&2
|
|
printf '%s\n' "$created" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# The APK first and the manifest last, which is the ordering the client depends on: it reads the
|
|
# manifest and then fetches what the manifest names, so a manifest published before its APK is a
|
|
# few seconds in which every phone is told to download something that is not there yet.
|
|
for file in "$STAGED"/*.apk "$STAGED"/android-nightly.json; do
|
|
echo "Uploading $(basename "$file")"
|
|
curl -fsS -X POST -H "$auth" \
|
|
-F "attachment=@$file" \
|
|
"$api/releases/$release/assets?name=$(basename "$file")" >/dev/null
|
|
done
|
|
|
|
echo "Published nightly $VERSION"
|
|
|
|
image:
|
|
name: api image
|
|
# Gated on the tests rather than parallel with them, which costs a few minutes of wall
|
|
# clock on every main commit and buys the thing worth having: no image reaches the
|
|
# registry from a commit whose tests were red. An image is not a build artefact anyone
|
|
# inspects — it is the thing that gets deployed.
|
|
needs: [build]
|
|
runs-on: [linux]
|
|
steps:
|
|
# Duplicated from the build job; see the comment there for why it cannot be factored
|
|
# out. Any change here has to be made in all three.
|
|
- 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)"
|
|
|
|
# Warned about rather than failed on. Distributions pin their nodejs package to
|
|
# the release they shipped with — Ubuntu 24.04 still serves 18, which is past end
|
|
# of life and older than the runtime these actions declare. It generally runs
|
|
# them anyway, since act_runner uses whichever node is on PATH regardless of what
|
|
# the action asked for, so this is a note for when one of them misbehaves in a
|
|
# way that makes no sense, not a reason to stop a build that is probably fine.
|
|
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, and it is load-bearing rather than tidy. MinVer derives the version from the
|
|
# nearest v* tag, and checkout's default shallow clone has no tags at all — so it would not fail,
|
|
# it would quietly answer 0.0.0-alpha.0.N and every build would ship that. Velopack decides
|
|
# whether an installed client is out of date by comparing versions, which makes a plausible wrong
|
|
# answer here a client that never updates.
|
|
#
|
|
# Repeated in all three jobs, like the node preamble above and for the same reason. Change one
|
|
# copy, change all three.
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
# The daemon and the client are two different things to have, and this runner had only
|
|
# one of them. Testcontainers reaches Docker straight over /var/run/docker.sock from a
|
|
# .NET library, so every integration suite in the build job passed while `docker` was
|
|
# not a command here at all — which surfaced as exit 127 from the build step below,
|
|
# after the tags had been worked out and everything looked healthy.
|
|
#
|
|
# Only the CLI. The socket is already there and a daemon is already answering on it;
|
|
# installing an engine would start a second one beside the one being used.
|
|
- name: ensure the docker cli
|
|
run: |
|
|
set -eu
|
|
SUDO=""
|
|
[ "$(id -u)" -eq 0 ] || SUDO="sudo"
|
|
|
|
if ! command -v docker >/dev/null 2>&1; then
|
|
echo "Installing the docker cli"
|
|
if command -v apk >/dev/null 2>&1; then
|
|
$SUDO apk add --no-cache docker-cli
|
|
elif command -v apt-get >/dev/null 2>&1; then
|
|
$SUDO apt-get update -qq
|
|
$SUDO apt-get install -y --no-install-recommends docker.io
|
|
elif command -v dnf >/dev/null 2>&1; then
|
|
$SUDO dnf install -y docker-cli
|
|
else
|
|
echo "No apt-get, apk or dnf here, so the docker client cannot be installed from" >&2
|
|
echo "inside the job. Add it to the runner's image." >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
docker --version
|
|
|
|
# buildx after the client, and wanted rather than required. Without the plugin
|
|
# `docker build` falls back to the legacy builder, which still produces the image
|
|
# and says on every run that it will not do so forever; with it the same command
|
|
# routes through BuildKit and the Dockerfile's independent stages stop being
|
|
# serialised. Alpine's docker-cli package does not carry it, which is why a job
|
|
# that had just been given a working client still built the deprecated way.
|
|
#
|
|
# A distribution with no package for it should get a warning and an image, not a
|
|
# failed release — so every branch here ends in `|| true` and the check below
|
|
# reports rather than exits.
|
|
if ! docker buildx version >/dev/null 2>&1; then
|
|
echo "Installing buildx"
|
|
if command -v apk >/dev/null 2>&1; then
|
|
$SUDO apk add --no-cache docker-cli-buildx || true
|
|
elif command -v apt-get >/dev/null 2>&1; then
|
|
$SUDO apt-get update -qq || true
|
|
$SUDO apt-get install -y --no-install-recommends docker-buildx || true
|
|
elif command -v dnf >/dev/null 2>&1; then
|
|
$SUDO dnf install -y docker-buildx || true
|
|
fi
|
|
fi
|
|
|
|
if docker buildx version >/dev/null 2>&1; then
|
|
docker buildx version
|
|
else
|
|
echo "::warning::buildx is unavailable, so this image was built by the legacy" \
|
|
"builder Docker has deprecated. Add a buildx package to the runner image."
|
|
fi
|
|
|
|
# No docker/* actions here, deliberately. The build is single-architecture, so it
|
|
# wants the daemon this runner already has, a client, and BuildKit — all of which the
|
|
# step above arranges with two packages. What it does not want is QEMU, a builder
|
|
# instance to create and tear down, or a third-party action whose SHA has to be
|
|
# audited and re-pinned on a schedule. Adding linux/arm64 later is where that trade
|
|
# changes, and where setup-buildx-action starts earning its place.
|
|
- name: work out the tags
|
|
id: tags
|
|
env:
|
|
REGISTRY: registry-docker.dodotech.cloud
|
|
IMAGE: dodotech/dodossh-api
|
|
run: |
|
|
set -euo pipefail
|
|
repo="$REGISTRY/$IMAGE"
|
|
short="$(git rev-parse --short HEAD)"
|
|
|
|
# sha- prefixed, because a bare hex tag is ambiguous with a digest to both a human
|
|
# and a fair amount of tooling. This one is on every build and never moves, which
|
|
# makes it the only tag safe to pin a deployment to.
|
|
tags="$repo:sha-$short"
|
|
version="$short"
|
|
|
|
case "$GITHUB_REF" in
|
|
refs/tags/v*)
|
|
v="${GITHUB_REF#refs/tags/v}"
|
|
version="$v"
|
|
tags="$tags $repo:$v"
|
|
# The moving major.minor tag and :latest, but only for a release proper.
|
|
# v1.3.0-rc1 sorts after v1.2.9 and would otherwise take :latest with it,
|
|
# which is how a release candidate ends up on somebody's server.
|
|
case "$v" in
|
|
*-*) ;;
|
|
*)
|
|
tags="$tags $repo:${v%.*}"
|
|
tags="$tags $repo:latest"
|
|
;;
|
|
esac
|
|
;;
|
|
refs/heads/main)
|
|
tags="$tags $repo:main"
|
|
version="main-$short"
|
|
;;
|
|
esac
|
|
|
|
# The version MSBuild is allowed to see, which is not the same string as the one above.
|
|
# `version` is a docker tag and is `main-<short sha>` on a main build; handing that to
|
|
# -p:Version fails the publish with NETSDK1018. So this is set only when it is a real
|
|
# version, and the Dockerfile leaves the SDK default alone when it is empty.
|
|
assembly_version=""
|
|
case "$GITHUB_REF" in
|
|
refs/tags/v*) assembly_version="${GITHUB_REF#refs/tags/v}" ;;
|
|
esac
|
|
|
|
echo "tags=$tags" >> "$GITHUB_OUTPUT"
|
|
echo "version=$version" >> "$GITHUB_OUTPUT"
|
|
echo "assemblyVersion=$assembly_version" >> "$GITHUB_OUTPUT"
|
|
echo "created=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
|
|
echo "Tagging: $tags"
|
|
|
|
# Every value from a step output or the event goes through env rather than being
|
|
# interpolated into the script text. A git tag may contain a semicolon, and
|
|
# `${{ }}` is a textual substitution performed before the shell ever sees the line —
|
|
# so an interpolated tag name is a command the workflow agreed to run.
|
|
- name: build the image
|
|
env:
|
|
TAGS: ${{ steps.tags.outputs.tags }}
|
|
VERSION: ${{ steps.tags.outputs.version }}
|
|
ASSEMBLY_VERSION: ${{ steps.tags.outputs.assemblyVersion }}
|
|
REVISION: ${{ github.sha }}
|
|
CREATED: ${{ steps.tags.outputs.created }}
|
|
run: |
|
|
set -euo pipefail
|
|
args=()
|
|
for tag in $TAGS; do
|
|
args+=(--tag "$tag")
|
|
done
|
|
# --pull rather than whatever the runner happens to have cached: the base images
|
|
# are floating tags, and a runner that has held aspnet:10.0-noble-chiseled for a
|
|
# month is a month of unapplied CVE fixes shipping in every image built on it.
|
|
docker build \
|
|
--pull \
|
|
--file src/DodoSSH.Api/Dockerfile \
|
|
--build-arg "VERSION=$VERSION" \
|
|
--build-arg "ASSEMBLY_VERSION=$ASSEMBLY_VERSION" \
|
|
--build-arg "REVISION=$REVISION" \
|
|
--build-arg "CREATED=$CREATED" \
|
|
"${args[@]}" \
|
|
.
|
|
|
|
# Everything above runs on a pull request too. Building a fork's Dockerfile is safe —
|
|
# nothing is pushed and no credential is in scope — and it means a change that breaks
|
|
# the image fails on the PR rather than on main. Only these last two steps are held
|
|
# back, and the condition is on the event rather than on the branch so that a PR
|
|
# targeting main cannot reach them.
|
|
- name: log in to the registry
|
|
if: github.event_name != 'pull_request'
|
|
env:
|
|
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
|
|
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
|
run: |
|
|
set -eu
|
|
|
|
# Checked before use, because an unset secret is not an error anywhere upstream of
|
|
# here: an expression that resolves to nothing renders as the empty string, so
|
|
# docker is handed --username "" and answers with something about credentials,
|
|
# which sends people to the registry to debug a value that never left the
|
|
# settings page.
|
|
#
|
|
# Note for anyone editing this comment: an expression delimiter written literally
|
|
# here is interpolated even though this is a shell comment. The runner substitutes
|
|
# the whole script before any shell sees it, so an empty one fails the step with a
|
|
# parse error and no line number — which is how this very block broke the release
|
|
# it was added to protect.
|
|
#
|
|
# Reported by length, and never by value. Gitea masks known secret values in logs,
|
|
# but a mask is only as good as the runner's bookkeeping and a length answers the
|
|
# only question being asked: did anything arrive.
|
|
missing=""
|
|
[ -n "${REGISTRY_USERNAME:-}" ] || missing="$missing REGISTRY_USERNAME"
|
|
[ -n "${REGISTRY_PASSWORD:-}" ] || missing="$missing REGISTRY_PASSWORD"
|
|
|
|
if [ -n "$missing" ]; then
|
|
echo "Empty or unset:$missing" >&2
|
|
echo >&2
|
|
echo "Both come from repository secrets, which in Gitea are at" >&2
|
|
echo " Settings -> Actions -> Secrets" >&2
|
|
echo "and are a different page from Settings -> Actions -> Variables. A value" >&2
|
|
echo "added as a variable is invisible to the secrets context and arrives here" >&2
|
|
echo "as an empty string, which is exactly what this message means." >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "username: ${#REGISTRY_USERNAME} characters; password: set"
|
|
|
|
printf '%s' "$REGISTRY_PASSWORD" \
|
|
| docker login registry-docker.dodotech.cloud \
|
|
--username "$REGISTRY_USERNAME" --password-stdin
|
|
|
|
- name: push
|
|
if: github.event_name != 'pull_request'
|
|
env:
|
|
TAGS: ${{ steps.tags.outputs.tags }}
|
|
run: |
|
|
set -euo pipefail
|
|
for tag in $TAGS; do
|
|
docker push "$tag"
|
|
done
|
|
|
|
# The daemon is shared with every other job on this runner, and a credential left in
|
|
# ~/.docker/config.json outlives the job that created it. always(), so a failed push
|
|
# does not leave it behind.
|
|
- name: log out
|
|
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.
|
|
#
|
|
# 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.
|
|
#
|
|
# 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.
|
|
#
|
|
# 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
|
|
# has: it packages an APK nobody installs, so that a link-time break fails here rather than later.
|