Skip to content

Generate every package manifest at release time - #11

Merged
muimsd merged 2 commits into
mainfrom
fix/packaging-manifests
Sep 12, 2026
Merged

muimsd merged 2 commits into
mainfrom
fix/packaging-manifests

Conversation

@muimsd

@muimsd muimsd commented Sep 12, 2026

Copy link
Copy Markdown
Owner

The problem

The release workflow maintained two of six package manifests. The rest had rotted:

Manifest Before
Formula/tilefeed.rb generated ✓
bucket/tilefeed.json generated ✓
packaging/aur/PKGBUILD pkgver=0.1.0, sha256sums=('SKIP')
packaging/winget/muimsd.tilefeed.yaml PackageVersion: 0.1.0, URL pointing at v0.1.0, InstallerSha256: __CHECKSUM__
packaging/chocolatey/tools/chocolateyinstall.ps1 checksum64 = '__CHECKSUM__'
packaging/chocolatey/tilefeed.nuspec bumped by hand during the v0.8.1 release

Those three could not be published as they stood. Having to hand-edit the nuspec while cutting v0.8.1 was the symptom.

The fix

scripts/update-manifests.sh <version> downloads that version's release assets, checksums them, and rewrites all six. The workflow's three inline steps collapse to one call, and the script can be run by hand afterwards to repair a manifest without cutting a release.

Two bugs that fell out of moving generation into a script

The published Homebrew formula's test block is broken. The workflow emitted shell_output("\#{bin}/tilefeed --help") — the backslash was there to survive the bash heredoc, but Ruby reads \#{ as escaped interpolation:

$ ruby -e 'bin = "/opt/homebrew/bin"; puts "\#{bin}/tilefeed --help"'
#{bin}/tilefeed --help          # ← brew test runs this literally, and fails

The published Scoop manifest's indentation is mangled, because sed 's/^ //' strips exactly ten spaces from every line regardless of its depth. Valid JSON, but visibly wrong.

Both are artifacts of writing file content in YAML heredocs; neither can recur in a real script file.

Downloads now use curl -f and check each archive's type. Previously a missing asset wrote GitHub's HTML error page to the file and the job published the checksum of that page as if it were the binary.

Verification

  • Ran the script against the real published v0.8.1. Formula/ and bucket/ came out identical to what the workflow generated except the two bugs above, which is the equivalence check I wanted.
  • The packaging files in this PR are the script's own output for v0.8.1, so they now match the release that is already live.
  • Fails cleanly with exit 1 on an unpublished version, and writes nothing: every download happens before the first file is touched.
  • Both workflow files still parse as YAML; 300 tests pass.
  • Added a shellcheck scripts/*.sh CI job — this script runs on the release path, where a mistake is only visible once a release is already published.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UDAsxXjaouazD2L4NhDxSb

Only the Homebrew formula and Scoop manifest were maintained by the release
workflow. The AUR PKGBUILD and winget manifest were frozen at 0.1.0, and winget
and Chocolatey both carried a literal `__CHECKSUM__`, so those three packages
could not be published from this repo at all. Bumping the Chocolatey nuspec by
hand during the v0.8.1 release was the symptom.

`scripts/update-manifests.sh <version>` now downloads that version's assets,
checksums them, and rewrites all six manifests. The release workflow calls it,
and it can be run by hand afterwards to repair one.

Moving the generation out of the workflow YAML fixed two bugs that the embedded
heredocs had caused:

- The formula's test block was emitted as `shell_output("\#{bin}/tilefeed --help")`.
  The backslash was there to survive bash, but Ruby reads `\#{` as escaped
  interpolation, so `brew test tilefeed` ran a command literally named
  `#{bin}/tilefeed` and failed. Verified against the published v0.8.1 formula.
- The Scoop manifest's indentation was mangled by `sed 's/^          //'`
  stripping ten spaces from every line regardless of depth.

Downloads now use `curl -f` and check each archive's type: previously a missing
asset would write GitHub's HTML error page to the file and publish the checksum
of that page as if it were the binary.

The packaging manifests in this commit are the script's own output for v0.8.1,
so they now match the release that is already published.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDAsxXjaouazD2L4NhDxSb
@muimsd
muimsd force-pushed the fix/packaging-manifests branch from 4bfa0be to be840ac Compare September 12, 2026 06:28
The review's first finding was the important one: the script meant to stop
manifests rotting could rot the same way. A `perl -pi` substitution that matched
nothing rewrote the file unchanged, and the script still exited 0 reporting the
manifest as updated — resetting the rot once rather than preventing it. Every
targeted edit now fails loudly, naming the file and the pattern, when it matches
nothing. Verified by renaming a field and watching it exit 1; a whitespace
reformat is tolerated, and re-running for the same version stays a no-op success.

Also fixed:

- The .zip type guard always passed. `file x.zip` prints the filename before the
  type, so grepping its output for "zip" matched the name — an HTML error page
  served as a .zip sailed through. Now `file -b` with a case match, which also
  removes a `pipefail` + `grep -q` SIGPIPE flake on the release path.
- `brew install` on Linux arm64 installed an x86_64 binary: the formula had no
  on_arm branch under on_linux, though the aarch64 Linux asset has been built and
  published all along. Both Linux architectures are now referenced.
- A prerelease tag would have overwritten the stable manifests with RC URLs, and
  written a pkgver containing a hyphen, which makepkg rejects. The job is skipped
  for such tags and the script refuses anything but MAJOR.MINOR.PATCH.
- Tag names permit backticks and $(...), and the job holds contents:write and the
  tap token, so the version reaches the shell through env rather than `${{ }}`.
- `git diff --cached --quiet || git commit` hid the case where a release changes
  no manifest at all. That is now an error in this repo; it stays tolerated for
  the tap, where a no-op is expected.
- Downloads retry: assets are CDN-served and this job starts the moment the
  release is published, so a blip failed the job with the release already public.
- The winget manifest is generated whole rather than patched. Patching installer
  entries in place would rewrite an arm64 block's URL and hash to the x64 ones
  the day someone adds it.
- REPO comes from $GITHUB_REPOSITORY, so a fork checksums its own assets.
- The source tarball goes through fetch() too, so it gets the same type check.
- KEEP_DOWNLOADS prints where it kept them.
- shellcheck is installed explicitly rather than relying on the runner image.

Kept the per-file `replace` calls separate rather than merging them into one
perl invocation: with each one verified, a format change names the exact line
that stopped matching.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDAsxXjaouazD2L4NhDxSb
@muimsd
muimsd merged commit ca6a20d into main Sep 12, 2026
15 checks passed
@muimsd
muimsd deleted the fix/packaging-manifests branch September 12, 2026 06:46
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