Skip to content

ci(release): point the updater at this repository, and ship on the tested Node - #485

Merged
PathGao merged 1 commit into
masterfrom
ci/release-points-at-current-repo
Aug 6, 2026
Merged

ci(release): point the updater at this repository, and ship on the tested Node#485
PathGao merged 1 commit into
masterfrom
ci/release-points-at-current-repo

Conversation

@PathGao

@PathGao PathGao commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Three places where how we ship still names something that has moved on.

(a) The updater endpoint named the repository we no longer are

src-tauri/tauri.conf.json pointed auto-update at alecdotdev/Markpad. Now sftwrdotdev/Markpad.

Verified this session:

claim how
It was a transfer, not a rename GET /repos/alecdotdev/Markpad and GET /repos/sftwrdotdev/Markpad both return repository id 734498718 — one repository, reachable by both paths
alecdotdev is a separate, still-live account user id 18179415, created 2016-03-31, type User — distinct from the sftwrdotdev organization, id 311965959, created 2026-08-02
The old URL still works curl -IL on https://github.com/alecdotdev/Markpad/releases/latest/download/latest.jsonHTTP 301sftwrdotdev/Markpad → the release asset. The body it lands on is v2.7.0's feed ("version": "2.7.0")
That feed's keys are all unsuffixed darwin-aarch64, darwin-x86_64, linux-x86_64, windows-aarch64, windows-x86_64

The one-line change is the smaller half. The endpoint is compiled into the binary, so every installed copy up to and including v2.7.0 asks for the old URL and will use that redirect forever. And GitHub deletes a transfer redirect permanently if the old location is occupied again — from Transferring a repository:

If you create a new repository or fork at the previous repository location, the redirects to the transferred repository will be permanently deleted.

Note "or fork": forking sftwrdotdev/Markpad back to alecdotdev would void it just as a fresh repository would.

So alecdotdev/Markpad may never exist again, and nothing in the tree said so. tauri.conf.json is strict JSON and cannot carry a comment, so the constraint goes in RELEASING.md as a new §5, beside the other permanent release constraint (the un-rotatable pubkey). The failure it prevents is silent: latest.json 404s, the updater reports no update available, and users on old versions just stop being offered new ones.

Not a security issue, and the PR says so. The pubkey in tauri.conf.json is pinned and tauri-plugin-updater calls verify_signature(&buffer, &self.signature, &self.config.pubkey) in download before install (v2.10.1, src/updater.rs:712). Whoever ended up serving that URL could not produce a minisign signature that verifies, so they could not ship an update that installs. The exposure is broken or stalled updates, not code execution.

(b) build.yml built the shipped app on an end-of-life Node

It pinned node-version: '20' in two places, while test.yml and test_build.yml use lts/*. We tested on one Node and shipped on another.

Node 20 reached end-of-life in April 2026 and was removed from the runner images' toolcache in May. Confirmed in the v2.7.0 release run (30972242008) — all five jobs:

Attempting to download 20...
Acquiring 20.20.2 - x64 from .../node-20.20.2-linux-x64.tar.gz

against the most recent test_build.yml run, where lts/* is a cache hit:

Found in cache @ /opt/hostedtoolcache/node/24.18.0/x64

Both pins become lts/*. The risk is already retired: test_build.yml builds macOS universal, Linux and Windows on lts/*, and its latest run passed all three.

(c) A conditional coupling worth recording

__TAURI_BUNDLE_TYPE fails to be stamped into the Windows and Linux binaries. In the v2.7.0 build, three times (Windows x64, Windows arm64, Linux — never macOS):

Warn Failed to add bundler type to the binary: __TAURI_BUNDLE_TYPE variable
not found in binary. ... Updater plugin may not be able to update this
package.

Re-verified against tauri-plugin-updater v2.10.1 (the version in Cargo.lock) rather than taken from the warning text:

  • get_urls (src/updater.rs:568) pushes {os}-{arch}-{installer} only inside if let Some(installer) = installer, then unconditionally pushes {os}-{arch}.
  • installer_for_bundle_type(bundle_type()) is None when the variable is absent, so Windows/Linux clients look up the plain key and nothing else.
  • macOS is unaffected for a reason: tauri_utils::platform::bundle_type() returns Some(BundleType::App) on macOS in the fallback arm, without reading the variable.
  • build.yml's generate-update-feed job emits exactly the five unsuffixed keys, and the live latest.json confirms it.

It holds — this is harmless for us, so it gets a comment rather than a fix. The comment at the latest.json step records that adding a per-installer key (windows-x86_64-nsis, say) would make the missing bundle type suddenly load-bearing: Windows and Linux clients cannot ask for that key, so they would silently ignore it — and if the unsuffixed key were removed in the same edit, they would get TargetsNotFound and stop updating.

Tests

Three contract tests in scripts/releaseWorkflow.test.ts, each locking a fact that now exists in more than one place. Each was verified by breaking the property it protects:

broken message
build.yml back to node-version: '20' build.yml must request the same Node as the workflows that test the code it ships
build.yml pinned to '24' (i.e. right version, wrong mechanism) same — it asserts against the test workflows, not against a literal
test.yml'22', so the two test workflows disagree the test workflows disagree on Node: 22, lts/*
a windows-x86_64-nsis key added to latest.json latest.json key "windows-x86_64-nsis" carries an installer suffix; Windows and Linux clients cannot ask for one until the missing bundle type is fixed
endpoint reverted to alecdotdev/Markpad tauri.conf.json points the updater at alecdotdev/Markpad while RELEASING.md documents sftwrdotdev/Markpad
RELEASING.md drops the statement RELEASING.md must state which repository the updater endpoint names

The endpoint test failed falsification on its first shape and was rewritten. It originally asserted "the endpoint's repo is mentioned somewhere in RELEASING.md" — which passed with the endpoint reverted to alecdotdev/Markpad, because the new section warning against recreating that repository necessarily names it. The guard was satisfied by the very text that documents the hazard. It now matches the single sentence that states the endpoint's target.

npm run check — 0 errors, 651 files. npm test — 749/749.

What I could not verify

  • None of this can be proven by running it. (a) and (c) only fully execute during a release, and (b) only on a release runner. I read the v2.7.0 logs and the plugin source; I did not cut a release.
  • I did not test the negative case for the redirect. I have GitHub's documented statement that occupying the old location voids the redirect, and I confirmed the redirect currently works. I obviously did not create alecdotdev/Markpad to watch it break.
  • lts/* is a moving target. It resolved to 24.18 in the runs I looked at; a future LTS rollover changes what the release builds on, with no commit here. That is the same exposure the test workflows already accept, and the test now guarantees all three move together — but it is not a pin.
  • I did not confirm Node 20's toolcache removal date from an actions/runner-images changelog. What I confirmed is the behaviour: 20 downloads on every run, lts/* is a cache hit.
  • I have not observed a Windows or Linux client actually performing an update. (c) is a source-level argument that the two lookup keys reduce to one, plus the published feed matching that one. The end-to-end path is untested here.

Out of scope

The remaining alecdotdev references are the author's identity, not the repository's location, and are deliberately untouched: FUNDING.yml, the Chocolatey nuspec <authors>, the Windows registry Publisher value, and the bundle identifier com.alecdotdev.markpad — which must not change, or installed copies stop recognising themselves as upgradable. README.md, snapcraft.yaml and the in-app "source code" links still point at the redirect; they work, they are not compiled into a released binary's update path, and they belong in a separate pass.

🤖 Generated with Claude Code

…sted Node

Three places where how we ship still names something that has moved on.

**The updater endpoint named the old repository.** Markpad was
transferred from `alecdotdev/Markpad` to `sftwrdotdev/Markpad` --
verified as a transfer rather than a rename: both paths resolve to
repository id 734498718, and `alecdotdev` is still a live account
(user id 18179415, 2016) distinct from the `sftwrdotdev` organization
(id 311965959, created 2026-08-02). `src-tauri/tauri.conf.json` still
pointed at the old path, which works today only because GitHub answers
it with a 301; that chain currently resolves all the way to v2.7.0's
`latest.json`.

The one-line fix is the smaller half. Every installed copy of Markpad up
to v2.7.0 has the old URL compiled in and will use that redirect
forever, and GitHub deletes a transfer redirect permanently if the old
location is occupied again -- by a new repository *or by a fork*
([Transferring a repository]). So nobody may ever create
`alecdotdev/Markpad` again, and nothing in the tree said so.
`tauri.conf.json` is strict JSON and cannot carry a comment, so the
constraint goes in RELEASING.md beside the other permanent release
constraint, the un-rotatable pubkey.

That pubkey is also why this is an availability problem and not a
security one: the key is pinned and `tauri-plugin-updater` verifies the
minisign signature before installing, so whoever ends up serving that
URL cannot ship an update that installs. The exposure is stalled
updates, not code execution.

[Transferring a repository]: https://docs.github.com/en/repositories/creating-and-managing-repositories/transferring-a-repository

**build.yml built the shipped app on an end-of-life Node.** It pinned
`node-version: '20'` in both jobs while test.yml and test_build.yml use
`lts/*` -- so every release was produced by a runtime nothing had
exercised. Node 20 went end-of-life in April 2026 and left the runner
images' toolcache in May; the v2.7.0 run downloaded it fresh in all five
jobs (`Attempting to download 20... Acquiring 20.20.2`), while `lts/*`
is a cache hit on 24.18. Both pins become `lts/*`. The risk is already
retired -- test_build.yml builds macOS universal, Linux and Windows on
`lts/*` and passes on all three.

**A conditional coupling that was true by accident.** The
`__TAURI_BUNDLE_TYPE` patch fails on Windows and Linux, three times in
the v2.7.0 build:

    Warn Failed to add bundler type to the binary: __TAURI_BUNDLE_TYPE
    variable not found in binary. ... Updater plugin may not be able to
    update this package.

Re-checked against tauri-plugin-updater 2.10.1 rather than taken from
the warning: `get_urls` pushes `{os}-{arch}-{installer}` only when the
bundle type is known and *always* pushes `{os}-{arch}`, and the
`generate-update-feed` job publishes only unsuffixed keys. So the
warning is inert here, and macOS never sees it -- tauri-utils returns
`BundleType::App` there without reading the variable. Not a bug, so it
gets a comment rather than a fix. Adding a per-installer key later would
make the missing bundle type suddenly load-bearing.

All three are locked in scripts/releaseWorkflow.test.ts, since none of
them can be caught by running anything: the Node pins are asserted
against the test workflows rather than against a literal, the
`latest.json` keys must stay unsuffixed, and the endpoint must name the
repository RELEASING.md documents.

That last one had to be rewritten after falsification: matching "the
endpoint's repo is mentioned in RELEASING.md" passed with the endpoint
reverted to `alecdotdev/Markpad`, because the section warning against
recreating that repository necessarily names it. It now matches the one
sentence that states the endpoint's target.

Out of scope: the remaining `alecdotdev` references are the author's
identity, not the repository's location -- FUNDING.yml, the nuspec
author, the Windows `Publisher` value, and the bundle identifier
`com.alecdotdev.markpad`, which must not change or installed copies stop
recognising themselves as upgradable. The README, snapcraft.yaml and
in-app links still point at the redirect and are left for a separate
pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@PathGao
PathGao merged commit 1f953b2 into master Aug 6, 2026
4 checks passed
PathGao added a commit that referenced this pull request Aug 6, 2026
….0 npm (#490)

Follow-up to #484, which is now doing its job -- the recreated cargo
batch (#489) came back with 7 minor/patch crates instead of 16 with
majors inside. Three things that first pass did not cover, and one that
was planned and is not here.

**Cadence.** npm and cargo move to `quarterly`; github-actions stays
`monthly`. Security advisories never use this schedule -- they are a
repository setting and open their own pull requests as they land -- so
version updates exist only to stop drift, and drift costs what somebody
else's deadline costs. On Actions somebody else always sets it: runner
images retire toolchains on their own timetable, which is how `build.yml`
came to build releases on a Node pulled from the image toolcache (#485).
An Actions bump is also usually a one-line tag move. Nothing external
forces a TypeScript or a serde upgrade, and a quarter of them in one
grouped pull request is less interrupting and more reviewable than three
monthly ones.

**`minor` does not mean compatible below 1.0, and the two ecosystems
disagree about it.** Dependabot types an npm update from the literal
position of the number, so katex 0.16.47 -> 0.18.1 is a `minor` and went
straight into #487 -- the batch whose whole premise is that it is safe to
skim. It is not: KaTeX 0.18.0 renamed every internal CSS class and 0.17.0
changed `__defineFunction`, both flagged BREAKING upstream. npm's own
caret rule agrees, since `^0.16.47` stops before 0.17.

Cargo does not have this bug. `Dependabot::Cargo::Version` implements the
pre-1.0 rule, so windows 0.61.3 -> 0.62.2 counts as a major -- which is
visible in #489, where those crates are absent from the batch. The npm
side has no equivalent subclass.

There is no `update-types` value for "pre-1.0 minor", and `patterns`
match names rather than versions, so this cannot be written as a rule.
It can be written as a list: `exclude-patterns` names the three pre-1.0
npm dependencies, and they get individual pull requests. The test derives
that list from package.json, so a new 0.x dependency -- or an existing
one reaching 1.0 -- fails the suite instead of quietly rejoining the
batch.

**Two crates held.** `tauri` requires `windows ^0.61` and
`webview2-com ^0.38`, still true at 2.11.5. Taking `windows` 0.62 or
`webview2-com` 0.39 puts two `windows-core` versions in one graph and the
build stops in our own WebView2 call with E0599. Grouping already keeps
them out of the batch; what is left is an individual pull request every
quarter that cannot be merged at any published tauri 2.x.

Bounded to one minor series each rather than written as a semver level,
so the entries lapse. The semver-level form would work -- `Cargo::Version`
overrides `ignored_major_versions` so ">= 0.62" is what major means for a
0.61 crate -- but it would also swallow 0.63 and 0.64 in silence.
`windows` 0.63 does not exist yet; the day it does, Dependabot proposes
it and we find out whether tauri has moved.

The Tauri packages themselves are deliberately not ignored, and the file
says so: the lockfile is on 2.10.2 while 2.11.5 shipped 2026-07-01, and
the red pull request is the only notification there is.

**The cooldown is not here, because the case for it does not survive
being checked.** The mechanism is fine -- `cooldown` is valid alongside
`groups`, dependabot-core passes it on both the grouped and the individual
path, and it cannot touch a security update
(`update_cooldown: job.security_updates_only? ? nil : job.cooldown` in
both). What fails is the premise that the red pull requests are red
because their targets are fresh:

  - typescript 7.0.2 shipped 2026-07-08; svelte-check 4.7.4 shipped
    2026-07-27, nineteen days later, and narrowed its peer range to
    `^5.0.0 || ^6.0.0` -- excluding 7 on purpose.
  - windows 0.62.2 has been out since 2025-10-06 and webview2-com 0.39.1
    since 2026-03-11. Ten months and five.
  - katex 0.18.1 is three weeks old, but it is breaking at any age.
  - the rest is the Tauri parity guard, which is not about age at all.

None of them is a freshness problem. And on a quarterly schedule a major
caught by an N-day window is not delayed N days, it is delayed a quarter.
Also worth recording: GitHub Actions does not support `semver-major-days`
at all, so a cooldown could never have applied to the one ecosystem that
stays monthly.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
PathGao added a commit that referenced this pull request Aug 6, 2026
…d say less (#495)

Section 5 has been growing. #485 added it, and a follow-up wanted another
paragraph to close the loophole a careful reader finds — that a repository at
the old location could serve a correct feed and keep old installs working, so
the rule is not quite as absolute as it reads.

Prose was the wrong instrument. Section 4 earns its place by sitting directly
after the step it constrains: generating the keypair is step 1, and "the pubkey
is permanent" is the warning on step 1. Section 5 constrains an action that is
not a step in this runbook at all — nobody cutting a release is going to fork
the repository — so it has been compensating with length for not being adjacent
to anything.

The check asserts the property instead of a proxy for it. It does not ask
whether a redirect exists; it fetches that URL and checks that the feed's
download URLs still name this repository. A fork or a deletion fails it. Someone
occupying the old location while serving a correct feed passes it, which is the
right answer and is exactly the case the extra paragraph was going to spend
words on. A network failure is not evidence either way, so it warns and lets the
release proceed rather than blocking on a flake.

It runs before the draft is created, so a broken endpoint does not leave half a
release behind.

Verified against the live endpoint: the feed's five platform URLs all resolve to
this repository. Falsified three ways — a repository name that does not match
reports all five as foreign; a response that is not a feed takes the "not an
updater feed" branch; an unreachable URL takes the warning branch and exits 0.

Section 5 loses ten lines and keeps every instruction.

Co-authored-by: PathGao <gaoyanbo@gaoyanbodeMacBook-Air.local>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@PathGao PathGao mentioned this pull request Aug 6, 2026
@PathGao
PathGao deleted the ci/release-points-at-current-repo branch August 6, 2026 04:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant