Name the organisation the repository actually lives in
ci / build and test (push) Canceled after 0s
ci / android head (push) Canceled after 0s
ci / api image (push) Canceled after 0s

It moved to DodoTech-Public, and every address in the product still said
DodoTech. That looked like it worked, which is the part worth writing down:
Gitea leaves a 301 at the old path and HttpClient follows a redirect on a GET, so
both update channels would have kept polling through it.

What a 301 does not survive is a POST. `vpk upload gitea` publishes the desktop
release by POSTing to that URL, so the stale address would have failed at the one
step the whole feature depends on — and a redirect is a thing an operator can
delete, which turns "works today" into the same silent outage this session has
already spent two commits on.

So both channel constants, both release scripts, the workflow's REPO, the image's
source label and the curl in phase 16 all name the live path. The local remote
too, which had been printing a redirect warning on every push.

Measured after the move: the org, the repo and the nightly release all answer 200
anonymously, and that release now carries both assets — the manifest and a 54 MB
APK. The upload going through also answers the open question about the reverse
proxy's body-size limit, which nothing local could test.
This commit is contained in:
2026-08-05 12:51:19 +02:00
parent 23f1db9dc8
commit 253c72d2b7
9 changed files with 30 additions and 9 deletions
+1 -1
View File
@@ -517,7 +517,7 @@ jobs:
if: github.ref == 'refs/heads/main'
env:
FORGE: https://git.dodotech.cloud
REPO: DodoTech/DodoSSH
REPO: DodoTech-Public/DodoSSH
TOKEN: ${{ secrets.GITHUB_TOKEN }}
STAGED: ${{ steps.nightly.outputs.staged }}
VERSION: ${{ steps.nightly.outputs.version }}
+1 -1
View File
@@ -141,7 +141,7 @@ not go. If the repository is private, every check answers `404`, and the honest
about that is say so. It is one line to check:
```bash
curl -so /dev/null -w '%{http_code}\n' https://git.dodotech.cloud/api/v1/repos/DodoTech/DodoSSH
curl -so /dev/null -w '%{http_code}\n' https://git.dodotech.cloud/api/v1/repos/DodoTech-Public/DodoSSH
```
`200` and updates work. `404` and they cannot. `/api/v1/version` answering `200` proves only that the forge
@@ -118,7 +118,7 @@ than assumed:
- **The feed repository must be readable with no credentials, and that is a deployment precondition** rather
than a property of the code. It is checkable in one line, which is the only reason it is worth stating:
`curl -so /dev/null -w '%{http_code}' https://git.dodotech.cloud/api/v1/repos/DodoTech/DodoSSH` answers
`curl -so /dev/null -w '%{http_code}' https://git.dodotech.cloud/api/v1/repos/DodoTech-Public/DodoSSH` answers
`200` when the design holds and `404` when it does not. `/api/v1/version` answering `200` proves only that
the forge is reachable, which is what made this look like nothing was wrong.
- **An unreachable channel must be distinguishable from a current build.** `IUpdateChannel.CheckAsync` used
+1 -1
View File
@@ -1802,7 +1802,7 @@ Nothing else in this phase or the next means anything until this passes. Run it
nothing:
```bash
curl -so /dev/null -w '%{http_code}\n' https://git.dodotech.cloud/api/v1/repos/DodoTech/DodoSSH
curl -so /dev/null -w '%{http_code}\n' https://git.dodotech.cloud/api/v1/repos/DodoTech-Public/DodoSSH
```
**Pass:** `200`.
+1 -1
View File
@@ -50,7 +50,7 @@ $RepoRoot = Split-Path -Parent $PSScriptRoot
$Project = Join-Path $RepoRoot 'src/DodoSSH.Client.Android/DodoSSH.Client.Android.csproj'
$Staging = Join-Path $RepoRoot 'artifacts/android'
$Forge = 'https://git.dodotech.cloud'
$Repo = 'DodoTech/DodoSSH'
$Repo = 'DodoTech-Public/DodoSSH'
$Api = "$Forge/api/v1/repos/$Repo"
function Write-Step([string] $Message) {
+5 -1
View File
@@ -62,7 +62,11 @@ $PackAuthors = 'DodoTech'
# The project's own forge. Never a DodoSSH deployment — ADR 0011 rule 2. The same URL is a constant in
# VelopackUpdateChannel, and the two have to agree or the client polls somewhere nothing is published.
$RepoUrl = 'https://git.dodotech.cloud/DodoTech/DodoSSH'
#
# The owner is part of it. When the repository moved organisations Gitea left a 301 at the old path, which
# a GET follows and an upload does not — so a stale URL here fails at the one step that matters rather than
# redirecting quietly.
$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
+1 -1
View File
@@ -111,7 +111,7 @@ LABEL org.opencontainers.image.title="DodoSSH API" \
org.opencontainers.image.description="DodoSSH server: sync, identity, teams and relay authorization." \
org.opencontainers.image.vendor="DodoTech" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.source="https://git.dodotech.cloud/DodoTech/DodoSSH" \
org.opencontainers.image.source="https://git.dodotech.cloud/DodoTech-Public/DodoSSH" \
org.opencontainers.image.version="${VERSION}" \
org.opencontainers.image.revision="${REVISION}" \
org.opencontainers.image.created="${CREATED}"
@@ -76,7 +76,14 @@ internal sealed partial class ForgeJsonContext : JsonSerializerContext;
internal sealed class AndroidUpdateChannel : IUpdateChannel
{
/// <summary>The project's own forge, and the one address in this file.</summary>
private const string RepositoryApi = "https://git.dodotech.cloud/api/v1/repos/DodoTech/DodoSSH";
/// <remarks>
/// ◆ The owner is <c>DodoTech-Public</c> and that half of the path matters. The repository was moved
/// between organisations, and Gitea leaves a 301 at the old one — so a client still naming it looks
/// fine, because <c>HttpClient</c> follows a redirect on a GET. What it buys is a dependency on a
/// redirect somebody can delete, and it does not extend to the release scripts, whose uploads are
/// POSTs. The live path is named here, in <c>VelopackUpdateChannel</c> and in both scripts.
/// </remarks>
private const string RepositoryApi = "https://git.dodotech.cloud/api/v1/repos/DodoTech-Public/DodoSSH";
/// <summary>
/// The session name the installer writes under, and it is reused rather than made unique.
@@ -75,14 +75,24 @@ internal sealed class VelopackUpdateChannel : IUpdateChannel
/// </summary>
/// <remarks>
/// <b>This must never become a setting.</b> ADR 0011 rule 2 says the deployment a client signs in to
/// <para>
/// is never where the client comes from, and it says the same about the update check: an operator who
/// can answer "is there a newer version" can answer "no" forever, and pin a chosen user to a build
/// with a known hole without holding any key. A configurable feed URL is exactly the knob that would
/// hand them that, whether through a settings screen or through somebody editing the plaintext
/// settings.json by hand. A constant is that rule expressed structurally rather than as a convention
/// somebody has to keep.
/// </para>
/// <para>
/// ◆ <b><c>DodoTech-Public</c>, and the owner is part of the address rather than incidental.</b> The
/// repository was moved between organisations, and Gitea leaves a 301 behind at the old path — which is
/// why a client pointing at the old one appears to work: <c>HttpClient</c> follows a redirect on a GET.
/// It does not follow one on a POST, so <c>vpk upload</c> against the stale URL fails rather than
/// redirecting, and a redirect is a thing an operator can remove. Both heads and both release scripts
/// name the live path.
/// </para>
/// </remarks>
private const string RepositoryUrl = "https://git.dodotech.cloud/DodoTech/DodoSSH";
private const string RepositoryUrl = "https://git.dodotech.cloud/DodoTech-Public/DodoSSH";
/// <summary>
/// The release channel to read, and it is stated rather than left to the default.