Releases in the Focalet repository

This policy covers Focalet Desktop and Focalet Capture. A release builds both products from one version and exact source SHA. Each has its own Windows, Ubuntu and architecture-specific Mac installer. Capture builds independently of Flutter and the agent broker. See product boundaries.

Publish installers in timctho/focalet Releases, alongside the source. Release tags point to the exact accepted source revision. Releases inherit repository visibility; publishing does not change it. Downloads from a private repository require GitHub access. Actions artifacts expire and are not the release distribution channel.

Each release lists only the platforms actually accepted for that revision, with checksums and a manifest recording the source SHA. Windows previews can contain only the per-user Windows installer. Mac disk images are included when built and accepted on that same revision. Raw desktop acceptance evidence stays private and is not uploaded by the publisher.

When builds run

Trigger Workflow / machine Result
Every PR, including forks; every push to main; manual dispatch PR checks, GitHub-hosted ubuntu-24.04, windows-2025, macos-15, macos-15-intel Tests and a native release-mode package on each platform and Mac architecture, plus Windows installer compilation; no installer upload or published Release
A release version change pushed to main Release workflow, GitHub-hosted Windows, Ubuntu and both Mac architectures Builds, checks and automatically publishes all eight installers
Manual dispatch of Build and publish release Same release workflow, GitHub-hosted runners for the selected platforms Builds installers and optionally publishes after every selected build passes
Manual dispatch of Native acceptance Native acceptance; Ubuntu/Windows on opted-in self-hosted runners, Mac on GitHub-hosted runners Native acceptance and optional package artifacts; Mac can create an installer in a draft release
Maintainer runs publish_release.py --publish with accepted installer metadata Maintainer's build/publication environment Publishes the verified installers, checksums and source manifest to GitHub Releases

PR checks have no path filters: a documentation-only PR also runs those checks. Automatic releases watch src/Focalet.Flutter/pubspec.yaml on main and compare its release version before and after the whole push. An unchanged release version, a dependency/description edit or a build-number-only change skips installer builds and publication. Pushing a Git tag does not trigger a release.

Making the repository public preserves these triggers and runner choices. Standard GitHub-hosted runners are free for public repositories; larger runners are billed separately. Actions still has to be enabled, and fork contributions can require approval before their workflow runs. Public visibility does not move self-hosted jobs to GitHub's machines or change release triggers.

Run a release from GitHub Actions

Change the app version in a PR, review the changes and merge it into main. That version change starts the release automatically; no extra button or manual Git tag is needed. The committed version in src/Focalet.Flutter/pubspec.yaml is the source:

version: 0.1.0-preview.8+1

This produces tag v0.1.0-preview.8 and a prerelease. A version such as 0.1.0+2 produces the stable tag v0.1.0 and marks the release as latest. The +N is Flutter's build number; it is not part of the Git tag. The workflow creates the tag at the exact built commit when publishing. Before a subsequent release, bump the version in a PR (for example to 0.1.0-preview.9+2); changing only +N does not create a new release version. Published tags are never overwritten.

GitHub sorts releases from the same day by their numeric version, then compares prerelease suffixes alphabetically. That puts preview.9 above preview.10. Before crossing that boundary, increase the patch version and restart the preview counter: for example, use 0.1.1-preview.1+5 after 0.1.0-preview.11+4. The publisher rejects tags that could sort below an existing version. Keep Git tags unchanged. Remove obsolete GitHub releases and their assets only when explicitly requested, after replacement downloads are published and verified.

Automatic releases include both products (--include-capture) and use the all platform set: Windows x64, Ubuntu x64, macOS Apple Silicon and macOS Intel. The workflow builds the installers once, runs the release checks below and publishes those same files only if every selected platform succeeds. A failed or cancelled check prevents publication. Adding this workflow without a version change does not retroactively publish the version already in the file.

Manual builds and retries

The manual entry remains available for build-only checks, other platform sets and recovery. Open Actions → Build and publish release → Run workflow, select main and choose:

Input Meaning
platforms all (default), windows-ubuntu, windows, ubuntu, macos (both Mac architectures), macos-arm64, or macos-x64
publish Off: retain installers as Actions artifacts. On: publish them to this repository's Releases after all selected builds pass

For example, build all eight installers without publishing:

gh workflow run release.yml --ref main \
  -f platforms=all -f publish=false

Set publish=true to build and publish in one run. For an infrastructure failure, rerun the original failed workflow to keep its exact source SHA. If a code fix is needed, merge it and manually build/publish the corrected main, or bump the release version in that PR. An already published version always needs a new tag. A new manual dispatch rebuilds from the selected main; it does not promote artifacts from an earlier build-only run.

All jobs use the triggering push or dispatch's exact source SHA even if main advances. Runs for different commits do not cancel each other; publication for the same tag is serialized. The workflow requires main, pins its actions, gives build jobs read-only access, and grants contents: write only to the final publication job. It needs no personal access token or self-hosted runner; GITHUB_TOKEN publishes into this repository.

Ubuntu produces Focalet-Ubuntu-amd64.deb, Windows produces Focalet-Setup-x64.exe, and Mac produces architecture-specific DMGs. Capture assets use the Focalet-Capture- prefix with the same platform and architecture suffixes. Ubuntu builds require 24.04 and validate bundled ELF runtime requirements against the package compatibility baseline before creating an installer. Installers are unsigned by this workflow; Mac apps use ad-hoc signing and DMGs are not notarized. Ubuntu runs an installed-package GNOME Wayland check in a private virtual desktop and removes the package; Windows runs packaged non-visual capture checks; Mac verifies selection geometry and the mounted DMG. Ubuntu's automated checks use real GNOME, portals and synthetic apps; physical display and tray acceptance also needs the GNOME desktop checks. These checks do not replace real desktop acceptance of tray menus, OS permissions, or agent sign-in.

Each selected platform must succeed. Missing platforms, mixed source revisions, incorrect Ubuntu package versions and changed installer hashes stop publication. The publisher checks uploaded digests before making the draft public, and refuses to overwrite a published release. Build-only runs retain artifacts for seven days and create no release or tag. Actions must be available for the account; jobs blocked by billing or runner availability have not built anything.

Build and accept one revision

Run the checks required by CONTRIBUTING.md for the revision and inspect hosted PR checks. Record unavailable CI separately from local results; never describe jobs that did not start as passed. Keep persistent runners reserved for operator-invoked native acceptance. Build Windows locally with scripts/package-windows.ps1 -Runtime win-x64, then run the packaged Windows acceptance scripts. The package bundles MSVC and .NET runtime libraries. All platforms include the Apache 2.0 license and retained third-party notices. On macOS these are copied inside the app before code signing, so they survive installation from the DMG.

With NSIS 3 installed, create the Windows installer:

python scripts/build_installer.py artifacts/focalet-windows-x64 \
  --expected-commit <main-sha> --output artifacts/installers

NSIS can also wrap the Windows package on Linux (--makensis /path/to/makensis). The installed payload must then be verified on Windows: install, compare every packaged file, launch and capture from the installed location, uninstall, and confirm that unrelated files and user settings remain.

python scripts/accept_windows_installer.py artifacts/installers/Focalet-Setup-x64.exe `
  --expected-commit <main-sha> --output artifacts/installer-acceptance

This requires an interactive Windows desktop and PowerShell 7, and refuses to overwrite an already registered Focalet installation. Existing portable Focalet processes must be closed before the installer runs. The capture acceptance helper suspends and restores conflicting app processes during its own checks.

For each Mac architecture, dispatch the manual Native acceptance workflow on the same main revision:

gh workflow run ci.yml --ref main -f macos_only=true -f macos_arch=arm64 \
  -f macos_draft_release=true -f run_shared_runners=false
gh workflow run ci.yml --ref main -f macos_only=true -f macos_arch=x64 \
  -f macos_draft_release=true -f run_shared_runners=false

Architecture-specific concurrency allows both Mac builds to complete. CI builds the app, checks interactive capture, creates the DMG, mounts it, compares the app payload with the accepted package, and verifies its code signature. The private draft releases macos-test-<sha12>-<architecture> retain the DMGs and their .release.json metadata without consuming Actions artifact storage.

On a local Mac, build the native architecture and create the same verified DMG:

bash scripts/package-unix.sh macos
python3 scripts/build_installer.py artifacts/focalet-macos-arm64 \
  --expected-commit "$(git rev-parse HEAD)" --output artifacts/installers

Use focalet-macos-x64 on Intel. The installer builder smoke-tests the bundled helpers, verifies the DMG, mounts it, compares the app payload and checks its signature. Run the Mac acceptance checks for capture and permissions as well. These local builds remain available when hosted Actions cannot start.

Prepare and publish

Collect the accepted installer and its .release.json sidecar. With an authenticated local gh session, prepare a Windows preview:

python scripts/publish_release.py --repository timctho/focalet \
  --tag "$(python scripts/release_version.py)" --expected-commit <main-sha> \
  --windows-only \
  --metadata installers/Focalet-Setup-x64.exe.release.json \
  --output artifacts/release

This prepares the manifest, checksum list and release notes without changing GitHub. Add --publish to verify that the source commit exists in the destination, upload to a draft, check GitHub's asset digests and publish the preview. The tag must match the installer version and source revision. Published tags cannot be overwritten; choose a new version for each release. Existing draft assets must belong to the same release asset set.

For a release with Windows and Mac, omit --windows-only and add the accepted Mac Apple Silicon .release.json metadata; Mac Intel is optional. For a Mac-only preview, use --platforms macos with both DMGs, or explicitly select --platforms macos-arm64 or --platforms macos-x64 with the corresponding .release.json. Every selected installer must be present; choosing macos still requires both architectures. Automatic version releases require all eight installers.

After publication, download the actual installer and checksum files from timctho/focalet and compare them with the accepted local bytes. Use authenticated downloads for a private repository; also verify anonymous downloads if it is public. Keep the repository's visibility unchanged.

The README download table lists each platform and Mac architecture separately. Update a row's versioned installer link only after that asset is published and its download has been verified. Until then, label the planned installer and link to the /releases page; do not advertise an unavailable download. Different platforms can link to different accepted releases. Keep the installation guide and the table's all-releases link on /releases, which includes previews. GitHub's /releases/latest excludes prereleases and returns 404 while only previews exist.

Signing

The current installer builder produces an unsigned Windows setup executable and a non-notarized Mac DMG. Operating systems may require explicit opening confirmation; document that in preview release notes.

The code signing policy records provider readiness and release approval requirements. SignPath approval and integration are still pending.

For a normal distribution release, sign the Windows application and installer with Authenticode, and sign the Mac app with an Apple Developer ID, notarize and staple the distribution. Recompute installer hashes after signing. The existing package builders accept FOCALET_WINDOWS_SIGNING_THUMBPRINT and FOCALET_MACOS_SIGNING_IDENTITY; signing only the app does not sign the installer or notarize the Mac distribution.

Local publication needs no additional Actions secret. A workflow publishing in this source repository can use its GITHUB_TOKEN with contents: write; no separate downloads repository or cross-repository token is needed.