How a PostPile release goes out: a v* tag builds the app on GitHub Actions, attaches the zip to a GitHub release, and renders the Homebrew cask into PostHog/homebrew-tap. Installed apps then update themselves from that release (see Self-update).
Done once, on 2026-09-28, when the repo went public. Kept here so a new repo or a rotated key can be set up the same way.
- History. The private history was rewritten with
git filter-repo --replace-text(personal paths, private repo names, real people) and re-signed. All commits and their dates were kept. - Repo.
PostHog/postpile, public, default branchmain. The description says "Internal DevEx tool" so the repo does not read like a new PostHog product. - Homebrew tap token. The PostHog GitHub App behind
GH_APP_HOMEBREW_TAP_RELEASER_*already writes to the tap for phrocs and other PostHog tools. Its secrets are environment secrets per repo, not org secrets, so every repo that publishes to the tap needs its own copy:-
Repo settings › Environments:
homebrew-tap, with a deployment tag rulev*(no branches), so only release tags can use the token. -
In that environment,
GH_APP_HOMEBREW_TAP_RELEASER_APP_IDholds the app's numeric App ID (from the app's settings page), andGH_APP_HOMEBREW_TAP_RELEASER_PRIVATE_KEYholds the app's private key. The key is in 1Password asreleaser-homebrew-tap.<date>.private-key:op document get <item id> | gh secret set GH_APP_HOMEBREW_TAP_RELEASER_PRIVATE_KEY -R PostHog/postpile --env homebrew-tap gh secret set GH_APP_HOMEBREW_TAP_RELEASER_APP_ID -R PostHog/postpile --env homebrew-tap --body <app id> -
The app needs no change. The token is scoped to
PostHog/homebrew-tap.
-
- Signing environment. Repo settings › Environments:
desktop-signing, with a deployment tag rulev*(no branches). It holds the Apple secrets, see Signing and notarization. Until it has them, releases go out ad-hoc signed. - PostHog source maps.
POSTHOG_CLI_API_KEYin thedesktop-signingenvironment, see Error tracking source maps. Until it is set, releases go out with bundled, release-less error stacks. - Still open: a ruleset on
mainthat requires the CIcheckjob, and secret scanning plus Dependabot alerts under Security.
-
Pick the version. Every release counts the minor up:
0.2.0,0.3.0, … A quick fix on top of a release bumps the patch:0.2.1. No-alphasuffix (decided 2026-09-29, after0.1.0-alpha.0); the app is alpha in words only (README, release notes). A version without a-is not marked pre-release on GitHub, which is fine. -
Bump it everywhere. Every
package.json(root and workspaces) carries the same version; a test fails when they drift. For example:pnpm -r exec npm pkg set version=0.2.0 && npm pkg set version=0.2.0 -
Changelog. Rename
## <version> (unreleased)to## <version>inCHANGELOG.md. The release workflow takes this section as the release notes. -
Check locally:
pnpm typecheck,pnpm test,pnpm dist. Commit through a PR and merge tomain. -
Tag and push from the merged
main:git tag -a v0.2.0 -m "PostPile 0.2.0" git push origin v0.2.0If the Release workflow does not start (a tag ruleset bypass does not fire the push trigger), run it by hand: Actions › Release › Run workflow, with the tag. Pick the tag under "Use workflow from" as well: the
desktop-signingandhomebrew-tapenvironments only admitv*refs, and GitHub rejects a run frommainthere. -
What the workflow does (
.github/workflows/release.yml):buildonmacos-14(arm64), in thedesktop-signingenvironment: checks the tag equalsv+apps/desktop/package.jsonversion, installs with the frozen lockfile, typechecks, tests, builds the bundle, injects chunk ids and uploads the source maps to PostHog (whenPOSTHOG_CLI_API_KEYis there, see below), packages the app (Developer ID signed and notarized when the Apple secrets are there, else ad-hoc, see below), verifies the signature (codesign --verify --deep --strict), the bundle id (com.posthog.postpile), the version inInfo.plistandapp-update.ymlin the bundle, checkslatest-mac.ymlagainst the zip (signed builds), then creates the GitHub release withPostPile-<version>-mac-arm64.zipandPostPile-<version>-mac-arm64.zip.sha256, pluslatest-mac.ymlandPostPile-<version>-mac-arm64.zip.blockmapfor a signed build. The release is a draft until every file is up. Versions with a-become pre-releases.publish-homebrewon ubuntu, in thehomebrew-tapenvironment: rendershomebrew/postpile.rb.tmplwith the version, the sha256 and the right caveats (signed or ad-hoc), mints a tap token from the GitHub App, and commitsCasks/postpile.rbto PostHog/homebrew-tapmain.
-
Verify:
- The GitHub release has the zip and the
.sha256(and for a signed buildlatest-mac.ymland the.blockmap), with the changelog notes. It is marked pre-release only for a version with a-(like the old0.1.0-alpha.0). shasum -a 256 -c PostPile-<version>-mac-arm64.zip.sha256passes on the downloaded zip.- PostHog/homebrew-tap has a commit "chore: update postpile cask to " with the right version and sha256.
- The build log says which way the app was signed: a "building an ad-hoc signed, not notarized release" warning, or a green "Verify signing and notarization" step.
- The build log has a green "Upload source maps to PostHog" step, or the "POSTHOG_CLI_API_KEY is not set" warning. With the upload, the
postpilerelease at the new version and its uploaded symbol sets show up in PostHog Error Tracking (project PostPile). - On a Mac:
brew update && brew install --cask posthog/tap/postpile(orbrew upgrade --cask postpile), open the app (for an ad-hoc release, clear the quarantine flag first as the caveats say). About PostPile shows the version, the status bar shows it too. brew audit --cask --tap posthog/tap postpilehas no errors worth fixing in the template.
- The GitHub release has the zip and the
Why: an ad-hoc signed app has no stable identity. Gatekeeper blocks its first open (users need xattr or Open Anyway), and every build counts as a new app for macOS privacy permissions (TCC), so grants are forgotten and asked for again after each update. A Developer ID signature fixes both; notarization lets Gatekeeper open the app without asking.
What the workflow does, mirroring PostHog/posthog's desktop-release.yml for the PostHog desktop app:
- The
buildjob runs in thedesktop-signingenvironment. A "Check signing secrets" step looks at the certificate:- Not set: a
::warning::and an ad-hoc build with plainpnpm dist. The release still goes out, with the xattr caveat in the cask. - Set, but anything else missing: the job fails. A signed app that is not notarized would still be blocked by Gatekeeper.
- Not set: a
- Signed build:
pnpm build, then electron-builder withelectron-builder.ymlplus command-line overrides:mac.identity= the team ID (picks the "Developer ID Application: … ()" certificate),mac.hardenedRuntime=true,mac.notarize=trueandforceCodeSigning=true.electron-builder.ymlstays ad-hoc so localpnpm distworks without a certificate.- electron-builder imports the base64
.p12fromCSC_LINKinto a temporary keychain, so there is no keychain step. - Entitlements:
apps/desktop/build/entitlements.mac.plist(JIT, unsigned executable memory, no library validation; the same as PostHog's desktop app minus the microphone), for the app and its helpers. - Notarization: notarytool with the Apple ID and app-specific password, then the ticket is stapled to the app before the zip is made.
- electron-builder imports the base64
- "Verify signing and notarization" unzips the release zip and checks both that app and the one in
dist/:codesign --verify --deep --strict, a Developer ID authority, the hardened runtime flag,spctl --assess --type execute -vvandxcrun stapler validate. - The cask drops the ad-hoc caveat block (xattr / Open Anyway) for a signed build.
The desktop-signing environment (deployment tag rule v*) needs:
| Name | Kind | What |
|---|---|---|
APPLE_CODESIGN_CERT_BASE64 |
secret | Developer ID Application certificate with its key, .p12, base64 (passed as CSC_LINK) |
APPLE_CODESIGN_CERT_PASSWORD |
secret | Password of the .p12 (passed as CSC_KEY_PASSWORD) |
APPLE_APP_SPECIFIC_PASSWORD |
secret | App-specific password of the Apple ID used for notarization |
APPLE_ID |
variable or secret | That Apple ID |
APPLE_TEAM_ID |
variable or secret | PostHog's Apple team ID |
The workflow reads vars.X || secrets.X for the last two, so either kind works, and org secrets shared with the repo work too.
How to get access: PostHog already has these for its desktop app, as APPLE_* org secrets managed in PostHog's infrastructure code. Ask the infra team; both ways need approval from the desktop app owners and security:
- Share the five org secrets above with this repo. Simple, but org secrets shared this way are readable by any workflow in the repo, not gated by the environment and its tag rule.
- Or copy the values into the
desktop-signingenvironment of PostHog/postpile (gh secret set <name> -R PostHog/postpile --env desktop-signing). Onlyv*tag runs can read them then; the copies have to be updated by hand when the certificate or password rotates.
How installed apps get a release (DESIGN.md "Self-update"): electron-updater in the app reads latest-mac.yml from the newest published release on github.com, downloads the zip it names, checks the zip's sha512 against the file, and Squirrel.Mac installs it when the user clicks "Restart to update" or quits.
electron-builder.ymlhaspublish: github(PostHog/postpile). electron-builder writesContents/Resources/app-update.yml(where to look) and, next to the zip,latest-mac.ymland the.blockmap.--publish neverkeeps it from uploading anything; the workflow attaches the files.- Only signed builds attach
latest-mac.yml: Squirrel.Mac checks that the new app has the same Developer ID as the running one, so an ad-hoc release could never install. Without the file, installed apps show the brew command. - Don't edit a release's zip by hand after it is out:
latest-mac.ymlholds its sha512, and a changed zip fails every download. - To pull a bad release back from auto-update, mark it a draft or delete
latest-mac.ymlfrom it; apps then look at the release before it (or show the brew command), and never go back to an older version on their own. - To check a release by hand: on a Mac with the previous version installed from brew, PostPile › Check for Updates… says "Downloading PostPile ", and a minute later the pill says "Update ready". Restart, then About PostPile shows the new version. The log (
~/Library/Logs/PostPile/main.log) has electron-updater's lines.
Why: the app ships bundled JavaScript, so without source maps an error stack in PostHog Error Tracking points at main/chunks/engine-from-env-<hash>.js:35040 instead of the TypeScript source. The upload also creates the PostHog release for the version, so issues can be marked resolved in a release (DESIGN.md "Usage analytics" › Errors).
What the workflow does:
pnpm buildwrites hidden source maps (.mapfiles without asourceMappingURLcomment).electron-builder.ymlleaves*.mapout of the app, so they never ship, with or without the upload.- "Check PostHog CLI secret": no
POSTHOG_CLI_API_KEYmeans a::warning::and no upload. The release still goes out. - "Upload source maps to PostHog", for
apps/desktop/out/mainandapps/desktop/out/renderer:posthog-cli sourcemap inject(pinned@posthog/cli) prepends a snippet with the file's chunk id and the id of thepostpilerelease at this version (created on first use), thenposthog-cli sourcemap upload --delete-aftersends the maps and deletes them. Only after that is the app packaged, because the injected files are what must ship. A failed upload fails the job: rerun it, or remove the secret to release without source maps. - Project
635117(PostPile, US cloud) and the host are set in the workflow; neither is a secret.
One-time setup (the repo owner):
-
On us.posthog.com, Settings › Personal API keys: create a key, for example "postpile release source maps", limited to the PostPile project, with the scopes error tracking: write and organization: read (what
posthog-clineeds, per PostHog's upload docs). The key acts as the person who made it; rotate it when they leave. -
Put it into the
desktop-signingenvironment, so onlyv*tag runs can read it.ghasks for the value, so it never lands in the shell history:gh secret set POSTHOG_CLI_API_KEY -R PostHog/postpile --env desktop-signing -
The next release shows a green "Upload source maps to PostHog" step. Check the first issue after it in PostHog: its frames should show TypeScript file names and the release.
posthog-cli still accepts the old names POSTHOG_CLI_TOKEN and POSTHOG_CLI_ENV_ID; the workflow uses the current POSTHOG_CLI_API_KEY and POSTHOG_CLI_PROJECT_ID.
- Releases are Developer ID signed and notarized since 0.2.0 (the first one, 2026-09-29). Without the Apple secrets the workflow falls back to ad-hoc: Gatekeeper then blocks the first open until the quarantine flag is cleared or the user clicks Open Anyway, and macOS forgets privacy grants on every update. See Signing and notarization.
- The bundle id changed from
com.postpile.apptocom.posthog.postpilein 0.1.0-alpha.0. macOS asks for notification permission again on the first launch of the new id. Data in~/Library/Application Support/PostPileis unaffected. - Local builds never publish: the desktop
distscript passes--publish neverto electron-builder. - Self-update starts with 0.16.0. Older builds can't update themselves; their users update once with
brew upgrade --cask postpile.