Skip to content

ci(canary): clear bundle output so canaries can't ship a stale installer - #5429

Open
Glucksberg wants to merge 1 commit into
block:mainfrom
Glucksberg:ci/canary-stale-installer
Open

ci(canary): clear bundle output so canaries can't ship a stale installer#5429
Glucksberg wants to merge 1 commit into
block:mainfrom
Glucksberg:ci/canary-stale-installer

Conversation

@Glucksberg

Copy link
Copy Markdown

Summary

All four canary workflows can upload an installer that was not built by that run.

Each one restores desktop/src-tauri/target from an actions/cache entry and lists !desktop/src-tauri/target/**/release/bundle alongside it, with a comment stating that this keeps installers out of the cache:

# Only this trusted, main-bound canary writes the cache. Excluding bundle
# output prevents installers from entering it.

That exclusion does nothing. actions/cache resolves the path: list through @actions/glob into a set of paths and hands those to tar. A ! pattern removes entries from that resolved set — but desktop/src-tauri/target/**/release/bundle was never a separate entry, only its ancestor desktop/src-tauri/target was. Removing something that isn't in the list changes nothing, and tar still recurses into the directory. Bundle output goes into the cache, and a cache hit restores the previous run's installer into a tree where the new build is about to land.

Then every locate step picks its artifact like this:

EXE=$(find "$BUNDLE_DIR/nsis" -name '*.exe' -type f | head -1)

With two installers present, head -1 is a coin flip, and the losing side is a canary artifact containing last run's code.

Evidence

This is not theoretical — I hit it on a fork while field-testing a Windows fix:

  • Run 1 built canary fork.1.
  • Run 2 built fork.2 on top of new commits, got a cache hit, and uploaded run 1's fork.1 installer. I installed it, saw the old behaviour, and spent a cycle re-diagnosing a bug that was already fixed.

A maintainer can confirm the cache side directly: on the next canary run that reports cache-hit: true, ls -R desktop/src-tauri/target/*/release/bundle immediately after the restore step and before the build — the previous run's artifacts are there.

The Linux canary shows the same fault as a hard failure rather than a silent swap. Its AppImage post-process step counts what it finds:

if [[ ${#APPIMAGES[@]} -gt 1 ]]; then
  echo "::error::Expected exactly one AppImage, found ${#APPIMAGES[@]}: ${APPIMAGES[*]}"

A restored AppImage plus a freshly built one is exactly two.

The signed macOS canary is the worst case: a stale DMG picked by head -1 is then signed and notarized by the rest of that job, so a correctly signed, correctly notarized build ships the wrong code.

The change

One step per workflow, immediately before pnpm tauri build:

- name: Remove stale bundle output
  run: rm -rf desktop/src-tauri/target/release/bundle

(target/${TARGET}/release/bundle in the two cross-compiled workflows.) It removes only bundler output — no compiled crates, no incremental artifacts — so cache effectiveness is unchanged.

The four misleading comments above the cache-save steps are corrected to say what actually happens.

Workflow Bundle dir
windows-canary.yml target/${TARGET}/release/bundle
macos-intel-canary.yml target/${TARGET}/release/bundle
linux-canary.yml target/release/bundle
signed-macos-canary.yml target/release/bundle

Deliberately left alone

Bundle output still enters the cache on save — this PR stops the stale artifact from being used, not from being stored. Making the storage side match the original intent needs the bundle directory cleared before actions/cache/save, and the step order differs across these files (Linux and signed macOS save the cache before their locate/upload steps, Windows saves after), so there is no uniform placement. Happy to follow up if you want the dead ! patterns removed and the save side actually enforced — I kept this PR to the defect that ships wrong binaries.

I also considered tightening each find to match the run's derived version string. windows-canary.yml and signed-macos-canary.yml expose steps.version.outputs.version, but macos-intel-canary.yml derives its version inside a run: block with no output, so it would have been inconsistent — and redundant once the directory is empty.

Testing

  • YAML parses for all four files; no change to any ${{ }} expression, so nothing new for zizmor to flag.
  • These workflows are workflow_dispatch-only and gated on github.repository == 'block/buzz', so I can't exercise them from a fork against this repo. The equivalent rm -rf has been running in my fork's Windows canary since the stale-installer incident: subsequent runs shipped the right build, with no change in Cargo cache hit rate or build time.

Searched open PRs: none touch .github/workflows/*canary*.

The four canary workflows restore `desktop/src-tauri/target` from an
actions/cache entry and rely on `!desktop/src-tauri/target/**/release/bundle`
in the path list to keep bundle output out of it. That exclusion is a no-op:
negating a subpath of an already-included directory does not stop tar from
recursing into it, so installers do enter the cache and a cache hit restores
the previous run's artifact next to the new one.

Every locate step then picks a file with `find … | head -1`, which can return
the restored artifact instead of the one just built. On a fork running the
Windows canary this uploaded run 1's installer as run 2's canary. On Linux the
same restore trips the AppImage post-process step's "expected exactly one
AppImage" check and fails the build.

Remove the bundle directory before each `tauri build`, and correct the cache
comments that claim the exclusion works.

Signed-off-by: Glucksberg <glucksberg89@gmail.com>
@Glucksberg
Glucksberg requested a review from a team as a code owner August 9, 2026 21:12
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