Skip to content

ci!: stop publishing the "portable" binaries, and delete publish-portable.yml - #4380

Merged
jqnatividad merged 6 commits into
masterfrom
drop-portable-workflow
Aug 9, 2026
Merged

ci!: stop publishing the "portable" binaries, and delete publish-portable.yml#4380
jqnatividad merged 6 commits into
masterfrom
drop-portable-workflow

Conversation

@jqnatividad

@jqnatividad jqnatividad commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

The premise no longer holds

The portable subvariants (qsvp, qsvplite, qsvpdp, qsvpmcp) existed as a SIGILL
fallback — README described them as "compiled without any CPU features enabled."

-C target-cpu=native was removed from the x86_64 publish targets in fd74f8f0c
precisely because it caused SIGILL on GitHub runners, so the regular prebuilts have been
equally portable ever since. Today target-cpu appears in exactly one place —
.cargo/config.toml's [target.aarch64-apple-darwin] — and there is no [build] section.
publish-portable.yml built no macOS target, so across all five targets it did build,
there was no CPU-feature difference at all.

The shipped artifacts confirm it

Central directory of qsv-22.0.1-x86_64-unknown-linux-gnu.zip (read via an HTTP range
request on the tail — no 479 MB download):

entry uncompressed compressed
qsvlite 21,718,792 8,099,201
qsvplite 21,718,792 8,099,201 identical
qsvdp 105,779,368 33,982,234
qsvpdp 105,779,368 33,982,234 identical
qsvmcp 127,310,080 41,593,747
qsvpmcp 127,310,080 41,594,842 same size
qsv 193,374,752 65,763,107 PGO
qsvp 179,134,752 61,079,681 non-PGO

Matching compressed sizes to the byte is near-conclusive for identical content. Only
qsvp differed from qsv, and only because qsv is PGO-optimized while qsvp was not —
an optimization difference, not a portability one. The duplication added ~330 MB to that
archive, ~255 MB of it byte-identical copies of binaries already inside it.

Not a regression

qsvp.exe is already absent from 22.0.1's Windows MSVC zip: portable's Windows legs
build qsvmcp from the release tag and so hit the same viz_static stall (#4376) that kept
Windows from publishing at all. This change makes an existing state explicit rather than
creating one.

Docs

  • README:167 — SIGILL-fallback paragraph rewritten: the x86_64 prebuilts are already portable.
  • README:305 — Variants note now explains the subvariants are no longer published, and why.
  • README:169 — Easy-installer paragraph de-qsvp'd, plus an [!IMPORTANT] note (see below).
  • README:165deliberately left alone. Its target-cpu=native claim for Apple Silicon,
    Windows ARM, ppc64le and s390x was verified accurate — each of those workflows does set it.
  • CHANGELOG ### Removed entry carrying the table above.
  • Stale comment references in publish-target.yml and rust-linux-arm64.yml.

Follow-up in an external repo — a deadline, not an outage

dathere/qsv-easy-windows-installer extracts qsvp.exe and must be repointed at qsv.exe.

An earlier revision of this PR claimed it was already broken. That was wrong, and the
correction came from the installer-side session, verified independently on both sides:

  • The installer calls api.github.com/repos/dathere/qsv/releases/latest, which excludes
    prereleases
    . 22.0.1 is published with prerelease=true, so that endpoint resolves to
    21.1.0 — whose Windows MSVC zip does contain qsvp.exe. The installer works today.
  • I had verified against 22.0.1's zip, which the installer never fetches.

The break lands when the first stable release ships without qsvp.exe — i.e. when
22.0.1 is promoted, or at the next stable tag. Merging this PR does not itself break anyone.

Also surfaced by that session, and worth recording here:

  • The installer locates the binary by a single hardcoded literal
    (src-tauri/src/lib.rs:34, zip.by_name("qsvp.exe")); its destination is already
    qsv.exe. The fix is that one string.
  • It performs no zipsign verification — it fetches over HTTPS, extracts, and prepends its
    bin dir to HKCU\Environment\Path. Noting it so nobody assumes downstream installers
    verify our signatures; qsv's docs make no such claim, and this PR does not add one.
  • It reads the release title (.name) and interpolates it into the download URL as if it
    were the tag. That works only because qsv titles releases with the bare version — verified
    true for all 8 most recent releases.

Both release-time constraints this repo owns (keep title == tag; don't promote out of
prerelease before the installer ships) are now recorded in .claude/skills/release-prep.

Validation

actionlint reports 0 findings on every touched file;
scripts/check-publish-matrix-sync.py OK; scripts/docs-drift-check.py OK; the #variants
and #installation-options anchors were verified to exist.

Note: publish-qsvpy.yml has 12 pre-existing actionlint findings on master (shellcheck
errors and an undefined musl-prep key). Untouched here — flagging so they aren't read as
fallout from this change.

🤖 Generated with Claude Code

…able.yml

The portable subvariants (qsvp, qsvplite, qsvpdp, qsvpmcp) existed as a SIGILL
fallback -- README described them as "compiled without any CPU features
enabled". That distinction no longer exists.

`-C target-cpu=native` was removed from the x86_64 publish targets in fd74f8f
precisely because it caused SIGILL on GitHub runners, so the regular prebuilts
have been equally portable ever since. Today `target-cpu` appears in exactly one
place -- .cargo/config.toml's [target.aarch64-apple-darwin] -- and there is no
[build] section. publish-portable.yml built no macOS target, so for all five
targets it did build there was no CPU-feature difference at all.

The shipped artifacts confirm it. Reading the central directory of
qsv-22.0.1-x86_64-unknown-linux-gnu.zip:

    entry                uncompressed    compressed
    qsvlite                21,718,792     8,099,201
    qsvplite               21,718,792     8,099,201   identical
    qsvdp                 105,779,368    33,982,234
    qsvpdp                105,779,368    33,982,234   identical
    qsvmcp                127,310,080    41,593,747
    qsvpmcp               127,310,080    41,594,842   same size
    qsv                   193,374,752    65,763,107   PGO
    qsvp                  179,134,752    61,079,681   non-PGO

Matching compressed sizes to the byte is near-conclusive for identical content.
Only qsvp differed from qsv, and only because qsv is PGO-optimized while qsvp
was not -- an optimization difference, not a portability one. The duplication
added ~330MB to that archive, ~255MB of it byte-identical copies of binaries
already inside it.

Deleting the workflow does not regress the current release: qsvp.exe is already
absent from 22.0.1's Windows MSVC zip, because portable's Windows legs build
qsvmcp from the release tag and so hit the same viz_static stall that kept
Windows from publishing at all.

Docs updated accordingly: the SIGILL fallback paragraph and the Variants note
now explain that the subvariants are no longer published and why. README's
target-cpu claim for Apple Silicon / Windows ARM / ppc64le / s390x is left
alone -- it was verified accurate, each of those workflows does set it.

KNOWN FOLLOW-UP, external repo: dathere/qsv-easy-windows-installer fetches the
qsvp binary. It is already broken for 22.0.1 and must be repointed at qsv. A
README note flags this for users until that ships.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

jqnatividad and others added 2 commits August 9, 2026 11:30
My previous commit's follow-up note said dathere/qsv-easy-windows-installer was
"already broken for 22.0.1", and README carried an [!IMPORTANT] banner telling
Windows users to bypass it. Both were wrong, and the banner steered users away
from an installer that works.

The installer calls api.github.com/repos/dathere/qsv/releases/latest, which
EXCLUDES prereleases. 22.0.1 is published with prerelease=true, so that endpoint
resolves to 21.1.0 -- verified -- and 21.1.0's Windows MSVC zip does contain
qsvp.exe (qsv, qsvlite, qsvmcp, qsvp, qsvplite, qsvpmcp, qsvpy311/312/313).
I had checked 22.0.1's zip, which the installer never fetches.

So this is a deadline, not an outage: the break lands when the first STABLE
release ships without qsvp.exe. Banner removed, and the Variants note now says
portable shipped "up to and including 21.1.0" and is being discontinued, rather
than claiming it is already gone -- the current stable release still has it.

Recorded both cross-repo constraints in the release-prep skill, since they are
release-time checks this repo owns:
  - the release TITLE must stay identical to the tag; the installer interpolates
    .name into the download URL as the tag, so "v22.0.1" would 404 every user
  - confirm the installer has shipped its qsvp -> qsv change before promoting a
    release out of prerelease

Both corrections came from the qsv-easy-windows-installer session, which
verified the release metadata independently.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ange

roborev 4144 (LOW, README.md:305), valid on both counts.

`qsvpmcp` was missing from the list of portable subvariants, and "up to and
including 21.1.0" contradicted this branch's own CHANGELOG entry, which cites
qsvp/qsvplite/qsvpdp/qsvpmcp inside qsv-22.0.1-x86_64-unknown-linux-gnu.zip.
22.0.1's Linux archives really do carry them -- a portable publish run got
through its three Linux legs before its two Windows legs were cancelled -- while
22.0.1's Windows archives have no qsvp.exe at all.

Added qsvpmcp and replaced the release-range claim with "varies by release and
platform", plus an explicit migration mapping so anyone using a portable binary
knows which regular one replaces it.

Did not adopt the reviewer's suggested "distinguish stable from prerelease"
framing literally: portable never built any macOS target, so 21.1.0's Darwin
archive has no portable binaries either, and any per-platform coverage sentence
would have been wrong in a different way. Naming the variance and giving the
mapping is both shorter and true.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jqnatividad and others added 3 commits August 9, 2026 16:16
qsv-easy-windows-installer v1.1.2 fixed both assumptions this repo was working
around: it reads `tag_name` instead of the release title, and extracts
`qsv.exe` instead of `qsvp.exe` (074b16b).

Verified against the live API rather than taking the release note on trust:
`releases/latest` -> tag, the constructed download URL returns 200, and
`qsv.exe` is present in the archive -- for 21.1.0 (current stable) and for
22.0.1 (what latest resolves to once it is promoted). So the download and
extract path is exercised end to end. The HKCU\Environment\Path edit is not,
and needs a real Windows machine.

The constraints narrow rather than disappear. Users still on v1.1.1 or earlier
extract `qsvp.exe` by hardcoded name; they keep working only because
`releases/latest` skips prereleases and so still resolves to 21.1.0, which
ships `qsvp.exe`. Promoting 22.0.1 to stable breaks them. That is now recorded
as a release-notes item, along with the reason to keep titles == tag for those
same users even though v1.1.2+ no longer cares.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… item

I described the ≤ v1.1.1 breakage as an unwrap() panic. Wrong in the way that
matters. Verified against that repo's actual code:

  src-tauri/src/lib.rs:8   async fn run_path_update(app_handle: tauri::AppHandle)
                           -> returns (), not Result; no error channel to the UI
  src/App.tsx:25-28        invoke("run_path_update").finally(() => { setLoading(false);
                           alert("Successfully installed qsv...") })  -- no .catch()

The success alert fires unconditionally. Affected users are TOLD the install
succeeded while nothing was written, so they will report "qsv is not on my PATH"
or "qsv didn't update" — never a crash, never an error message.

That installer also has no self-update or version-check path, so it cannot reach
already-installed v1.1.1 users. The qsv release note is therefore not a partial
mitigation, it is the entire remedy. Reworded for the observable symptom and
added a suggested wording that tells users to verify with `qsv --version` rather
than trust the installer's own success message.

Both corrections came from the qsv-easy-windows-installer session and were
verified here against that repo's code before being written down.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…n claim

roborev 4155, two LOW findings, both valid.

README.md:171 — the Easy-installer download badge still linked v1.1.1, the exact
version the release-prep guidance added two commits ago tells users to upgrade
away from. The public install path was handing people the broken version.
Repointed to v1.1.2 after confirming that release is published (not draft, not
prerelease), is that repo's `latest`, and that the asset URL returns 200.

release-prep/SKILL.md — the verification sentence claimed releases/latest was
checked "for both 21.1.0 and 22.0.1", which contradicts the very next bullet
saying releases/latest excludes prereleases and still resolves to 21.1.0. Both
statements were true of different checks; the sentence conflated them. Split:
21.1.0 verified through releases/latest (the live path today), 22.0.1 verified
by building the same URL from its tag, as the post-promotion target.

Also took the reviewer's alternative suggestion in addition to the link fix,
since they are not redundant: the badge is a hardcoded versioned MSI URL that
follows nothing and goes stale silently, so keeping it current is now a
release-prep checklist item rather than something to rediscover by review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jqnatividad
jqnatividad merged commit 209a807 into master Aug 9, 2026
28 checks passed
@jqnatividad
jqnatividad deleted the drop-portable-workflow branch August 9, 2026 20:24
jqnatividad added a commit that referenced this pull request Aug 10, 2026
…orkflow to strip the portable binaries from its zip (#4382)

* docs(changelog): file the post-tag packaging work under 22.0.1, not Unreleased

The 22.0.1 release is still a pre-release, and fifteen commits of packaging and
publishing work landed after the tag to make it shippable. Both changelogs filed
that work under `[Unreleased]` — the section directly above `[22.0.1]` — so it read
as coming *after* the release it actually ships in.

Move those entries into each file's 22.0.1 section under a new "Packaging &
Publishing (post-tag)" subsection. The tag stays at 0dbd4c5, so the preamble says
plainly that these landed after it rather than implying the tag contains them, and
notes that a per-target asset refresh is still in flight — the x86_64-linux-gnu zip
predates the portable-binary removal by about six hours and still contains them.

Also corrects the synthesize/profile target list, which was wrong in both
directions: it claimed the x86_64 Windows targets had `profile` (they have neither
`profile` nor `synthesize`) and omitted aarch64-apple-darwin (which has both, via
the self-hosted macOS ARM64 workflows — publish.yml's entry for that target is
commented out). Verified against each publish workflow's feature list.

The 21.1.0...22.0.1 compare link only covers the tag, so a second SHA-pinned link
is added for the post-tag range rather than a moving master ref.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: correct two stale viz_static claims that #4368 invalidated

#4368 dropped viz_static from the Windows publish targets, which left two places
asserting the opposite.

CHANGELOG.md's 22.0.1 viz entry claimed "Windows and macOS now compile the
viz_static tier". That is false for Windows at HEAD — rust-windows.yml builds
`...,ui,viz` only, because adding viz_static pushed the job from ~27min past its
90min timeout without ever reaching the tests. The entry contradicted the
"Windows prebuilts: viz yes, viz_static no" note in its own section. Narrowed to
macOS, with the Windows revert and its reason spelled out.

rust-windows.yml's own comment still justified itself with "Windows ships
viz_static in publish.yml" — the premise #4368 removed. The conclusion (no
Windows viz_static coverage) happens to still hold, so only the reasoning is
rewritten; the cargo test invocation is untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci: add a manual workflow to strip portable binaries from a published zip

#4380 stopped publishing the portable binaries (qsvp, qsvplite, qsvpdp, qsvpmcp),
but assets uploaded before it landed still contain them — the 22.0.1
x86_64-unknown-linux-gnu zip among them. Rebuilding a target to drop four files is
disproportionate: that zip already postdates #4371/#4372/#4373, so the portable
binaries are its only discrepancy, and a rebuild would emit non-deterministic PGO
output differing from what users already downloaded.

The unsign -> modify -> re-sign flow mirrors macOS-arm64-selfhosted-publish-qsvpy.yml,
which already rewrites a published pre-release zip to add qsvpy binaries.

Scoped to x86_64-unknown-linux-gnu, with no target input, and running on ubuntu-24.04
to match the runner publish.yml builds that artifact on — glibc is forward
incompatible, so a binary built on 24.04 will not start on 22.04 and the smoke test
would fail on a good archive. The post-delete assertion requires qsv + qsvlite +
qsvdp + qsvmcp to survive, which holds only for this target: publish-target.yml skips
qsvdp for aarch64-unknown-linux-gnu and all non-Linux targets, and skips qsvmcp for
musl and aarch64-unknown-linux-gnu. The header records how to extend it properly.

Deletes with `7z d -tzip`, not Info-ZIP `zip -d`: publish-target.yml creates these
archives with `7z a -tzip`. Entries are resolved by exact basename rather than a
glob — a `qsv*` prefix match would take `qsv` itself with them.

Trust model, because this replaces a published artifact:
- the DOWNLOADED asset is verified against the release tag's public key before
  anything touches it. Without that anchor the workflow would strip whatever
  signature the asset carried (or none) and apply a fresh valid one, laundering a
  tampered artifact into a signed release asset — every later check only attests to
  the signature this workflow itself created
- the public key comes from the tag (`git show "$TAG:src/qsv-zipsign-public.key"`),
  not the branch: that is the key clients of that release embed via include_bytes!,
  so a rotation surfaces as a failure instead of false confidence
- the private key exists for exactly three steps — fetch, sign, shred. It is
  materialized after checkout, after the third-party downloader action, and after all
  archive manipulation, and destroyed before any archive content executes.
  persist-credentials: false keeps a git credential out of the same window
- the signature is re-verified after the smoke test, immediately before upload, so
  the bytes published are the bytes attested
- the tag must resolve to MAJOR.MINOR.PATCH, exist, and still be a PRERELEASE.
  Replacing an asset changes its sha256 and a stable release is likelier to have been
  pinned. Fails closed on an empty or unexpected isPrerelease value
- dry_run defaults to true; upload is the only gated step
- finding no portable binaries is a hard error, so an already-stripped archive is
  never re-signed and re-uploaded unchanged
- every keeper must survive and every victim must be gone before signing
- zipsign signs bytes, not zip structure, so a corrupted central directory could
  still verify: the rewritten qsv is extracted and run
- a constant concurrency group: it is evaluated from the raw input before the tag is
  resolved, so keying it on release_tag would let an auto-detect run and an explicit
  run for the same tag race anyway

Validated end-to-end locally with a throwaway zipsign keypair against a 7z-created
archive: an authentic asset completes the full flow, while a tampered asset and an
unsigned asset are both refused at the trust anchor, before any unsign. Also covers
the flat, nested and already-stripped layouts, and the prerelease guard against
prerelease/stable/missing/empty outcomes.

Note zipsign salts signatures with --context, defaulting to the input filename, so
the asset name must stay byte-identical throughout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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