Tagging a release, triggering the workflow manually, and the CI invariants you must not let drift.
The mirror setup that this depends on lives in mirror-sync.md.
Every release after initial setup is a single command.
Before tagging, ensure build.gradle.kts has the correct version:
version = "1.2.0" // must match the tag exactlyTag and push:
git tag v1.2.0
git push origin v1.2.0The CI workflow (.github/workflows/release.yml) then automatically:
- Runs all tests.
- Builds native binaries on Linux x64, macOS arm64, Windows x64 (Intel macOS dropped — see
release.ymlcomment and CI invariants below). - Runs smoke tests on the Linux binary only (macOS runners ship no Docker; Windows skips its smoke step too).
- Publishes all binaries +
.sha256files to the GitHub release for that tag. - Updates
Formula/localdevstack.rb(version, URLs, sha256 hashes) for Homebrew distribution. - Updates
bucket/localdevstack.json(version, URL, hash) for Scoop distribution. - Commits and pushes those updates so package managers pick up the new version.
After the workflow completes, brew upgrade localdevstack and scoop update localdevstack pick up the new version automatically.
If you need to re-run a release without pushing a new tag (e.g. a failed workflow), or to fire the workflow against an rc tag that the strict push trigger ignores:
- GitHub → dev repo → Actions → Release → Run workflow.
- Enter the tag to build (e.g.
v1.2.0orv1.2.0-rc1).
For the full rc round-trip flow, see mirror-sync.md → Step 5.
Lessons learned during the CI bringup that aren't obvious from release.yml alone. Reverting any of these re-introduces a real failure that took an rc cycle to diagnose.
-
The Gradle wrapper must stay committed and intact. Four files (
gradlew,gradlew.bat,gradle/wrapper/gradle-wrapper.jar,gradle/wrapper/gradle-wrapper.properties) plus three guards:.gitignore's!gradle/wrapper/gradle-wrapper.jarnegation (the*.jarrule above eats it otherwise — observed: "Could not find or load main class org.gradle.wrapper.GradleWrapperMain" on the runner),.gitattributes'sgradlew text eol=lf(Windowscore.autocrlf=trueotherwise rewrites the shebang and Linux runners report "exec format error"), and the executable bit in the git index (git ls-files --stage gradlewmust show mode100755; if it's100644, the runner reports "Permission denied"). When committing wrapper changes from Windows, rungit update-index --chmod=+x gradlewbefore the commit. -
Foojay resolver in
settings.gradle.ktsis load-bearing.build.gradle.kts:39requestsjvmToolchain(17), but the Windows CI job only installs GraalVM 21 viasetup-graalvm@v1. Withoutorg.gradle.toolchains.foojay-resolver-convention, Gradle fails with "No locally installed toolchains match … toolchain download repositories have not been configured." The Linux/macOS runners pass by accident because they ship JDK 17 preinstalled. Don't remove the plugin. -
release.yml's tag trigger excludes rc tags by design.on.push.tags: 'v[0-9]+.[0-9]+.[0-9]+'is an anchored glob —v1.2.0-rc1does NOT match. Rc testing usesworkflow_dispatch(see mirror-sync.md → Step 5). The strict pattern is intentional: it stops prerelease tags from shipping to brew/scoop users. Don't relax it tov[0-9]+.[0-9]+.[0-9]+*without also rethinking the formula/manifest update path. -
The version-match guard is gated on
github.event_name == 'push'. Real tag pushes get the safety check (catches "tagged v1.2.0 butbuild.gradle.ktsstill says 1.1.0"); rcworkflow_dispatchruns skip it because the synthetic tag intentionally won't match. Don't drop theif:. -
No smoke step on macOS or Windows; smoke runs Linux-only. macOS GitHub runners don't ship Docker and the CLI's Docker availability probe exits before any generator runs. Windows runners do ship Docker but historically had startup-time flakes that doubled the wall-clock of the release. The Linux job's smoke step exercises the same Kotlin → native binary; macOS/Windows coverage is "nativeCompile succeeds." Don't add a smoke step to either platform without also providing Docker.
-
Windows native-image needs
ilammy/msvc-dev-cmd@v1as a separate step before./gradlew nativeCompile. Without it (or with onlysetup-graalvm'snative-image-msvc: 'true'— observed to fail onwindows-latestin May 2026),native-image.cmdexits with code 20 and "Failed to find 'vcvarsall.bat' in a Visual Studio installation" even though VS Build Tools are installed on the runner. Theilammy/msvc-dev-cmdaction invokesvcvarsall.bat amd64directly and exports the MSVC env vars to subsequent steps; that's the reliable path. -
Intel macOS (
build-macos-x64) was dropped, not just disabled. GitHub retired themacos-13runner image in late 2025; no free Intel macOS runner replaced it (macos-14-largeetc. exist but are paid). The Homebrew formula'son_intel doblock was also removed (Formula/localdevstack.rb), as were the x64 sha256/urlsedlines in the publish step. If you ever re-add Intel macOS support, all four sites must change together: a build job, thepublishjob'sneeds:list, thesedrules, and the formula. -
Pushing the wrapper from a fresh
gradle wrappermay fail SSL verification locally. The wrapper's first-use download fromservices.gradle.orgcan hitPKIX path building failedon machines with stale JDK truststores — this is a local issue, not a wrapper-generation issue. The wrapper files are correct; CI runners have current CA bundles and will fetch fine.
Verify nativeCompile works on your machine before tagging:
# Install GraalVM 21 via SDKMAN (macOS/Linux)
sdk install java 21-graalce
# Windows — download from https://www.graalvm.org/downloads/ and set JAVA_HOME
./gradlew nativeCompile
./build/native/nativeCompile/localdevstack --version