docs: record the archived repos' channel audit and the cryptify version-tag hole - #320
Conversation
…on-tag hole Five repos were archived after their code moved; their release and package channels were never audited. Archiving retires code, not channels: an archived repo keeps serving Releases and release assets, a GHCR package is owned by the org rather than the repo, and raw.githubusercontent.com keeps serving the frozen default branch. A consumer left pointing at one reports success forever while its artifact set stops growing. CLAUDE.md gains the per-repo channel table (channel, who publishes it now, verdict) so the next consolidation does not re-derive it, plus the two findings that do not fit a table: how a self-hosted add-on update channel has to be handed over, and the cryptify version-tag hole. That hole: cryptify-v0.1.28 shipped three security fixes but delivery.yml had no cryptify publish job at that commit, so ghcr.io/encryption4all/cryptify:0.1.28 was never built and cannot be built by re-running anything. The highest semver tag in GHCR is still 0.1.27, pushed by the archived repo. COMPATIBILITY.md's pg-js support window said 1.x leaves when "the Outlook migration lands"; it has landed, and the issue it cited is closed inside an archived repo. Record the landed half and point at the workspace path.
|
Thanks, Ruben — I'm on it. Reviewing this PR now: the CLAUDE.md inventory of what still consumes the archived repos' Releases/GHCR/raw.githubusercontent.com channels, plus the two write-ups on the Thunderbird add-on update channel handoff and the missing |
There was a problem hiding this comment.
Three findings in the added prose: one wrong mechanism, one misattribution, one broken code span. The conclusions this PR draws are right, and the cryptify one is right for a stronger reason than the entry gives. Each fix is one line, suggestions attached.
Both factual findings also stand in the PR description
The description is not in the diff, so fixing only the file leaves the untrue version as the description of the work:
- body line 5 — "
#288–#291and#293each checked the publisher per repo before archiving" is the same misattribution asCLAUDE.md:47. #288–#291 are issue-transfer tasks; only #293 touched a publisher. - body line 10 — "
workflow_dispatchcannot recover it: it takes the workflow file from the ref you dispatch" is the same non-existent path asCLAUDE.md:58.delivery.ymldeclares noworkflow_dispatch.
Both are a body edit, so no push and no CI rerun.
Verified clean, so you do not have to re-check it
Every sha, date and interval in the new entry holds: 18e0ab0 at 09:38:49 → f1b2e14 at 11:03:29 is the 85 minutes claimed, and 506144c is #309 on 2026-08-07. delivery.yml:378 really is the sole semver-tag source, and release-plz-release really is gated if: github.ref == 'refs/heads/main'. The title passes the pr-title.yml conventional-commit gate. Part of #312 is the right deliberate non-closing form for half of a still-open task, and the #NN refs are all same-repo so they need no qualifying. No prose-rule breaches, and CLAUDE.md is the correct home for these lessons rather than the shared rule set.
| | `cryptify` | GitHub Releases `v0.1.1`–`v0.1.27` (no assets); GHCR `cryptify` | postguard `delivery.yml`, **same GHCR name**, sole publisher since #293 | The one real gap — see the version-tag hole below. Also `ghcr.io/encryption4all/cryptify-backend` is a **dead name** (renamed to `cryptify` years ago) that two live consumers still referenced. | | ||
|
|
||
| Two lessons the table does not carry. **A self-hosted add-on update channel needs one last publish from the old repo, and the successor release must not be `releases/latest`.** `postguard-tb-addon` got this right: installed builds ≤0.9.3 poll `postguard-tb-addon/releases/latest/download/updates.json`, so the old repo had to advertise 0.9.4 one final time before going read-only — hence the deliberately non-`v*` tag `channel-migration-0.9.4` (a `v*` tag would have fired the old build workflow). Archive first and every installed add-on stops updating silently. And in the monorepo the channel moved *off* `releases/latest` entirely, onto the raw file on `main`, because `releases/latest` in a repo shared with `@e4a/pg-js` is whichever package released most recently and a pg-js release carries no `updates.json`. **Verify a "verdict" release actually resolves**: `releases/latest` is not the newest `published_at` you see in the API list, so read it from `repos/{o}/{r}/releases/latest` and then `curl -L` the asset itself. | ||
| - **A release that predates its own publish job leaves a version with no artifact, and nothing reports it.** `cryptify-v0.1.28` was released from this workspace at `18e0ab0` (2026-08-03 09:38:49 +0200) carrying three security fixes — GHSA-5rhx-xgvv-h78h (unauthenticated `GET /usage`), a non-constant-time `cryptify_token` compare, and RUSTSEC-2026-0141 (lettre) — but `delivery.yml` had **no cryptify publish at all** at that commit: the jobs landed 85 minutes later in `f1b2e14` (#280), gated behind `vars.PUBLISH_CRYPTIFY_IMAGE`, ungated only in `506144c` (#309, 2026-08-07). The old repo never saw the release either, because the release happened here. So **`ghcr.io/encryption4all/cryptify:0.1.28` does not exist and cannot be built by re-running anything** — `workflow_dispatch` takes the workflow file from the ref you dispatch, and that file has no cryptify job. GHCR's highest semver tag is `0.1.27`, pushed by the *archived* repo on 2026-05-16; every tag this pipeline has pushed since taking over is `edge` or `pr-*`. Consequence for ops: `privacybydesign/postguard-ops`' `procolix/environments/prod.tfvars` pins `cryptify_image_tag = "0.1.25"`, and the newest tag it *could* move to today is still the archived repo's last build. The next cryptify release (release-plz #278 has `0.1.28 -> 0.1.29` queued) is what restores semver tagging from this repo; bump the ops pin to that, not to `0.1.28`. General rule: when moving a Docker publish between repos, land the publish job **before** the first release cut in the new home, and check the registry for the tag afterwards — a release-plz release and an image tag are separate events and only one of them is visible in the repo. |
There was a problem hiding this comment.
The stated reason 0.1.28 cannot be recovered is wrong. delivery.yml declares no workflow_dispatch at all — its on: block is only push: branches: [main] and pull_request (.github/workflows/delivery.yml:12-16); build.yml is the only workflow in the repo with a manual trigger, and it runs cargo test/clippy without publishing an image. So "workflow_dispatch takes the workflow file from the ref you dispatch" describes a path that does not exist here, and a reader who trusts it will try gh workflow run delivery.yml, get it rejected outright, and never learn why.
The conclusion is right and actually stronger: the semver tag comes solely from release-plz-release's cryptify_version output (delivery.yml:378), and that job is if: github.ref == 'refs/heads/main', so only a real release on main can ever push a semver tag. That is also exactly why every tag since the takeover is edge or pr-*.
| - **A release that predates its own publish job leaves a version with no artifact, and nothing reports it.** `cryptify-v0.1.28` was released from this workspace at `18e0ab0` (2026-08-03 09:38:49 +0200) carrying three security fixes — GHSA-5rhx-xgvv-h78h (unauthenticated `GET /usage`), a non-constant-time `cryptify_token` compare, and RUSTSEC-2026-0141 (lettre) — but `delivery.yml` had **no cryptify publish at all** at that commit: the jobs landed 85 minutes later in `f1b2e14` (#280), gated behind `vars.PUBLISH_CRYPTIFY_IMAGE`, ungated only in `506144c` (#309, 2026-08-07). The old repo never saw the release either, because the release happened here. So **`ghcr.io/encryption4all/cryptify:0.1.28` does not exist and cannot be built by re-running anything** — `workflow_dispatch` takes the workflow file from the ref you dispatch, and that file has no cryptify job. GHCR's highest semver tag is `0.1.27`, pushed by the *archived* repo on 2026-05-16; every tag this pipeline has pushed since taking over is `edge` or `pr-*`. Consequence for ops: `privacybydesign/postguard-ops`' `procolix/environments/prod.tfvars` pins `cryptify_image_tag = "0.1.25"`, and the newest tag it *could* move to today is still the archived repo's last build. The next cryptify release (release-plz #278 has `0.1.28 -> 0.1.29` queued) is what restores semver tagging from this repo; bump the ops pin to that, not to `0.1.28`. General rule: when moving a Docker publish between repos, land the publish job **before** the first release cut in the new home, and check the registry for the tag afterwards — a release-plz release and an image tag are separate events and only one of them is visible in the repo. | |
| - **A release that predates its own publish job leaves a version with no artifact, and nothing reports it.** `cryptify-v0.1.28` was released from this workspace at `18e0ab0` (2026-08-03 09:38:49 +0200) carrying three security fixes — GHSA-5rhx-xgvv-h78h (unauthenticated `GET /usage`), a non-constant-time `cryptify_token` compare, and RUSTSEC-2026-0141 (lettre) — but `delivery.yml` had **no cryptify publish at all** at that commit: the jobs landed 85 minutes later in `f1b2e14` (#280), gated behind `vars.PUBLISH_CRYPTIFY_IMAGE`, ungated only in `506144c` (#309, 2026-08-07). The old repo never saw the release either, because the release happened here. So **`ghcr.io/encryption4all/cryptify:0.1.28` does not exist and cannot be built by re-running anything** — `delivery.yml` has no `workflow_dispatch` trigger at all (only `push` on `main` and `pull_request`), and the semver tag comes solely from `release-plz-release`'s `cryptify_version` output, which only a real release on `main` produces. GHCR's highest semver tag is `0.1.27`, pushed by the *archived* repo on 2026-05-16; every tag this pipeline has pushed since taking over is `edge` or `pr-*`. Consequence for ops: `privacybydesign/postguard-ops`' `procolix/environments/prod.tfvars` pins `cryptify_image_tag = "0.1.25"`, and the newest tag it *could* move to today is still the archived repo's last build. The next cryptify release (release-plz #278 has `0.1.28 -> 0.1.29` queued) is what restores semver tagging from this repo; bump the ops pin to that, not to `0.1.28`. General rule: when moving a Docker publish between repos, land the publish job **before** the first release cut in the new home, and check the registry for the tag afterwards — a release-plz release and an image tag are separate events and only one of them is visible in the repo. |
| - Do not hand-write changelog entries under a crate's `## [Unreleased]` heading. `release-plz.toml` sets no changelog template, so entries are generated from commit subjects and each new version section is inserted *below* the `## [Unreleased]` heading. A manual bullet there is never folded into the release that follows it — it just sits above the newest version forever, undated. To get something into a dated changelog entry, it has to ride a commit subject on a PR that bumps that crate. | ||
| - `pg-wasm`'s web target: set `[package.metadata.wasm-pack.profile.release.wasm-bindgen] omit-default-module-path = true` in `pg-wasm/Cargo.toml` to drop the `new URL('index_bg.wasm', import.meta.url)` branch from generated `__wbg_init` glue (Webpack 5 otherwise statically resolves it and breaks bundler consumers that always pass an explicit `module_or_path`). Cleaner than post-build regex-stripping the generated JS. | ||
| - `pg-pkg` (the postguard PKG service) CLI flags: `-t irma_token`, `-i irma_server_url`, `-d postgres_url`. Env vars: `IRMA_SERVER`, `DATABASE_URL`, `RUST_LOG`. | ||
| - **Archiving a repo retires its *code*, never its *channels*, and the two are audited separately.** #288–#291 and #293 each checked the image/build *publisher* before archiving; nobody checked who still *fetches*. A channel outlives its source repo in every direction that matters: an archived repo keeps serving GitHub Releases and release assets, a GHCR package is owned by the org rather than the repo, and `raw.githubusercontent.com` keeps serving the frozen default branch. So the failure is never an error — it is a consumer pinned to an artifact set that stops growing, reporting success forever. The full audit of the five (#312, 2026-08-09) is below; **`ghcr.io/…` names were deliberately retained on the move, so an image name matching an archived repo is not by itself a hit** — check the publisher, then check whether new tags are actually arriving. |
There was a problem hiding this comment.
"#288–#291 and #293 each checked the image/build publisher before archiving" misattributes the publisher check. #288–#291 are issue-transfer tasks: #290 is "transfer postguard-tb-addon's 17 open issues into postguard-js, then re-archive", and the other three are the same shape for postguard-website, postguard-outlook-addon and postguard-examples. None of them has a publisher step. Only #293 ("cut cryptify's Docker publish pipeline over to postguard, then retire the old repo's build") was about a publisher.
That weakens the exact lesson the entry exists to record: for four of the five repos neither half was audited at archive time, which is a sharper warning than "the publisher was checked, the consumers were not".
| - **Archiving a repo retires its *code*, never its *channels*, and the two are audited separately.** #288–#291 and #293 each checked the image/build *publisher* before archiving; nobody checked who still *fetches*. A channel outlives its source repo in every direction that matters: an archived repo keeps serving GitHub Releases and release assets, a GHCR package is owned by the org rather than the repo, and `raw.githubusercontent.com` keeps serving the frozen default branch. So the failure is never an error — it is a consumer pinned to an artifact set that stops growing, reporting success forever. The full audit of the five (#312, 2026-08-09) is below; **`ghcr.io/…` names were deliberately retained on the move, so an image name matching an archived repo is not by itself a hit** — check the publisher, then check whether new tags are actually arriving. | |
| - **Archiving a repo retires its *code*, never its *channels*, and the two are audited separately.** #293 checked cryptify's image/build *publisher* before retiring the old build; #288–#291 only transferred open issues and re-archived. Nobody, on any of the five, checked who still *fetches*. A channel outlives its source repo in every direction that matters: an archived repo keeps serving GitHub Releases and release assets, a GHCR package is owned by the org rather than the repo, and `raw.githubusercontent.com` keeps serving the frozen default branch. So the failure is never an error — it is a consumer pinned to an artifact set that stops growing, reporting success forever. The full audit of the five (#312, 2026-08-09) is below; **`ghcr.io/…` names were deliberately retained on the move, so an image name matching an archived repo is not by itself a hit** — check the publisher, then check whether new tags are actually arriving. |
| - `@e4a/pg-js` (npm): the last two majors. `1.x` leaves the window when | ||
| telemetry shows no `1.x` traffic. The other half of that condition, the | ||
| Outlook add-in's v1 → v2 migration, **has landed**: the add-in lives at | ||
| `apps/outlook-addon` in [postguard-js] on `@e4a/pg-js": "workspace:*"` (2.x) |
There was a problem hiding this comment.
Malformed inline code span: the opening double quote of the package.json key is missing, so this renders as @e4a/pg-js": "workspace:*" — an unbalanced quote in what is meant to be a verbatim dependency line. No CI job lints markdown here, so nothing else will catch it.
| `apps/outlook-addon` in [postguard-js] on `@e4a/pg-js": "workspace:*"` (2.x) | |
| `apps/outlook-addon` in [postguard-js] on `"@e4a/pg-js": "workspace:*"` (2.x) |
|
Reviewed and requested changes on postguard#320 — the audit itself holds up (shas, timestamps, and the
The two factual points also appear in the PR description itself, so I called that out in the review body too since fixing just the files would leave the description out of sync. |
Part of #312 — the audit of what still pulls from the five archived repos' release and package channels. This is the durable half: the inventory belongs in the repo, not only in an issue comment.
What the audit found
#288–#291and#293each checked the publisher per repo before archiving. Nobody checked the consumers. Archiving retires code, not channels — an archived repo keeps serving Releases and release assets, a GHCR package is owned by the org rather than the repo, andraw.githubusercontent.comkeeps serving the frozen default branch. So a consumer left pointing at one never errors; it just stops receiving new artifacts.CLAUDE.mdgains the per-repo table (channels published, who publishes them now, verdict), plus the two findings a table cannot hold:postguard-tb-addon/releases/latest/download/updates.json, so that repo had to advertise 0.9.4 once more before going read-only — the deliberately non-v*tagchannel-migration-0.9.4. Verified end to end:releases/latestreally does resolve to it and itsupdates.jsonreally does point into postguard-js.ghcr.io/encryption4all/cryptify:0.1.28does not exist.cryptify-v0.1.28was released at18e0ab0(2026-08-03 09:38:49 +0200) carrying three security fixes;delivery.ymlhad no cryptify publish job at that commit — the jobs landed 85 minutes later inf1b2e14(ci(cryptify): publish the cryptify image from this repo #280) behindvars.PUBLISH_CRYPTIFY_IMAGE, ungated only in506144c(ci: publish cryptify's image unconditionally now that GHCR grants postguard Write #309). The old repo never saw the release either, because the release happened here.workflow_dispatchcannot recover it: it takes the workflow file from the ref you dispatch, and that file has no cryptify job. GHCR's highest semver tag is still0.1.27, pushed by the archived repo on 2026-05-16.That last one is the verdict
privacybydesign/postguard-opsneeds: itsprocolix/environments/prod.tfvarspinscryptify_image_tag = "0.1.25", and the newest tag it could move to today is still the archived repo's last build. release-plz #278 hascryptify: 0.1.28 -> 0.1.29queued; that release is what restores semver tagging from this repo, and the ops pin should move to it rather than to0.1.28.Also in here
COMPATIBILITY.md's pg-js support window made1.xleaving conditional on "the Outlook migration lands ([postguard-outlook-addon#125])". It has landed — the add-in is atapps/outlook-addonin postguard-js on"@e4a/pg-js": "workspace:*"and released asoutlook-addin-v1.0.0— and that issue is closed inside a now-archived repo. The remaining condition is the telemetry half. No policy change and the machine-readReader listblock is untouched.