Every .pkg craft has ever produced can install itself somewhere other than where it says, and report success.
createPKG let pkgbuild infer its component. pkgbuild's default for a bundle is BundleIsRelocatable = true, so the package carries:
<relocate><bundle id="dev.example.app"/></relocate>That directive tells installer the payload path is only a suggestion. It looks the bundle identifier up on the target volume and, if the system has a copy of that bundle registered anywhere, writes the payload over that copy instead — and exits 0. The user gets "The install was successful" and finds nothing at /Applications.
I had this wrong, twice
I told you this was Intel-specific. It isn't — darwin-arm64 failed with a byte-identical ENOENT in an adjacent run, on the same code and the same runner image.
And the relocation directive alone isn't the cause either — it's present in packages from green runs too. It's the enabler; the trigger is a registered copy of the same bundle id existing when the install happens. That's why it's intermittent, and what actually separates green from red is how long the job had already been running:
| lifecycle step began | outcome |
|---|---|
| ~22 minutes into the job | all 3 failing macOS legs |
| 20–124 seconds in | all 8 passing macOS legs |
Which is what a race against the system's bundle indexer looks like. The lifecycle script leaves two copies of the fixture bundle in its work directory — those are the relocation target.
The fix
Write the component plist instead of inferring it, with BundleIsRelocatable off. Verified on a real fixture package, not just in the unit test:
relocate: <relocate/>
install root mode: drwxr-xr-x "."
payload exe count: 1Empty <relocate/> — nothing for installer to redirect to — with the payload unchanged.
Note the pkg-info attribute relocatable="false" appears either way and does not govern this; only the <relocate> element does. I built both variants side by side to confirm that, because that attribute is exactly the thing that makes the current package look fine on inspection.
BundleIsVersionChecked is off too: with it on, installing an older version over a newer one is skipped — silently turning the rollback step, and a user's rollback, into a no-op.
Two things found alongside
- The package asked for
/to be0700. The staging dir came frommkdtemp(mode 0700), andpkgbuildrecords the--rootdirectory's own mode as the mode of.— which under--install-location /is the target volume's root. Measured:drwx------ ".". Now staged in a0755child, measureddrwxr-xr-x. - The failure left no evidence.
installerreporting success while writing elsewhere surfaced only as a bareENOENTfrom the launch step, which cannot distinguish "nothing installed" from "installed elsewhere". The receipt is now recorded after each install, intoreport.jsonand the uploaded evidence —location:is where the payload actually landed. The workflow uploads the whole evidence directory too; the compiled fixtures sat outside the old upload path, which is why this needed a workflow dispatch to diagnose rather than an artifact download.
Verification
bun test— 471 pass, including 3 new ones; I confirmed they discriminate by flippingBundleIsRelocatableback totrueand watching 18 pass / 1 failtsc --noEmitclean,pickierclean- real fixture package rebuilt and inspected with
pkgutil --expand/lsbom/pkgutil --payload-files
This is a user-facing bug, not a CI one. The lifecycle workflow is just the thing that noticed.