ci(release): point the updater at this repository, and ship on the tested Node - #485
Merged
Merged
Conversation
…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>
This was referenced Aug 6, 2026
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>
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.jsonpointed auto-update atalecdotdev/Markpad. Nowsftwrdotdev/Markpad.Verified this session:
GET /repos/alecdotdev/MarkpadandGET /repos/sftwrdotdev/Markpadboth return repository id734498718— one repository, reachable by both pathsalecdotdevis a separate, still-live account18179415, created 2016-03-31, typeUser— distinct from thesftwrdotdevorganization, id311965959, created 2026-08-02curl -ILonhttps://github.com/alecdotdev/Markpad/releases/latest/download/latest.json→HTTP 301→sftwrdotdev/Markpad→ the release asset. The body it lands on is v2.7.0's feed ("version": "2.7.0")darwin-aarch64,darwin-x86_64,linux-x86_64,windows-aarch64,windows-x86_64The 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:
Note "or fork": forking
sftwrdotdev/Markpadback toalecdotdevwould void it just as a fresh repository would.So
alecdotdev/Markpadmay never exist again, and nothing in the tree said so.tauri.conf.jsonis strict JSON and cannot carry a comment, so the constraint goes inRELEASING.mdas a new §5, beside the other permanent release constraint (the un-rotatable pubkey). The failure it prevents is silent:latest.json404s, 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
pubkeyintauri.conf.jsonis pinned andtauri-plugin-updatercallsverify_signature(&buffer, &self.signature, &self.config.pubkey)indownloadbeforeinstall(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.ymlbuilt the shipped app on an end-of-life NodeIt pinned
node-version: '20'in two places, whiletest.ymlandtest_build.ymluselts/*. 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:
against the most recent
test_build.ymlrun, wherelts/*is a cache hit:Both pins become
lts/*. The risk is already retired:test_build.ymlbuilds macOS universal, Linux and Windows onlts/*, and its latest run passed all three.(c) A conditional coupling worth recording
__TAURI_BUNDLE_TYPEfails to be stamped into the Windows and Linux binaries. In the v2.7.0 build, three times (Windows x64, Windows arm64, Linux — never macOS):Re-verified against
tauri-plugin-updaterv2.10.1 (the version inCargo.lock) rather than taken from the warning text:get_urls(src/updater.rs:568) pushes{os}-{arch}-{installer}only insideif let Some(installer) = installer, then unconditionally pushes{os}-{arch}.installer_for_bundle_type(bundle_type())isNonewhen the variable is absent, so Windows/Linux clients look up the plain key and nothing else.tauri_utils::platform::bundle_type()returnsSome(BundleType::App)on macOS in the fallback arm, without reading the variable.build.yml'sgenerate-update-feedjob emits exactly the five unsuffixed keys, and the livelatest.jsonconfirms it.It holds — this is harmless for us, so it gets a comment rather than a fix. The comment at the
latest.jsonstep 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 getTargetsNotFoundand 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:build.ymlback tonode-version: '20'build.yml must request the same Node as the workflows that test the code it shipsbuild.ymlpinned to'24'(i.e. right version, wrong mechanism)test.yml→'22', so the two test workflows disagreethe test workflows disagree on Node: 22, lts/*windows-x86_64-nsiskey added tolatest.jsonlatest.json key "windows-x86_64-nsis" carries an installer suffix; Windows and Linux clients cannot ask for one until the missing bundle type is fixedalecdotdev/Markpadtauri.conf.json points the updater at alecdotdev/Markpad while RELEASING.md documents sftwrdotdev/MarkpadRELEASING.md must state which repository the updater endpoint namesThe 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
alecdotdev/Markpadto 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.actions/runner-imageschangelog. What I confirmed is the behaviour:20downloads on every run,lts/*is a cache hit.Out of scope
The remaining
alecdotdevreferences are the author's identity, not the repository's location, and are deliberately untouched:FUNDING.yml, the Chocolatey nuspec<authors>, the Windows registryPublishervalue, and the bundle identifiercom.alecdotdev.markpad— which must not change, or installed copies stop recognising themselves as upgradable.README.md,snapcraft.yamland 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