Skip to content

docs(roadmap): close v0.82.0 #221

docs(roadmap): close v0.82.0

docs(roadmap): close v0.82.0 #221

Workflow file for this run

name: Release
# Phase 11 release contract:
# - Public tags are vX.Y.Z or vX.Y.Z-rcN.
# - Workflow dispatch without publish=true is a dry run.
# - Tag pushes and workflow_dispatch publish=true are release-cut gates.
# - Publishing uploads assets first, verifies the uploaded assets, generates
# /dl metadata from those assets, then deploys Pages.
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
release_tag:
description: Existing vX.Y.Z or vX.Y.Z-rcN tag to publish, required when publish is true from a branch
required: false
type: string
publish:
description: Upload release assets and /dl metadata; dispatch publishes no downstream targets
required: true
default: false
type: boolean
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
jobs:
context:
name: release context
runs-on: ubuntu-latest
outputs:
publish: ${{ steps.context.outputs.publish }}
tag: ${{ steps.context.outputs.tag }}
version: ${{ steps.context.outputs.version }}
prerelease: ${{ contains(steps.context.outputs.version, '-') }}
steps:
- uses: actions/checkout@v6
with:
path: chan
- id: context
name: Resolve release tag
env:
INPUT_PUBLISH: ${{ github.event.inputs.publish }}
INPUT_TAG: ${{ github.event.inputs.release_tag }}
run: |
set -e
publish=false
if [ "$GITHUB_REF_TYPE" = "tag" ]; then
tag="$GITHUB_REF_NAME"
publish=true
else
tag="$INPUT_TAG"
if [ -z "$tag" ]; then
version=$(sed -n 's/^version = "\(.*\)"/\1/p' chan/Cargo.toml | head -1)
tag="v${version}"
fi
if [ "$INPUT_PUBLISH" = "true" ]; then
publish=true
if [ -z "$INPUT_TAG" ]; then
echo "::error::release_tag is required when publish=true from a branch"
exit 1
fi
fi
fi
# Accept vX.Y.Z and an optional prerelease suffix (e.g. v0.50.0-rc1).
if [[ ! "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ ]]; then
echo "::error::release tag must use vX.Y.Z or vX.Y.Z-rcN, got $tag"
exit 1
fi
version=${tag#v}
cargo_version=$(sed -n 's/^version = "\(.*\)"/\1/p' chan/Cargo.toml | head -1)
desktop_version=$(sed -n 's/.*"version": "\([^"]*\)".*/\1/p' chan/desktop/src-tauri/tauri.conf.json | head -1)
if [ "$version" != "$cargo_version" ]; then
echo "::error::tag $tag does not match Cargo version $cargo_version"
exit 1
fi
if [ "$version" != "$desktop_version" ]; then
echo "::error::tag $tag does not match desktop version $desktop_version"
exit 1
fi
# The gateway is versioned in lockstep with the root; its
# nested workspace carries its own [workspace.package] version
# that must be bumped in the same commit.
gateway_version=$(sed -n 's/^version = "\(.*\)"/\1/p' chan/gateway/Cargo.toml | head -1)
if [ "$version" != "$gateway_version" ]; then
echo "::error::tag $tag does not match gateway version $gateway_version"
exit 1
fi
echo "publish=$publish" >> "$GITHUB_OUTPUT"
echo "tag=$tag" >> "$GITHUB_OUTPUT"
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "release tag: $tag"
echo "publish: $publish"
linux-validate:
name: linux validation
needs: context
runs-on: ubuntu-latest
env:
RUSTFLAGS: -D warnings
steps:
- uses: actions/checkout@v6
with:
path: chan
- run: cp chan/rust-toolchain.toml ./rust-toolchain.toml
- uses: actions-rust-lang/setup-rust-toolchain@v1
- uses: Swatinem/rust-cache@v2
with:
workspaces: chan
- uses: actions/setup-node@v6
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: chan/web/package-lock.json
- name: Install Linux Tauri build deps
run: |
sudo apt-get update
sudo apt-get install -y \
libwebkit2gtk-4.1-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
libsoup-3.0-dev \
patchelf
- run: make ci-linux
working-directory: chan
linux-cli-artifacts:
name: linux CLI tarball (${{ matrix.musl_target }})
needs: linux-validate
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
musl_target: x86_64-unknown-linux-musl
- os: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
musl_target: aarch64-unknown-linux-musl
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
with:
path: chan
- run: cp chan/rust-toolchain.toml ./rust-toolchain.toml
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
target: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
with:
workspaces: chan
# The standalone CLI tarball is musl (fully static) so a too-new build
# glibc does not gate older machines. zig is the cross C/C++ compiler
# cargo-zigbuild drives so the C/C++ deps (ring, bundled SQLite,
# tokenizers esaxx-rs/onig) link static.
- run: rustup target add ${{ matrix.musl_target }}
- uses: mlugg/setup-zig@v2
with:
version: 0.15.2
- uses: actions/setup-node@v6
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: chan/web/package-lock.json
- uses: taiki-e/install-action@v2
with:
tool: cargo-zigbuild
- run: make linux-chan-tarball LINUX_TARGET=${{ matrix.musl_target }}
working-directory: chan
- name: Stage Linux CLI artifacts
working-directory: chan
env:
MUSL_TARGET: ${{ matrix.musl_target }}
run: |
set -e
mkdir -p release-artifacts
cp "target/release/chan-${MUSL_TARGET}.tar.gz" release-artifacts/
ls -la release-artifacts
- uses: actions/upload-artifact@v7
with:
name: release-linux-cli-${{ matrix.target }}
path: chan/release-artifacts/*
if-no-files-found: error
gateway-linux-packages:
name: gateway packages (${{ matrix.target }})
# The gateway is a separate nested workspace and ships only the
# five server-side .deb packages, on linux amd64/arm64. Built on
# native runners per arch (no cross toolchain) like the CLI job.
# Artifact name uses the release-* prefix so publish-release picks
# it up via its existing download pattern + files glob.
needs: context
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
package_arch: amd64
- os: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
package_arch: arm64
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
with:
path: chan
- run: cp chan/rust-toolchain.toml ./rust-toolchain.toml
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
target: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
with:
workspaces: chan/gateway
key: gateway-${{ matrix.target }}
- uses: actions/setup-node@v6
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: chan/web/package-lock.json
- uses: taiki-e/install-action@v2
with:
tool: cargo-deb
- name: Build release binaries
# Single-sourced through chan/Makefile so this and the pre-push
# gateway-build stay in lockstep: GATEWAY_RELEASE_CRATES fixes the
# crate names (a rename can no longer drift CI off the real ones)
# and the gateway-spa prerequisite builds the identity SPA bundle
# that rust-embed needs before cargo build.
working-directory: chan
run: |
make gateway-build \
GATEWAY_CARGO_FLAGS="--release --target ${{ matrix.target }}"
- name: Build .deb packages
working-directory: chan/gateway
run: |
set -e
mkdir -p release-artifacts
for crate in $(make -C .. -s gateway-release-crates); do
cargo deb --no-build --no-strip \
--target ${{ matrix.target }} \
-p "$crate" \
--output release-artifacts/
done
ls -la release-artifacts
- uses: actions/upload-artifact@v7
with:
name: release-gateway-${{ matrix.package_arch }}
path: chan/gateway/release-artifacts/*.deb
if-no-files-found: error
linux-desktop-artifacts:
name: linux desktop packages (${{ matrix.package_arch }})
# GitHub-hosted runners are ubuntu-only, so CI ships the UNIVERSAL
# AppImage that Tauri targets:"all" already emits, on amd64 AND arm64
# (matching the CLI + gateway matrix jobs). Tauri also emits a .deb and
# .rpm alongside it, but those ship through COPR/PPA/AUR instead of as
# GitHub Release artifacts. The per-distro sdme rootfs templates under
# packaging/sdme/chan-desktop-{ubuntu,fedora,arch}.sdme own the LOCAL
# multi-distro dev/QA build path (build from macOS via lima); they are
# not consumable by GH Actions, so CI leans on the universal AppImage
# rather than literal fedora/arch container jobs (which would re-list
# distro deps = the single-source drift that killed v0.19.0).
needs:
- context
- linux-validate
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
package_arch: amd64
- os: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
package_arch: arm64
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
with:
path: chan
- run: cp chan/rust-toolchain.toml ./rust-toolchain.toml
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
target: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
with:
workspaces: chan
- uses: actions/setup-node@v6
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: chan/web/package-lock.json
- name: Install Linux Tauri build deps
run: |
sudo apt-get update
sudo apt-get install -y \
libwebkit2gtk-4.1-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
libsoup-3.0-dev \
patchelf \
xdg-utils \
desktop-file-utils \
file
- name: Install tauri-cli
uses: taiki-e/install-action@v2
with:
tool: tauri-cli@2
- run: make chan-desktop
working-directory: chan
- name: Stage Linux desktop artifacts
working-directory: chan
run: |
set -e
mkdir -p release-artifacts
# Tauri targets:"all" also emits a .deb and .rpm per arch, but CI
# stages only the AppImage as a GitHub Release artifact; the
# deb/rpm channel is COPR/PPA/AUR instead. Glob the format dir so
# the arch-specific filename skew (AppImage amd64/aarch64) needs
# no per-arch bookkeeping. Kept as a loop over one entry (not
# inlined) so a future format re-add is a one-line change and the
# missing-artifact error path stays shared.
# shellcheck disable=SC2066 # single-item on purpose, see above
for dir_glob in \
"target/release/bundle/appimage/*.AppImage"; do
file=$(find "$(dirname "$dir_glob")" -name "$(basename "$dir_glob")" -type f | head -1)
if [ -z "$file" ]; then
echo "::error::missing desktop artifact: $dir_glob"
find target/release/bundle -type f || true
exit 1
fi
cp "$file" release-artifacts/
done
ls -la release-artifacts
- uses: actions/upload-artifact@v7
with:
name: release-linux-desktop-${{ matrix.package_arch }}
path: chan/release-artifacts/*
if-no-files-found: error
macos-validate:
name: macOS validation
needs: context
runs-on: macos-latest
env:
RUSTFLAGS: -D warnings
steps:
- uses: actions/checkout@v6
with:
path: chan
- run: cp chan/rust-toolchain.toml ./rust-toolchain.toml
- uses: actions-rust-lang/setup-rust-toolchain@v1
- uses: Swatinem/rust-cache@v2
with:
workspaces: chan
- run: make ci-macos
working-directory: chan
macos-cli-artifact:
name: macOS CLI package
needs: macos-validate
runs-on: macos-latest
steps:
- uses: actions/checkout@v6
with:
path: chan
- run: cp chan/rust-toolchain.toml ./rust-toolchain.toml
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
target: aarch64-apple-darwin
- uses: Swatinem/rust-cache@v2
with:
workspaces: chan
- uses: actions/setup-node@v6
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: chan/web/package-lock.json
- run: make chan CHAN_TARGET=aarch64-apple-darwin
working-directory: chan
- name: Stage macOS CLI artifact
working-directory: chan
run: |
set -e
mkdir -p release-artifacts staging
cp target/aarch64-apple-darwin/release/chan staging/
cp LICENSE README.md staging/
tar -czf release-artifacts/chan-aarch64-apple-darwin.tar.gz -C staging .
ls -la release-artifacts
- uses: actions/upload-artifact@v7
with:
name: release-macos-cli
path: chan/release-artifacts/*
if-no-files-found: error
macos-desktop-artifacts:
name: macOS desktop package
needs:
- context
- macos-validate
runs-on: macos-latest
steps:
- uses: actions/checkout@v6
with:
path: chan
- run: cp chan/rust-toolchain.toml ./rust-toolchain.toml
- uses: actions-rust-lang/setup-rust-toolchain@v1
- uses: Swatinem/rust-cache@v2
with:
workspaces: chan
# macOS window chrome follows the SDK the binary was LINKED
# against: the runner's default Xcode is older than current, so
# the shipped app rendered the legacy opaque titlebar while a
# local build (current Xcode) gets the modern unified look.
# Select the newest Xcode on the image and log the SDK so a
# regression is visible in the build log.
- name: Select newest Xcode (window chrome follows the linked SDK)
run: |
sudo xcode-select -s "$(python3 chan/scripts/select-newest-xcode.py)"
xcodebuild -version
xcrun --show-sdk-version
- uses: actions/setup-node@v6
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: chan/web/package-lock.json
- name: Install tauri-cli
uses: taiki-e/install-action@v2
with:
tool: tauri-cli@2
- name: Verify signing secrets present
env:
APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
run: |
set -e
missing=()
[ -n "$APPLE_CERTIFICATE_BASE64" ] || missing+=("APPLE_CERTIFICATE_BASE64")
[ -n "$APPLE_CERTIFICATE_PASSWORD" ] || missing+=("APPLE_CERTIFICATE_PASSWORD")
[ -n "$APPLE_SIGNING_IDENTITY" ] || missing+=("APPLE_SIGNING_IDENTITY")
[ -n "$APPLE_TEAM_ID" ] || missing+=("APPLE_TEAM_ID")
[ -n "$APPLE_ID" ] || missing+=("APPLE_ID")
[ -n "$APPLE_PASSWORD" ] || missing+=("APPLE_PASSWORD")
[ -n "$TAURI_SIGNING_PRIVATE_KEY" ] || missing+=("TAURI_SIGNING_PRIVATE_KEY")
if [ ${#missing[@]} -gt 0 ]; then
echo "::error::Missing signing secrets: ${missing[*]}"
echo "::error::Populate Apple signing and updater key secrets per .agents/desktop.md."
exit 1
fi
- name: Import Developer ID certificate
uses: apple-actions/import-codesign-certs@v7
with:
p12-file-base64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
- name: Build, sign, and notarize chan-desktop
working-directory: chan
env:
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
run: |
set -o pipefail
make macos-chan-dmg-notarised 2>&1 | tee /tmp/chan-desktop-build.log
- name: Verify signature and stapled notarization
run: |
set -e
APP="chan/target/release/bundle/macos/Chan.app"
DMG=$(find chan/target/release/bundle/dmg -name '*.dmg' -type f | head -1)
codesign -dv --verbose=2 "$APP" 2>&1 | head -30
stapler validate "$DMG"
spctl -a -t install -v "$DMG"
- name: Normalize DMG filename
env:
VERSION: ${{ needs.context.outputs.version }}
run: |
set -e
DMG_DIR="chan/target/release/bundle/dmg"
OLD=$(find "$DMG_DIR" -name '*.dmg' -type f | head -1)
NEW="$DMG_DIR/Chan_${VERSION}.dmg"
if [ "$OLD" != "$NEW" ]; then
mv "$OLD" "$NEW"
fi
ls -la "$DMG_DIR"
- name: Build and sign desktop updater payload
working-directory: chan
env:
VERSION: ${{ needs.context.outputs.version }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: |
set -e
payload="target/release/bundle/macos/Chan_${VERSION}_aarch64.app.tar.gz"
tar -czf "$payload" -C target/release/bundle/macos Chan.app
output=$(cd desktop/src-tauri && cargo tauri signer sign "../../$payload")
printf '%s\n' "$output"
signature=$(printf '%s\n' "$output" | sed -n 's/^Signature: //p' | tail -1)
if [ -z "$signature" ] && [ -f "$payload.sig" ]; then
signature=$(cat "$payload.sig")
fi
if [ -z "$signature" ]; then
echo "::error::cargo tauri signer did not produce a signature"
exit 1
fi
printf '%s\n' "$signature" > "$payload.sig"
- name: Stage macOS desktop artifacts
working-directory: chan
env:
VERSION: ${{ needs.context.outputs.version }}
run: |
set -e
mkdir -p release-artifacts
cp "target/release/bundle/dmg/Chan_${VERSION}.dmg" release-artifacts/
cp "target/release/bundle/macos/Chan_${VERSION}_aarch64.app.tar.gz" release-artifacts/
cp "target/release/bundle/macos/Chan_${VERSION}_aarch64.app.tar.gz.sig" release-artifacts/
ls -la release-artifacts
- if: failure()
name: Upload notarization diagnostics
uses: actions/upload-artifact@v7
with:
name: notarization-diagnostics
path: |
/tmp/chan-desktop-build.log
chan/target/release/bundle/
~/Library/Logs/com.apple.amp.itmstransporter/
if-no-files-found: ignore
retention-days: 14
- uses: actions/upload-artifact@v7
with:
name: release-macos-desktop
path: chan/release-artifacts/*
if-no-files-found: error
windows-artifacts:
name: Windows packages (signed)
needs: context
runs-on: windows-latest
steps:
- uses: actions/checkout@v6
with:
path: chan
- run: cp chan/rust-toolchain.toml ./rust-toolchain.toml
shell: bash
- uses: actions-rust-lang/setup-rust-toolchain@v1
- uses: Swatinem/rust-cache@v2
with:
workspaces: chan
- uses: actions/setup-node@v6
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: chan/web/package-lock.json
- name: Install tauri-cli
uses: taiki-e/install-action@v2
with:
tool: tauri-cli@2
- name: Verify Windows signing secrets present
shell: pwsh
env:
ES_USERNAME: ${{ secrets.ES_USERNAME }}
ES_PASSWORD: ${{ secrets.ES_PASSWORD }}
CREDENTIAL_ID: ${{ secrets.CREDENTIAL_ID }}
ES_TOTP_SECRET: ${{ secrets.ES_TOTP_SECRET }}
run: |
$ErrorActionPreference = "Stop"
$missing = @()
foreach ($name in @("ES_USERNAME", "ES_PASSWORD", "CREDENTIAL_ID", "ES_TOTP_SECRET")) {
$value = [Environment]::GetEnvironmentVariable($name)
if ([string]::IsNullOrWhiteSpace($value)) {
$missing += $name
} else {
Write-Output "::add-mask::$value"
}
}
if ($missing.Count -gt 0) {
Write-Output "::error::Missing Windows signing secrets: $($missing -join ', ')"
Write-Output "::error::Populate ES_USERNAME, ES_PASSWORD, CREDENTIAL_ID, and ES_TOTP_SECRET before running the release workflow."
exit 1
}
- id: codesigntool
name: Install SSL.com CodeSignTool
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$archive = Join-Path $env:RUNNER_TEMP "codesigntool-for-windows.zip"
$extractRoot = Join-Path $env:RUNNER_TEMP "codesigntool"
# CodeSignTool from SSL.com's official GitHub releases (pinned). The
# ssl.com/download page is not a reliable CI source (intermittent 500s).
Invoke-WebRequest `
-Uri "https://github.com/SSLcom/CodeSignTool/releases/download/v1.3.2/CodeSignTool-v1.3.2-windows.zip" `
-OutFile $archive
New-Item -ItemType Directory -Force -Path $extractRoot | Out-Null
Expand-Archive -LiteralPath $archive -DestinationPath $extractRoot -Force
$tool = Get-ChildItem -LiteralPath $extractRoot -Filter "CodeSignTool.bat" -Recurse -File |
Select-Object -First 1
if (-not $tool) {
Write-Output "::error::Downloaded SSL.com CodeSignTool archive did not contain CodeSignTool.bat"
Get-ChildItem -LiteralPath $extractRoot -Recurse | Select-Object FullName
exit 1
}
"path=$($tool.FullName)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
"root=$($tool.DirectoryName)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
Write-Output "CodeSignTool.bat: $($tool.FullName)"
- name: Build Windows packages (signed CLI zip + signed NSIS installer)
working-directory: chan
shell: bash
env:
VERSION: ${{ needs.context.outputs.version }}
ES_USERNAME: ${{ secrets.ES_USERNAME }}
ES_PASSWORD: ${{ secrets.ES_PASSWORD }}
CREDENTIAL_ID: ${{ secrets.CREDENTIAL_ID }}
ES_TOTP_SECRET: ${{ secrets.ES_TOTP_SECRET }}
CODESIGNTOOL: ${{ steps.codesigntool.outputs.path }}
CODE_SIGN_TOOL_PATH: ${{ steps.codesigntool.outputs.root }}
run: |
set -euo pipefail
# chan / chan-desktop embed BOTH frontend bundles via rust-embed: web/dist
# (WebAssets) and web-launcher/dist (LauncherAssets). Both must exist before
# the Rust crates compile, else LauncherAssets bakes an empty dir.
(cd web && npm install && npm run build -w @chan/launcher && npm run build -w @chan/workspace-app)
# Console-subsystem chan.exe CLI (a real terminal program): shipped standalone
# as the Windows CLI zip AND embedded into the NSIS installer as a resource.
cargo build --release -p chan
powershell -NoProfile -ExecutionPolicy Bypass -File desktop/src-tauri/scripts/windows/sign.ps1 target/release/chan.exe
mkdir -p release-artifacts staging
cp target/release/chan.exe staging/
cp LICENSE README.md staging/
# chan-x86_64-pc-windows-msvc.zip -- the name the install page resolves.
(cd staging && 7z a -tzip ../release-artifacts/chan-x86_64-pc-windows-msvc.zip chan.exe LICENSE README.md)
cargo build --release -p chan-desktop
powershell -NoProfile -ExecutionPolicy Bypass -File desktop/src-tauri/scripts/windows/sign.ps1 target/release/chan-desktop.exe
# NSIS only: bundle.targets "all" would also try the WiX MSI we dropped.
# tauri.windows.conf.json deep-merges the chan.exe built above as a resource.
# Cargo reuses the signed desktop binary when sources are fresh; the
# signCommand in tauri.windows.conf.json signs the NSIS installer.
(cd desktop/src-tauri && cargo tauri build --bundles nsis --config tauri.windows.conf.json)
powershell -NoProfile -ExecutionPolicy Bypass -File desktop/src-tauri/scripts/windows/sign.ps1 target/release/chan-desktop.exe
cp "target/release/bundle/nsis/Chan_${VERSION}_x64-setup.exe" release-artifacts/
ls -la release-artifacts
- name: Verify Windows Authenticode signatures
working-directory: chan
shell: pwsh
env:
VERSION: ${{ needs.context.outputs.version }}
run: |
$ErrorActionPreference = "Stop"
$installer = "release-artifacts/Chan_${env:VERSION}_x64-setup.exe"
if (-not (Test-Path -LiteralPath $installer -PathType Leaf)) {
Write-Output "::error::Missing NSIS installer: $installer"
Get-ChildItem -LiteralPath "release-artifacts" -Recurse | Select-Object FullName
exit 1
}
$signtool = (Get-Command "signtool.exe" -ErrorAction SilentlyContinue).Source
if (-not $signtool) {
$windowsKits = Join-Path ${env:ProgramFiles(x86)} "Windows Kits\10\bin"
if (Test-Path -LiteralPath $windowsKits -PathType Container) {
$signtool = Get-ChildItem -LiteralPath $windowsKits -Filter "signtool.exe" -Recurse -File |
Where-Object { $_.FullName -match "\\x64\\signtool\.exe$" } |
Sort-Object FullName -Descending |
Select-Object -First 1 -ExpandProperty FullName
}
}
if (-not $signtool) {
throw "signtool.exe not found"
}
foreach ($file in @("target/release/chan.exe", "target/release/chan-desktop.exe", $installer)) {
$signature = Get-AuthenticodeSignature -FilePath $file
$signature | Format-List Path,Status,StatusMessage,SignerCertificate,TimeStamperCertificate
if ($signature.Status -ne "Valid") {
throw "Authenticode signature is not valid for ${file}: $($signature.Status)"
}
& $signtool verify /pa /v $file
if ($LASTEXITCODE -ne 0) {
throw "signtool verify failed for $file with exit code $LASTEXITCODE"
}
}
- name: Headless boot + /api/health smoke
working-directory: chan
shell: bash
run: |
set -uo pipefail
# chan-desktop.exe dispatches the chan CLI when arg0 stem is `chan` (ARGV0
# wins). Off unix `chan open` has no GUI to hand off to, so it serves a
# standalone server -- the headless proof the Windows-built server boots.
exe="target/release/chan-desktop.exe"
ws="$RUNNER_TEMP/chan-smoke-ws"
mkdir -p "$ws"
ARGV0=chan CHAN_NO_DESKTOP_HANDOFF=1 "$exe" open \
--no-browser --no-token --port 8799 "$ws" &
srv=$!
ok=
for _ in $(seq 1 30); do
if curl -fsS http://127.0.0.1:8799/api/health | grep -q '"status":"ok"'; then
ok=1; echo "embedded server answered /api/health"; break
fi
sleep 2
done
kill "$srv" 2>/dev/null || true
if [ -z "$ok" ]; then
echo "::error::chan-desktop embedded server did not answer /api/health"
exit 1
fi
- uses: actions/upload-artifact@v7
with:
name: release-windows
path: chan/release-artifacts/*
if-no-files-found: error
publish-release:
name: publish GitHub Release
if: needs.context.outputs.publish == 'true'
needs:
- context
- linux-cli-artifacts
- gateway-linux-packages
- linux-desktop-artifacts
- macos-cli-artifact
- macos-desktop-artifacts
- windows-artifacts
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v6
with:
path: chan
- uses: actions/setup-node@v6
with:
node-version: '20'
- uses: actions/download-artifact@v8
with:
path: artifacts
pattern: release-*
- name: Install web workspace deps
run: npm install
working-directory: chan/web
- name: Build site
working-directory: chan/web/packages/marketing
run: npm run build
- name: List release files
run: |
find artifacts -type f | sort
- uses: softprops/action-gh-release@v3
with:
tag_name: ${{ needs.context.outputs.tag }}
prerelease: ${{ needs.context.outputs.prerelease }}
files: |
artifacts/**/*
fail_on_unmatched_files: true
generate_release_notes: true
- name: Verify uploaded release assets
working-directory: chan/web/packages/marketing
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.context.outputs.tag }}
run: npm run verify:release -- --tag "$TAG"
pages-artifact:
name: build Pages artifact with /dl metadata
# GA tags only. A Pages deploy replaces the WHOLE published site, and
# generate-release-metadata.mjs writes cli/desktop latest.json (the CLI
# self-upgrade + desktop-updater pointers) plus releases.json from the
# published version. A prerelease (v*-rcN, prerelease == 'true') would
# move those to the rc and every GA install would self-upgrade onto it, so
# the /dl metadata + Pages deploy stay GA-only. The downstream Docker
# publisher mirrors this for `latest`; rc builds still ship as GitHub
# Release assets.
# collect-release-assets --latest-count 5 keeps the just-published release
# plus the 4 previous GA releases in /dl, so `chan upgrade --version
# X.Y.Z` keeps resolving older GA versions after newer releases ship.
if: needs.context.outputs.publish == 'true' && needs.context.outputs.prerelease == 'false'
needs:
- context
- publish-release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
path: chan
- uses: actions/setup-node@v6
with:
node-version: '20'
- name: Install web workspace deps
run: npm install
working-directory: chan/web
- name: Build site and release metadata
working-directory: chan/web/packages/marketing
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.context.outputs.tag }}
run: |
set -e
npm run build
node scripts/collect-release-assets.mjs --tag "$TAG" --latest-count 5 --out /var/tmp/chan-release-assets.json
node scripts/generate-release-metadata.mjs --manifest /var/tmp/chan-release-assets.json --out dist/dl
find dist/dl -type f | sort
- uses: actions/upload-pages-artifact@v5
with:
path: chan/web/packages/marketing/dist
deploy-pages:
name: deploy Pages
# GA-only, matching pages-artifact: never redeploy the site for a
# prerelease (a full-site replace would move /dl latest.json onto the rc).
if: needs.context.outputs.publish == 'true' && needs.context.outputs.prerelease == 'false'
needs:
- context
- pages-artifact
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v5