# Build release archives for a version tag and attach them to a GitHub # Release. Trigger with: # # git tag -a v0.1.0 -m "..." && git push origin v0.1.0 # # The build matrix only uploads artifacts; a single publish job creates the # release afterwards. When each build job published directly, all three ran # generate_release_notes and the release body carried the same changelog # line three times. # # Inherited from logman, plus the two halves logman does not have. Every build # job runs Gradle -> jlink -> cargo, in that order: rudbman-jdbc's build script # refuses to compile without bridge/build/libs/rudbman-bridge.jar, and the # archives ship a jlink runtime so users need no JDK of their own. What comes # out is a tree, not a binary — see architecture.md 10.3. name: Release on: push: tags: ["v*"] permissions: contents: write env: CARGO_TERM_COLOR: always jobs: build: name: ${{ matrix.target }} runs-on: ${{ matrix.os }} timeout-minutes: 90 strategy: fail-fast: false matrix: include: - os: ubuntu-latest target: x86_64-unknown-linux-gnu - os: windows-latest target: x86_64-pc-windows-msvc - os: macos-latest target: aarch64-apple-darwin steps: - uses: actions/checkout@v7 - name: Install Linux system libraries if: runner.os == 'Linux' run: | sudo apt-get update sudo apt-get install -y --no-install-recommends \ libxkbcommon-dev libxkbcommon-x11-dev libwayland-dev libfontconfig1-dev # gpui_windows' build script compiles HLSL shaders with fxc.exe from # the Windows SDK. It looks on PATH and then at whatever the registry # records, so point it at the SDK the runner image actually carries. - name: Locate fxc.exe if: runner.os == 'Windows' shell: pwsh run: | $fxc = Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin" -Recurse -Filter fxc.exe -ErrorAction SilentlyContinue | Where-Object { $_.FullName -match '\\x64\\' } | Sort-Object FullName | Select-Object -Last 1 if (-not $fxc) { throw "fxc.exe not found in the Windows SDK" } "GPUI_FXC_PATH=$($fxc.FullName)" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 # The x86_64-pc-windows-msvc target links the CRT dynamically by # default, so the resulting rudbman.exe needs VCRUNTIME140.dll and the # api-ms-win-crt-* forwarders at load time. Neither the installer nor # the zip carries the VC++ redistributable — a machine that has never # installed it (the winget-pkgs validation VM, in practice) fails to # even start rudbman.exe with STATUS_DLL_NOT_FOUND (0xC0000135). The # bundled jlink runtime under runtime\bin does carry its own copy of # these DLLs, but rudbman.exe does not search that subfolder for its # own dependencies, so it is no help here. Statically linking the CRT # folds those DLLs into the binary, so it runs standalone on a bare # Windows install, whether launched from the zip or the installer. # Only this OS's build needs it, hence a dedicated step rather than a # matrix-wide `env:` (which would also apply the flag, harmlessly but # pointlessly, to Linux and macOS). The winget manifest's # Microsoft.VCRedist.2015+.x64 dependency # (packaging/winget/*/Xcomart.Rudbman.installer.yaml) stays declared # regardless: it is a no-op once the runtime is already on the machine, # and a safety net if a future build ever drops this flag. Swatinem/ # rust-cache@v2 keys its cache on RUSTFLAGS, so toggling this between # runs cannot serve a stale dynamically-linked object into a # statically-linked build or vice versa. - name: Statically link the CRT (windows) if: runner.os == 'Windows' shell: pwsh run: | "RUSTFLAGS=-C target-feature=+crt-static" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 # 17 is what the bridge compiles against (--release 17), so it is also # what the shipped runtime is jlinked from. Temurin because it exists for # all three runners and carries the jmods jlink needs. - name: Install JDK uses: actions/setup-java@v5 with: distribution: temurin java-version: '17' cache: gradle cache-dependency-path: bridge/*.gradle # Before cargo, always: rudbman-jdbc's build script panics if the JAR is # missing. Only `jar` here — CI already ran the bridge's own suite on the # commit this tag points at. - name: Build the bridge JAR working-directory: bridge # bash on every runner: the Windows one has Git Bash, and this keeps # `./gradlew` meaning the same file on all three. shell: bash run: ./gradlew jar --no-daemon # The Java runtime that ships inside the archive, per architecture.md # 10.2. The module list is the load-bearing part: a driver that reaches # for a missing module fails at connect time, not at build time, which is # why the smoke step below checks the result. jdk.unsupported covers # drivers using sun.misc.Unsafe and jdk.charsets covers the extended # charsets the template engine's EUC-KR width maths needs. # # `--compress=2` is JDK 17's spelling of maximum compression; the # `--compress=zip-N` form only exists from JDK 21 on. # # `--strip-native-commands` drops the launcher binaries (`java`, # `keytool`, …). rudbman never runs them — the JVM is loaded in-process # through the shared library (`bin/server/jvm.dll` / `lib/server/libjvm.*`, # §4.1, `jvm.rs`) — and shipping them is actively harmful on Windows: # winget-pkgs validation launches every installed .exe with no # arguments, and `java.exe` prints usage and exits 1, which earns the # package a `Validation-Executable-Error` label. - name: Build the bundled Java runtime shell: bash run: | jlink --module-path "$JAVA_HOME/jmods" \ --add-modules \ java.base,java.sql,java.sql.rowset,java.naming,java.transaction.xa,java.security.jgss,java.security.sasl,java.management,java.logging,jdk.charsets,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported,jdk.net \ --strip-debug --strip-native-commands --no-header-files --no-man-pages --compress=2 \ --output runtime - uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.target }} - uses: Swatinem/rust-cache@v2 - name: Build run: cargo build --release --locked -p rudbman-app --target ${{ matrix.target }} # A bare Mach-O binary cannot carry an icon; macOS wants an .app bundle # with an Info.plist and an .icns. The runtime and the bridge JAR both go # under Contents/, one level above the executable, which is the second # place the JVM loader looks for each (architecture.md 4.1, 10.3). Not # under Contents/MacOS/: everything there is nested code to codesign, # and sealing the bundle fails on an unsigned JAR — the first v0.1.0 # run failed exactly this way. # # The bundle is ad-hoc signed (`codesign --sign -`): no Apple Developer # account, no secrets, and no identity in the signature. That is not # notarization and it does not satisfy Gatekeeper, so the first launch of # a downloaded copy still needs right-click -> Open. What it buys is one # signature over the bundle as a whole, sealing the Info.plist we just # wrote where the linker's own ad-hoc signature covers the executable # alone: from here nothing in the bundle can change without # `codesign --verify` noticing. Same tamper seal the Windows step settles # for, minus the certificate. # # logman verifies with --deep; rudbman cannot. The jlink runtime is a # tree of Mach-O files that arrive with signatures of their own, and a # deep verify walks into them and judges those instead of the seal this # step just made. --strict over the bundle is the claim being made here. - name: Package (macos) if: runner.os == 'macOS' run: | version="${GITHUB_REF_NAME#v}" app="rudbman.app" mkdir -p "$app/Contents/MacOS" "$app/Contents/lib" "$app/Contents/Resources" cp "target/${{ matrix.target }}/release/rudbman" "$app/Contents/MacOS/" cp bridge/build/libs/rudbman-bridge.jar "$app/Contents/lib/" cp -R runtime "$app/Contents/runtime" cp assets/icon.icns "$app/Contents/Resources/rudbman.icns" sed "s/__VERSION__/$version/g" packaging/macos/Info.plist > "$app/Contents/Info.plist" plutil -lint "$app/Contents/Info.plist" codesign --force --sign - "$app" codesign --verify --strict "$app" staging="rudbman-${{ github.ref_name }}-${{ matrix.target }}" mkdir "$staging" cp -R "$app" README.md "$staging/" tar czf "$staging.tar.gz" "$staging" echo "ASSET=$staging.tar.gz" >> "$GITHUB_ENV" echo "STAGING=$staging" >> "$GITHUB_ENV" # Linux gets the flat tree plus a desktop entry, hicolor icons and a # per-user install script; install.sh puts everything under ~/.local. - name: Package (linux) if: runner.os == 'Linux' run: | staging="rudbman-${{ github.ref_name }}-${{ matrix.target }}" mkdir -p "$staging/lib" "$staging/icons" cp "target/${{ matrix.target }}/release/rudbman" "$staging/" cp bridge/build/libs/rudbman-bridge.jar "$staging/lib/" cp -R runtime "$staging/runtime" cp README.md packaging/linux/com.aihouse.rudbman.desktop "$staging/" install -m755 packaging/linux/install.sh "$staging/install.sh" cp assets/icon-128.png "$staging/icons/rudbman-128.png" cp assets/icon-256.png "$staging/icons/rudbman-256.png" cp assets/icon.svg "$staging/icons/rudbman.svg" tar czf "$staging.tar.gz" "$staging" echo "ASSET=$staging.tar.gz" >> "$GITHUB_ENV" echo "STAGING=$staging" >> "$GITHUB_ENV" # signtool.exe ships with the Windows SDK and is not on PATH, so it needs # the same treatment as fxc.exe above. - name: Locate signtool.exe if: runner.os == 'Windows' shell: pwsh run: | $candidates = Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin" -Recurse -Filter signtool.exe -ErrorAction SilentlyContinue | Where-Object { $_.FullName -match '\\x64\\' } # Versioned SDK directories (bin\10.0.26100.0\x64) sort newest last; # the legacy unversioned bin\x64 copy is only a fallback because it # can predate the SDK actually installed on the image. $signtool = $candidates | Where-Object { $_.FullName -match '\\bin\\10\.[0-9.]+\\x64\\' } | Sort-Object FullName | Select-Object -Last 1 if (-not $signtool) { $signtool = $candidates | Select-Object -First 1 } if (-not $signtool) { throw "signtool.exe not found in the Windows SDK" } "SIGNTOOL=$($signtool.FullName)" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 # Authenticode-sign the exe with a self-signed certificate. The long form # of why — that this is a tamper seal and not a reputation, that # SmartScreen is unmoved by it, and why a missing secret has to skip # rather than fail — now lives at the top of packaging/windows/sign.ps1, # because the installer built further down needs the identical treatment # and the retry-and-cleanup dance is not worth having twice. # # The secrets are still mapped into env here rather than tested in `if:`: # the `secrets` context is not available in step-level `if:` expressions, # so the "is a certificate configured" question can only be asked inside # the script. - name: Sign (windows) if: runner.os == 'Windows' shell: pwsh env: PFX_BASE64: ${{ secrets.WINDOWS_SIGNING_PFX_BASE64 }} PFX_PASSWORD: ${{ secrets.WINDOWS_SIGNING_PFX_PASSWORD }} run: ./packaging/windows/sign.ps1 "target/${{ matrix.target }}/release/rudbman.exe" - name: Package (windows) if: runner.os == 'Windows' shell: pwsh run: | $staging = "rudbman-${{ github.ref_name }}-${{ matrix.target }}" New-Item -ItemType Directory "$staging/lib" | Out-Null Copy-Item "target/${{ matrix.target }}/release/rudbman.exe" $staging Copy-Item "bridge/build/libs/rudbman-bridge.jar" "$staging/lib/" Copy-Item runtime $staging -Recurse Copy-Item README.md $staging Compress-Archive -Path $staging -DestinationPath "$staging.zip" "ASSET=$staging.zip" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 "STAGING=$staging" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 # The archive layout is a contract with code that has already shipped: # the JVM loader finds its runtime and the bridge JAR by walking relative # paths from the executable, and a module the jlink list forgot only # surfaces as a NoClassDefFoundError when a user opens a connection. So # check the staged tree before it is published, not after (10.3). # # jlink strips the launchers (§10.2), so there is no `runtime/bin/java` # to run any more on any platform. Instead the module list is read # straight out of the `release` file jlink writes at the runtime root # (a `MODULES="..."` line), and the JVM shared library is checked # because that is exactly what the loader in `jvm.rs`'s # `is_java_home` looks for. - name: Smoke-test the staged tree shell: bash run: | if [ "$RUNNER_OS" = "macOS" ]; then contents="$STAGING/rudbman.app/Contents" exe="$contents/MacOS/rudbman" jar="$contents/lib/rudbman-bridge.jar" runtime="$contents/runtime" else exe="$STAGING/rudbman" if [ "$RUNNER_OS" = "Windows" ]; then exe="$exe.exe"; fi jar="$STAGING/lib/rudbman-bridge.jar" runtime="$STAGING/runtime" fi test -f "$exe" || { echo "no executable at $exe"; exit 1; } test -f "$jar" || { echo "no bridge JAR at $jar"; exit 1; } jvm_lib="" for candidate in "$runtime/lib/server/libjvm.so" "$runtime/lib/server/libjvm.dylib" "$runtime/lib/libjvm.dylib" "$runtime/bin/server/jvm.dll"; do if [ -f "$candidate" ]; then jvm_lib="$candidate"; break; fi done test -n "$jvm_lib" || { echo "no JVM shared library under $runtime"; exit 1; } release="$runtime/release" test -f "$release" || { echo "no jlink release file at $release"; exit 1; } modules=$(sed -n 's/^MODULES="\(.*\)"$/\1/p' "$release" | tr ' ' '\n') for module in jdk.charsets jdk.unsupported; do if ! printf '%s\n' "$modules" | grep -qx "$module"; then echo "the bundled runtime is missing $module" printf '%s\n' "$modules" exit 1 fi done echo "$exe, $jar and a runtime with $(printf '%s\n' "$modules" | wc -l) modules" # The second Windows artifact: an Inno Setup installer wrapped around the # very tree the step above just vouched for. The zip stays — the in-app # updater downloads and unpacks it (rugpui-shell, pointed at this product # by crates/rudbman-app/src/app_identity.rs), so # removing it would break every copy already installed. The installer is # what makes the package installable by winget: unzipping registers # nothing with Windows, and winget reads the "Apps & features" registry # entry to know which version is present and how to remove or upgrade it. # No ARP entry, no winget package. # # Inno Setup 6 comes and goes from the windows-latest image, so neither # "it is there" nor "it is not" is safe to assume: look in both Program # Files roots first and fall back to Chocolatey, which the image does # carry. `$version` drops the tag's "v" because VersionInfoVersion in the # script is a numeric quad and refuses a prefix, while the output file # name keeps the tag verbatim so it matches the zip beside it. - name: Build the installer (windows) if: runner.os == 'Windows' shell: pwsh run: | # Both Program Files roots, and a wildcard on the version folder so a # future "Inno Setup 7" (or whatever Chocolatey happens to lay down) # is still found rather than silently missed. function Find-Iscc { @("${env:ProgramFiles(x86)}", $env:ProgramFiles) | Where-Object { $_ } | ForEach-Object { Join-Path $_ "Inno Setup *\ISCC.exe" } | Get-Item -ErrorAction SilentlyContinue | Sort-Object FullName | Select-Object -Last 1 -ExpandProperty FullName } $iscc = Find-Iscc if (-not $iscc) { Write-Host "Inno Setup is not on this image; installing it with Chocolatey" choco install innosetup --no-progress -y $iscc = Find-Iscc } if (-not $iscc) { throw "ISCC.exe not found, even after installing Inno Setup" } Write-Host "compiling with $iscc" $version = "${{ github.ref_name }}" -replace '^v', '' $base = "rudbman-${{ github.ref_name }}-${{ matrix.target }}-setup" # ISCC resolves a relative OutputDir against the script's own # directory, which would bury the installer in packaging\windows\. # Absolute paths for both ends, so it lands next to the zip. $source = (Resolve-Path $env:STAGING).Path $out = (Get-Location).Path & $iscc "/DVersion=$version" "/DSourceDir=$source" "/DOutputDir=$out" "/DOutputBaseFilename=$base" packaging\windows\rudbman.iss if ($LASTEXITCODE -ne 0) { throw "ISCC exited $LASTEXITCODE" } $installer = Join-Path $out "$base.exe" if (-not (Test-Path $installer)) { throw "ISCC reported success but produced no $base.exe" } "INSTALLER=$installer" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 # Windows now uploads two files, so ASSET has to become multi-line, # which in GITHUB_ENV means the heredoc form. This later assignment # wins over the single-line ASSET the packaging step wrote. Written # through AppendAllText with explicit LFs rather than Out-File # because Out-File would end each line with CRLF on Windows and the # stray carriage returns would ride along inside the heredoc value, # turning the artifact paths into names that match nothing. Both # entries stay workspace-relative so upload-artifact resolves them # against the same root. $block = "ASSET< release-body.md # A lightweight tag has no message; fall back to the subject line. if ! grep -q '[^[:space:]]' release-body.md; then git tag -l --format='%(contents:subject)' "${GITHUB_REF_NAME}" > release-body.md fi cat release-body.md - uses: actions/download-artifact@v8 with: path: dist merge-multiple: true - name: Attach to release uses: softprops/action-gh-release@v3 with: body_path: release-body.md generate_release_notes: true files: dist/* # Push the new version to the Windows Package Manager repository, so that # `winget upgrade` sees a release the moment it is published instead of # whenever somebody remembers to open a pull request by hand. # # THIS JOB CANNOT CREATE THE PACKAGE. `wingetcreate update` edits manifests # that already exist in microsoft/winget-pkgs; if Xcomart.Rudbman is not # there yet it fails with "package not found". The first submission is a # manual step, done once, from the manifests kept in packaging/winget/ — # packaging/winget/README.md walks through it. Only once that pull request # has been merged is it worth creating the WINGET_PAT secret, a classic PAT # with `public_repo` scope on an account that has forked winget-pkgs. # # Until then the secret is absent and this job does nothing, which is # deliberate: a release must not fail because a downstream package # repository is not ready. The check happens inside the script rather than # in a step-level `if:` for the same reason the signing step does it that # way — the `secrets` context does not exist in `if:` expressions. winget: name: winget needs: publish runs-on: windows-latest timeout-minutes: 20 steps: - name: Submit the installer to winget-pkgs shell: pwsh env: WINGET_PAT: ${{ secrets.WINGET_PAT }} run: | if ([string]::IsNullOrWhiteSpace($env:WINGET_PAT)) { Write-Host "winget submission skipped (no WINGET_PAT secret)" exit 0 } $tag = "${{ github.ref_name }}" $version = $tag -replace '^v', '' $url = "https://github.com/xcomart/rudbman/releases/download/$tag/rudbman-$tag-x86_64-pc-windows-msvc-setup.exe" # Fetched straight from its GitHub release rather than through # `winget install wingetcreate`: the winget CLI itself is not # dependably present on a hosted runner (it ships with the Desktop # App Installer, which needs a logged-in user session), whereas a # plain download works everywhere. aka.ms/wingetcreate/latest is the # publisher's own permalink to the newest build. $exe = Join-Path $env:RUNNER_TEMP "wingetcreate.exe" Invoke-WebRequest -Uri "https://aka.ms/wingetcreate/latest" -OutFile $exe # --submit opens (or updates) the pull request against # microsoft/winget-pkgs from the token's fork; wingetcreate # downloads the installer from $url itself to compute its SHA256, so # the release assets must already be attached — hence needs: publish. & $exe update Xcomart.Rudbman --version $version --urls $url --submit --token $env:WINGET_PAT if ($LASTEXITCODE -ne 0) { throw "wingetcreate exited $LASTEXITCODE" }