Skip to content

Make extract_dir part of the Scoop publishing contract - #48

Merged
abdushakoor12 merged 1 commit into
mainfrom
fix/scoop-extract-dir-contract
Aug 2, 2026
Merged

Make extract_dir part of the Scoop publishing contract#48
abdushakoor12 merged 1 commit into
mainfrom
fix/scoop-extract-dir-contract

Conversation

@abdushakoor12

Copy link
Copy Markdown
Collaborator

TLDR

Both Scoop manifest writers can now declare extract_dir, and the compliance reference states the archive-root contract that ties a release archive's layout to the manifest describing it.

Details

Scoop resolves bin from the app root. If a release archive nests its payload under a top-level directory, the manifest has to name that directory in extract_dir — otherwise the download and SHA-256 verification both succeed and the install then fails at shim creation (Can't shim 'tool.exe': File doesn't exist). The partial success hides the cause.

Neither writer could emit that field, and nothing in the compliance docs connected archive layout to the manifest. The templates were correct only by accident: they zip a bare .exe, so the archive is flat and root-relative bin happens to resolve. An adopter that stages a directory first — multiple binaries, or a bundled README/LICENSE, which is the normal shape for an LSP + MCP + CLI product — silently got a broken bucket manifest with no warning from the template or the checklist. That is exactly what happened downstream in Nimblesite/Deslop#313.

templates/gh-actions/publish-scoop-bucket.yml — new optional extract_dir input (default: ""), wired to the writer through EXTRACT_DIR.

.github/workflows/release.reusable.yml — new optional scoop_extract_dir input (default: ""), same wiring. This workflow packages a single .exe and so is flat today; the input exists so the manifest cannot silently drift out of sync if its packaging step ever stages a directory, and so both writers stay identical.

Both writers omit the key entirely when the value is empty, rather than emitting "extract_dir": "" — Scoop reads an empty string as a directory named "", not as "no nesting", so an unconditional field would break flat archives.

docs/agents/shipwright-compliance/reference/implement-release.md §7 — documents that extract_dir MUST match the archive's top-level directory, that Compress-Archive -Path "dist/$stage" nests while "dist/$stage/*" does not, and that Homebrew masks the mistake by auto-descending into a lone top-level directory (so a green tap proves nothing about the bucket). Also covers carrying extract_dir into autoupdate, and keeping every versioned segment of the autoupdate URL — including the release tag — a literal $version placeholder.

docs/agents/shipwright-compliance/reference/audit-checklist.md §8 — three new FAIL conditions covering the same three defects, so an audit catches them.

No breaking changes. Both inputs are optional and default to empty; existing callers produce byte-identical manifests.

How Do The Automated Tests Prove It Works?

scoop manifest writers declare extract_dir for nested archives in tests/fixtures.test.mjs, run by node --test under make test.

It does not match workflow text — text matching cannot distinguish a declared field from a commented-out one. runScoopManifestWriter lifts the node - <<'NODE' heredoc out of the workflow, writes it to a temp .cjs file (the heredoc is CommonJS; this package is ESM), executes it with the step's environment, and parses the manifest it writes to disk. So the assertions are made against the artifact the release job would actually publish.

It runs against both writers — the vendored template and the reusable workflow — and asserts both directions of the contract:

  • With EXTRACT_DIR=sampletool-1.2.3-win32-x64, the emitted architecture.64bit.extract_dir equals it exactly.
  • With EXTRACT_DIR="", the key is absent ("extract_dir" in … is false), covering the flat-archive case that an unconditional field would break.
  • url, hash and bin are asserted to survive unchanged, which is what proves the extracted writer genuinely executed rather than the test passing vacuously.

Against main it fails with actual: undefined, expected: 'sampletool-1.2.3-win32-x64', and the url/hash/bin assertions pass first — confirming the failure is the missing field, not a broken harness. With this change all four assertions pass for both writers.

Full local suite: node --test tests/fixtures.test.mjs 8/8, cargo clippy --release --all-targets --workspace -- -D warnings clean, and node tools/validate-manifest/index.mjs fixtures/manifests validates all five golden manifests.

Scoop resolves `bin` from the app root, so a release archive that nests its
payload under a top-level directory must declare that directory as
extract_dir. Neither Scoop manifest writer could emit it, and nothing in the
compliance reference tied archive layout to the manifest, so an adopter that
stages a directory first — multiple binaries, or a bundled README/LICENSE —
got a manifest that downloads and hash-verifies successfully and then fails at
shim creation. The templates were only correct by accident: they zip a bare
.exe, so root-relative bin happens to work.

Both writers now read EXTRACT_DIR and omit the key entirely when it is empty,
because Scoop treats "" as a directory named "" rather than as no nesting. The
template takes an optional extract_dir input and the reusable workflow a
scoop_extract_dir input, both defaulting to empty.

The compliance reference and audit checklist now state the archive-root
contract explicitly, including the autoupdate block and the frozen-tag failure
mode, since a manifest that declares extract_dir only under architecture
regresses on the next auto-update.

Nimblesite/Deslop#313
@abdushakoor12
abdushakoor12 merged commit b651501 into main Aug 2, 2026
4 checks passed
@abdushakoor12
abdushakoor12 deleted the fix/scoop-extract-dir-contract branch August 2, 2026 09:05
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