Fail fast on missing psa-systems org secrets, and stop shipping the old org's update URL (LC-594) #564

Merged
longjacksonle merged 2 commits from chore/lc594-psa-systems-ci-secrets into main 2026-07-20 04:28:37 +02:00

Closes LC-594.

What the ticket asked for was already half done

The A8N_TOOLS_* -> PSA_SYSTEMS_* rename landed yesterday in 770f3cb, and it works. I verified it empirically rather than by reading: the OCI build and both desktop builds have run green on main since the move (runs 26948/26949/26950), so PSA_SYSTEMS_PRIVATE_PACKAGE_OWNER, PSA_SYSTEMS_PRIVATE_PACKAGE_PAT and RUNS_ON_OPENSUSE_BASE_LATEST all resolve in the new org. No workflow references an a8n name any more except one historical comment.

What was left is the release path, which has never run in psa-systems. create-release.yml has fired once ever (2026-06-24, pre-move) and publish-release.yml once, where it failed. Everything they need is org-level, and org values do not follow a repo across orgs.

Fail fast instead of failing deep

publish-release.yml only fires on a v* tag, so a missing org value surfaces twenty-odd minutes into a release rather than in CI. One of them would not have surfaced at all: DESKTOP_UPDATE_PUBLIC_KEY is passed as a build-arg with an empty default, and an empty key makes the updater return NotConfigured and refuse every update. An unset variable therefore ships binaries with a permanently dead self-updater, discoverable only by users, and unfixable for those users precisely because self-update is the thing that broke.

The new preflight step checks all four values before any build work and names every missing one in a single line. The existing late guard on the signing key stays as defence in depth. create-release.yml gets the same treatment for FORGEJO_PAT, so a missing token reports its own name instead of a 401 from a malformed Authorization header.

The stale org that was still compiled into the product

DEFAULT_UPDATE_URL in desktop/src/update.rs still read .../api/packages/a8n-tools/generic/lets-chat. Shipped binaries would have polled an owner CI no longer publishes under.

Rather than swap one hardcoded org for another, the publish workflow now derives the URL from the same PACKAGE_OWNER it uploads to and injects it via option_env!, mirroring how LETS_CHAT_UPDATE_PUBLIC_KEY already works. The upload destination and the compiled-in default now come from one value and cannot drift on the next org move. The in-source literal remains as the fallback for builds that inject nothing.

Verification

Both build-time arms were checked directly, not assumed: with a distinct value injected the injected value wins, and with an empty one it falls back. My first attempt at this check was vacuous (I injected a value identical to the fallback, so all three cases printed the same string) and I redid it.

The preflight nu script was executed locally on nu 0.112.2 in both the all-present and partially-missing cases, and every nu step in both workflows parses under nu --ide-check. The regression test asserts the absence of the stale owner rather than a fixed URL, since a release build legitimately compiles in a different string; it fails against the old constant. just check and the full just test suite are green.

Needs you, not code

I could not read the org variables directly: my token lacks read:organization and read:package, so presence of the release-only values is inferred, not confirmed. Worth a look in the psa-systems org settings before the next release:

  • DESKTOP_UPDATE_SIGNING_KEY and DESKTOP_UPDATE_PUBLIC_KEY are lets-chat-specific, no sibling repo uses them, and they are not repo-level here (repo secrets are empty, and the only repo variable is LETS_CHAT_UPDATE_URL). If they lived in the a8n-tools org they are gone, and the key pair has to be the same pair as before or already-shipped clients will reject the new manifest.
  • FORGEJO_PAT needs write:repository.

The preflight turns any of these being absent into an immediate, named failure rather than a broken release, so this is no longer urgent, only worth doing before you cut a tag.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BE9k4nWNUhPrjte9BcvASg

Closes LC-594. ## What the ticket asked for was already half done The `A8N_TOOLS_*` -> `PSA_SYSTEMS_*` rename landed yesterday in 770f3cb, and it works. I verified it empirically rather than by reading: the OCI build and both desktop builds have run green on `main` since the move (runs 26948/26949/26950), so `PSA_SYSTEMS_PRIVATE_PACKAGE_OWNER`, `PSA_SYSTEMS_PRIVATE_PACKAGE_PAT` and `RUNS_ON_OPENSUSE_BASE_LATEST` all resolve in the new org. No workflow references an `a8n` name any more except one historical comment. What was left is the release path, which has never run in psa-systems. `create-release.yml` has fired once ever (2026-06-24, pre-move) and `publish-release.yml` once, where it failed. Everything they need is org-level, and org values do not follow a repo across orgs. ## Fail fast instead of failing deep `publish-release.yml` only fires on a `v*` tag, so a missing org value surfaces twenty-odd minutes into a release rather than in CI. One of them would not have surfaced at all: `DESKTOP_UPDATE_PUBLIC_KEY` is passed as a build-arg with an empty default, and an empty key makes the updater return `NotConfigured` and refuse every update. An unset variable therefore ships binaries with a permanently dead self-updater, discoverable only by users, and unfixable for those users precisely because self-update is the thing that broke. The new preflight step checks all four values before any build work and names every missing one in a single line. The existing late guard on the signing key stays as defence in depth. `create-release.yml` gets the same treatment for `FORGEJO_PAT`, so a missing token reports its own name instead of a 401 from a malformed `Authorization` header. ## The stale org that was still compiled into the product `DEFAULT_UPDATE_URL` in `desktop/src/update.rs` still read `.../api/packages/a8n-tools/generic/lets-chat`. Shipped binaries would have polled an owner CI no longer publishes under. Rather than swap one hardcoded org for another, the publish workflow now derives the URL from the same `PACKAGE_OWNER` it uploads to and injects it via `option_env!`, mirroring how `LETS_CHAT_UPDATE_PUBLIC_KEY` already works. The upload destination and the compiled-in default now come from one value and cannot drift on the next org move. The in-source literal remains as the fallback for builds that inject nothing. ## Verification Both build-time arms were checked directly, not assumed: with a distinct value injected the injected value wins, and with an empty one it falls back. My first attempt at this check was vacuous (I injected a value identical to the fallback, so all three cases printed the same string) and I redid it. The preflight nu script was executed locally on nu 0.112.2 in both the all-present and partially-missing cases, and every nu step in both workflows parses under `nu --ide-check`. The regression test asserts the *absence* of the stale owner rather than a fixed URL, since a release build legitimately compiles in a different string; it fails against the old constant. `just check` and the full `just test` suite are green. ## Needs you, not code I could not read the org variables directly: my token lacks `read:organization` and `read:package`, so presence of the release-only values is inferred, not confirmed. Worth a look in the psa-systems org settings before the next release: - `DESKTOP_UPDATE_SIGNING_KEY` and `DESKTOP_UPDATE_PUBLIC_KEY` are lets-chat-specific, no sibling repo uses them, and they are not repo-level here (repo secrets are empty, and the only repo variable is `LETS_CHAT_UPDATE_URL`). If they lived in the a8n-tools org they are gone, and the key pair has to be the *same* pair as before or already-shipped clients will reject the new manifest. - `FORGEJO_PAT` needs `write:repository`. The preflight turns any of these being absent into an immediate, named failure rather than a broken release, so this is no longer urgent, only worth doing before you cut a tag. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01BE9k4nWNUhPrjte9BcvASg
chore(ci): fail fast on missing psa-systems org secrets, fix stale update URL
All checks were successful
check-secrets / Nosey parker (push) Successful in 5s
check-secrets / TruffleHog (push) Successful in 5s
check-secrets / Kingfisher (push) Successful in 14s
check-secrets / Nosey parker (pull_request) Successful in 4s
check-secrets / Kingfisher (pull_request) Successful in 12s
check-secrets / TruffleHog (pull_request) Successful in 13s
Check / clippy + fmt + tests (pull_request) Successful in 5m37s
ded73692d4
The a8n-tools -> psa-systems rename landed in 770f3cb and is proven for the package path: the OCI image and both desktop builds have run green on main since. Two gaps remained, both in the release path, which has not run in the new org at all.

Org values are now checked before any build work. Everything in these workflows resolves from org-level variables and secrets, and those do not follow a repo across orgs. The registry credentials failed loudly on the next push, but publish-release only fires on a `v*` tag, so a missing value there would surface twenty-odd minutes into a release. `DESKTOP_UPDATE_PUBLIC_KEY` would not have surfaced at all: it is a build-arg with an empty default, so an unset variable ships desktop binaries whose updater can never verify a manifest and therefore can never self-update to a fixed build. The preflight names every missing value in one line. `create-release.yml` gets the same guard on FORGEJO_PAT, which matches the psa-systems convention (mokosh-server and mokosh-apps read the same name) but is unverified here.

The desktop updater's default source is no longer a hardcoded org. `DEFAULT_UPDATE_URL` still named `a8n-tools`, so a shipped binary would have polled an owner CI no longer publishes under. The publish workflow now injects it from the same PACKAGE_OWNER it uploads with, so the compiled-in default cannot drift from the upload destination again; the in-source literal is the fallback for builds that inject nothing. Both build-time arms were verified directly: a distinct injected value wins, and an empty one falls back.

The regression test asserts the absence of the stale owner rather than a fixed URL, since a release build legitimately compiles in a different string. It fails against the old constant.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BE9k4nWNUhPrjte9BcvASg
fix(ci): correct the package owner to psa-systems-private
All checks were successful
check-secrets / Nosey parker (push) Successful in 5s
check-secrets / Kingfisher (pull_request) Successful in 5s
check-secrets / TruffleHog (pull_request) Successful in 7s
check-secrets / Nosey parker (pull_request) Successful in 6s
check-secrets / TruffleHog (push) Successful in 8s
check-secrets / Kingfisher (push) Successful in 10s
Check / clippy + fmt + tests (pull_request) Successful in 5m15s
Create release / Create release from merged PR (pull_request) Has been skipped
0abe6bbf9f
Read from the org rather than inferred: `PSA_SYSTEMS_PRIVATE_PACKAGE_OWNER` resolves to `psa-systems-private`, not `psa-systems`. The `lets-chat` generic package and container images both live under that owner; `psa-systems` holds the mokosh and vervain packages instead. The previous commit's fallback literal named the wrong one.

This only affects builds that inject nothing. Release builds derive the URL from the same variable they upload with, so they were already correct.

Also notes what the org read turned up: that owner is a private org, so an anonymous fetch of the default update URL is 401 and the default cannot serve public users as-is. Recorded next to the constant and in the README rather than worked around here, since it is a distribution decision, not a CI one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BE9k4nWNUhPrjte9BcvASg
Author
Owner

Follow-up after reading the org directly with a scoped token, which resolves the "needs you, not code" section of the description. Two corrections and one confirmation.

Corrected: the package owner is psa-systems-private, not psa-systems. The org variable resolves to the former, and that is where the lets-chat generic package and container images actually live; psa-systems holds the mokosh and vervain packages. The fallback literal in the first commit named the wrong one and is fixed in 0abe6bb. Release builds were correct either way, since they derive the URL from the same variable they upload with, which is precisely why deriving it was worth doing.

Confirmed: DESKTOP_UPDATE_SIGNING_KEY and DESKTOP_UPDATE_PUBLIC_KEY are absent at org level, with no repo-level secrets to fall back on. The next v* tag would have shipped desktop binaries with a permanently dead self-updater and no failure anywhere in CI. The preflight in this PR stops that release instead, which is the whole point of it. FORGEJO_PAT and PSA_SYSTEMS_PRIVATE_PACKAGE_PAT are both present.

Out of scope, filed as LC-607: the update channel is already broken end to end, independent of this PR. The packages owner is a private org, so an anonymous manifest fetch is 401 and no end user can reach it; and the latest package version holds the six binaries but no latest.json or latest.json.sig, so even an authenticated fetch 404s. Recovering the signing pair is also not merely "create a secret": the public key is compiled into already-distributed binaries, so the restored pair must be the same pair or those clients reject every future manifest.

This PR remains what it was: fail fast instead of failing deep, and stop compiling a stale org into the product. It does not fix distribution, and shouldn't.

Follow-up after reading the org directly with a scoped token, which resolves the "needs you, not code" section of the description. Two corrections and one confirmation. **Corrected: the package owner is `psa-systems-private`, not `psa-systems`.** The org variable resolves to the former, and that is where the `lets-chat` generic package and container images actually live; `psa-systems` holds the mokosh and vervain packages. The fallback literal in the first commit named the wrong one and is fixed in 0abe6bb. Release builds were correct either way, since they derive the URL from the same variable they upload with, which is precisely why deriving it was worth doing. **Confirmed: `DESKTOP_UPDATE_SIGNING_KEY` and `DESKTOP_UPDATE_PUBLIC_KEY` are absent** at org level, with no repo-level secrets to fall back on. The next `v*` tag would have shipped desktop binaries with a permanently dead self-updater and no failure anywhere in CI. The preflight in this PR stops that release instead, which is the whole point of it. `FORGEJO_PAT` and `PSA_SYSTEMS_PRIVATE_PACKAGE_PAT` are both present. **Out of scope, filed as LC-607:** the update channel is already broken end to end, independent of this PR. The packages owner is a private org, so an anonymous manifest fetch is 401 and no end user can reach it; and the `latest` package version holds the six binaries but no `latest.json` or `latest.json.sig`, so even an authenticated fetch 404s. Recovering the signing pair is also not merely "create a secret": the public key is compiled into already-distributed binaries, so the restored pair must be the *same* pair or those clients reject every future manifest. This PR remains what it was: fail fast instead of failing deep, and stop compiling a stale org into the product. It does not fix distribution, and shouldn't.
longjacksonle scheduled this pull request to auto merge when all checks succeed 2026-07-20 04:28:35 +02:00
longjacksonle deleted branch chore/lc594-psa-systems-ci-secrets 2026-07-20 04:28:38 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!564
No description provided.