Packaging & release

heap. ships from a single GitHub Actions workflow, .github/workflows/release.yml. It builds native artifacts for Windows, Linux and macOS and attaches them to a GitHub Release.

Triggering a release

The version lives in one place — project(heap VERSION X.Y.Z …) in CMakeLists.txt. The release tag mirrors it, and the workflow refuses to publish if the two disagree.

  • Cut a release (primary path): bump CMakeLists.txt to the new version, land it on master, then push the matching semver tag:

    git tag v1.2.3
    git push origin v1.2.3

    The tag name is used verbatim for the release and artifact names. A guard in the meta job fails the run if the tag’s numeric core (1.2.3) does not equal the CMake version. The release is marked latest, so the in-app updater picks it up.

  • Pre-release: push a hyphenated tag such as v1.2.3-rc.1 (its core must still match CMake). It is published but flagged pre-release and kept off /releases/latest, so the auto-updater never offers it.

  • Manual test build: run the Release workflow from the Actions tab (workflow_dispatch). It builds a vX.Y.Z-dev.<short-sha> bundle and attaches the artifacts to the workflow run — nothing is tagged or published. Use it to smoke-test packaging without cutting a release.

Artifacts

PlatformFormatHow it is built
Windows…-windows-portable.zipCMake portable target (windeployqt bundle)
Windows…-windows-setup.exeInno Setup (installer/heap.iss) wrapping the portable bundle
Linux…-linux-x86_64.AppImagelinuxdeploy + the Qt plugin over a Qt 6.9.1 build (install-qt-action), smoke-tested in a clean ubuntu:24.04 (packaging/linux/smoke-appimage.sh)
macOS…-macos.dmgmacdeployqt → hdiutil drag-to-Applications dmg, ad-hoc codesigned (Developer ID + notarized when signing secrets are set)

Every asset is listed in SHA256SUMS and, in the same form, in a collapsed “SHA-256 checksums” block of the release notes; each also has a build-provenance attestation (gh attestation verify <file> --repo sectapunterx/heap).

The Windows bundle carries only the Qt Quick Controls style heap uses (Basic; main.cpp forces it). packaging/windows/prune-bundle.sh drops the other styles and the unused Particles / LocalStorage (Qt6Sql) modules before copy-deps.sh walks and verifies the DLL closure, and fails the build if qml/ ever starts importing one of them.

macOS signing & notarization

By default the .app inside the .dmg is ad-hoc codesigned (no paid Apple Developer ID required). This gives it a valid signature — without it, the install_name_tool rpath rewrites macdeployqt performs on the arm64 binary and Qt frameworks leave broken signatures, and a downloaded (quarantined) copy fails to launch with the fatal “heap is damaged and can’t be opened”.

Because the ad-hoc build is not notarized, first launch still shows the bypassable “unidentified developer” prompt. Open it either way:

  • Right-click → Open (then Open again in the dialog), or
  • strip the download quarantine flag:
    xattr -dr com.apple.quarantine /Applications/heap.app

To codesign with a Developer ID Application certificate, notarize and staple the ticket automatically — which removes the prompt entirely — add these repo secrets. The workflow’s optional step activates when MACOS_CERT_P12 is present (it also signs with a hardened runtime + the allow-jit entitlement in packaging/macos/heap.entitlements that Qt’s JS engine needs):

SecretMeaning
MACOS_CERT_P12base64 of a Developer ID Application .p12 certificate
MACOS_CERT_PASSWORDpassword for that .p12
MACOS_NOTARY_APPLE_IDApple ID used for notarization
MACOS_NOTARY_PASSWORDapp-specific password for that Apple ID
MACOS_NOTARY_TEAM_IDApple Developer Team ID

Windows signing

Windows binaries and the installer are unsigned by default, so SmartScreen shows an “Unknown publisher” warning on first download/run. Signing is wired to SignPath Foundation — free Authenticode code signing for qualifying open-source projects — and activates when SIGNPATH_API_TOKEN is present (mirrors the macOS optional-signing pattern). Unsigned builds still ship when it is absent.

What signing does — and does not — do. As of 2024 no certificate removes the SmartScreen warning instantly (Microsoft Learn; EV lost its instant bypass). Signing (1) replaces “Unknown publisher” with the verified publisher “SignPath Foundation” (the certificate is issued to the Foundation, not to heap), and (2) lets SmartScreen reputation accumulate on that certificate so the warning fades over downloads. The Foundation cert is shared by many OSS projects, so it already carries some reputation — a head start over a fresh cert. The only way to a zero-warning first launch is Microsoft Store (MSIX) distribution, which is not used here.

Post-2023 CA rules forbid downloadable .pfx keys for publicly-trusted certificates (keys must live on an HSM/token or a managed service), which is why signing goes through SignPath’s cloud service rather than a .pfx secret. The flow: CI uploads the unsigned artifact as a GitHub workflow artifact, signpath/github-action-submit-signing-request submits a signing request, SignPath verifies the build origin, signs the configured PE files (with an RFC-3161 timestamp, so signatures outlive the cert), and returns the signed files. heap.exe + the bundled Qt DLLs are signed before packaging, then the setup .exe is signed after Inno builds it.

Configure it after the SignPath Foundation application is approved:

Repo settingKindMeaning
SIGNPATH_API_TOKENsecretSignPath API token with submitter permission
SIGNPATH_ORG_IDvariableSignPath organization ID
SIGNPATH_PROJECT_SLUGvariableSignPath project slug (e.g. heap)
SIGNPATH_POLICY_SLUGvariablesigning policy slug (e.g. release-signing)

In the SignPath console the project needs the GitHub.com trusted build system (with the SignPath GitHub App installed on the repo) and two artifact configurations: portable (recursively signs *.exe/*.dll in the bundle) and installer (signs the setup .exe). Their XML lives in packaging/windows/signpath/ — paste each into the console under the slug of the same name.

Status (0.5.3): not active. The repository has none of the settings above, so every release so far shipped unsigned (Get-AuthenticodeSignature → NotSigned) and the release notes say so. What the owner has to do, once:

  1. Apply to the SignPath Foundation OSS programme (signpath.org → Apply) with the repo URL, MIT licence and package-windows as the build definition, and wait for approval. MFA must be on for the GitHub and SignPath accounts.
  2. In SignPath: create the project (slug e.g. heap), a signing policy (e.g. release-signing), link the GitHub.com trusted build system and install the SignPath GitHub App on sectapunterx/heap.
  3. Add the two artifact configurations from packaging/windows/signpath/.
  4. Create an API token for a user with submitter rights on that policy.
  5. In GitHub → Settings → Secrets and variables → Actions: secret SIGNPATH_API_TOKEN; variables SIGNPATH_ORG_ID, SIGNPATH_PROJECT_SLUG, SIGNPATH_POLICY_SLUG.
  6. Run the Release workflow manually (workflow_dispatch) once and check the Verify the portable signatures / Verify the installer signature steps.

The workflow checks this configuration before it builds: no token means “unsigned, with a warning on a stable release and a note in the release notes”; a token with a missing variable fails the job, and a signing request that hands back unsigned files fails the verify steps rather than shipping. SignPath’s OSS program requires every job up to the signing request to run on GitHub-hosted runners — package-windows already does. The uninstaller (unins000.exe, which Inno generates on the target machine) is not signed.

Follow-ups (not yet automated)

These formats from the original scope are intentionally deferred — each needs extra tooling/infra that is easiest to add once the core three platforms are proven:

  • Flatpak — a org.heap.heap.yaml manifest built via flatpak-builder and pushed to a Flathub repo.
  • .deb / .rpm — only with Qt bundled or against a distribution whose Qt is 6.9+: until 0.5.1 a .deb built on Ubuntu 24.04’s Qt 6.4 shipped, and the UI does not run there.