Skip to content

release: bump MarkPad and MarkPDF CLI to 0.1.2 #47

release: bump MarkPad and MarkPDF CLI to 0.1.2

release: bump MarkPad and MarkPDF CLI to 0.1.2 #47

Workflow file for this run

name: Desktop Windows Build
on:
push:
branches:
- "**"
tags:
- "v*"
workflow_dispatch:
jobs:
build-windows:
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
include:
- arch: x64
rust_target: x86_64-pc-windows-msvc
libsql_pkg: '@libsql/win32-x64-msvc'
node_arch: x64
pandoc_asset: pandoc-3.6.4-windows-x86_64.zip
typst_asset: typst-x86_64-pc-windows-msvc.zip
- arch: arm64
rust_target: aarch64-pc-windows-msvc
libsql_pkg: '@libsql/win32-x64-msvc'
node_arch: x64
pandoc_asset: pandoc-3.6.4-windows-arm64.zip
typst_asset: typst-aarch64-pc-windows-msvc.zip
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Setup pnpm
uses: pnpm/action-setup@v5
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 24
cache: pnpm
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.rust_target }}
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: artifacts/artifacts/markpad/src-tauri -> target
- name: Install workspace dependencies
run: pnpm install --no-frozen-lockfile --force
- name: Ensure libsql Windows native module
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$pkg = "${{ matrix.libsql_pkg }}"
$ver = "0.5.29"
$resolved = ""
try {
$resolved = node -e "console.log(require.resolve('$pkg/package.json'))"
} catch {
}
if (-not $resolved) {
Write-Host "$pkg missing; installing deterministic fallback version $ver"
pnpm add -Dw "$pkg@$ver" --save-exact
try {
$resolved = node -e "console.log(require.resolve('$pkg/package.json'))"
} catch {
$resolved = ""
}
}
if (-not $resolved) {
throw "Unable to resolve $pkg after installation"
}
Write-Host "Resolved $pkg at $resolved"
- name: Validate frontend toolchain by loading Vite config
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
pnpm --filter @workspace/markpad exec node --input-type=module -e "import { loadConfigFromFile } from 'vite'; const loaded = await loadConfigFromFile({ command: 'build', mode: 'production' }, 'vite.config.ts'); if (!loaded) { throw new Error('Vite config did not load'); } console.log('vite config load OK');"
- name: Typecheck workspace
run: pnpm run typecheck
- name: Build API server bundle
run: pnpm --filter @workspace/api-server run build
- name: Build desktop frontend bundle
run: pnpm --filter @workspace/markpad run build:desktop:web
- name: Stage desktop backend resources
run: pnpm --filter @workspace/markpad run prepare:desktop:resources
- name: Download and stage runtime binaries
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$resourceRoot = "artifacts/artifacts/markpad/src-tauri/resources"
New-Item -ItemType Directory -Force -Path "$resourceRoot/runtime" | Out-Null
New-Item -ItemType Directory -Force -Path "$resourceRoot/bin" | Out-Null
$nodeVersion = (node -p "process.version").Trim().TrimStart("v")
$nodeArch = "${{ matrix.node_arch }}"
$nodeUrl = "https://nodejs.org/dist/v$nodeVersion/node-v$nodeVersion-win-$nodeArch.zip"
$nodeZip = "$env:RUNNER_TEMP/node-$nodeArch.zip"
Invoke-WebRequest $nodeUrl -OutFile $nodeZip
Expand-Archive -Path $nodeZip -DestinationPath "$env:RUNNER_TEMP/node-$nodeArch" -Force
$nodeDir = Get-ChildItem "$env:RUNNER_TEMP/node-$nodeArch" -Directory | Where-Object { $_.Name -like "node-v*-win-$nodeArch" } | Select-Object -First 1
if (-not $nodeDir) { throw "Node runtime directory not found for $nodeArch" }
Copy-Item $nodeDir.FullName "$resourceRoot/runtime/node" -Recurse -Force
function Download-WithFallback {
param(
[string]$PrimaryUrl,
[string]$FallbackUrl,
[string]$OutFile
)
try {
Invoke-WebRequest $PrimaryUrl -OutFile $OutFile
} catch {
if (-not $FallbackUrl) {
throw
}
Write-Warning "Primary download failed: $PrimaryUrl"
Write-Host "Falling back to: $FallbackUrl"
Invoke-WebRequest $FallbackUrl -OutFile $OutFile
}
}
$pandocZip = "$env:RUNNER_TEMP/pandoc.zip"
$pandocPrimary = "https://github.com/jgm/pandoc/releases/download/3.6.4/${{ matrix.pandoc_asset }}"
$pandocFallback = "https://github.com/jgm/pandoc/releases/download/3.6.4/pandoc-3.6.4-windows-x86_64.zip"
Download-WithFallback -PrimaryUrl $pandocPrimary -FallbackUrl $pandocFallback -OutFile $pandocZip
Expand-Archive -Path $pandocZip -DestinationPath "$env:RUNNER_TEMP/pandoc" -Force
$pandocExe = Get-ChildItem "$env:RUNNER_TEMP/pandoc" -Recurse -Filter pandoc.exe | Select-Object -First 1
if (-not $pandocExe) { throw "pandoc.exe not found in archive" }
Copy-Item $pandocExe.FullName "$resourceRoot/bin/pandoc.exe" -Force
$typstZip = "$env:RUNNER_TEMP/typst.zip"
$typstPrimary = "https://github.com/typst/typst/releases/download/v0.13.1/${{ matrix.typst_asset }}"
$typstFallback = "https://github.com/typst/typst/releases/download/v0.13.1/typst-x86_64-pc-windows-msvc.zip"
Download-WithFallback -PrimaryUrl $typstPrimary -FallbackUrl $typstFallback -OutFile $typstZip
Expand-Archive -Path $typstZip -DestinationPath "$env:RUNNER_TEMP/typst" -Force
$typstExe = Get-ChildItem "$env:RUNNER_TEMP/typst" -Recurse -Filter typst.exe | Select-Object -First 1
if (-not $typstExe) { throw "typst.exe not found in archive" }
Copy-Item $typstExe.FullName "$resourceRoot/bin/typst.exe" -Force
- name: Smoke test backend health + WebSocket preview
if: matrix.arch == 'x64'
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$resourceRoot = "artifacts/artifacts/markpad/src-tauri/resources"
$nodeExe = Join-Path $resourceRoot "runtime/node/node.exe"
$pandocExe = Join-Path $resourceRoot "bin/pandoc.exe"
$typstExe = Join-Path $resourceRoot "bin/typst.exe"
$backendEntry = "artifacts/artifacts/markpad/src-tauri/resources/backend/dist/index.mjs"
$dataDir = Join-Path $env:RUNNER_TEMP "markpad-data"
New-Item -ItemType Directory -Force -Path $dataDir | Out-Null
if (-not (Test-Path $pandocExe)) {
throw "pandoc executable missing at $pandocExe"
}
if (-not (Test-Path $typstExe)) {
throw "typst executable missing at $typstExe"
}
& $pandocExe --version | Select-Object -First 1 | Write-Host
& $typstExe --version | Write-Host
$server = $null
try {
$envMap = @{
PORT = "18080"
NODE_ENV = "production"
MARKPAD_DATA_DIR = $dataDir
MARKPAD_PANDOC_BIN = $pandocExe
MARKPAD_TYPST_BIN = $typstExe
PATH = "$resourceRoot/bin;$resourceRoot/runtime/node;$env:PATH"
}
$server = Start-Process -FilePath $nodeExe -ArgumentList $backendEntry -WorkingDirectory $env:GITHUB_WORKSPACE -Environment $envMap -PassThru
$deadline = (Get-Date).AddSeconds(45)
$healthy = $false
while ((Get-Date) -lt $deadline) {
try {
$resp = Invoke-RestMethod "http://127.0.0.1:18080/api/healthz"
if ($resp.status -eq "ok") {
$healthy = $true
break
}
} catch {
}
Start-Sleep -Milliseconds 400
}
if (-not $healthy) {
throw "Backend did not become healthy in time"
}
$wsScript = @(
"const timeout = setTimeout(() => {"
" console.error('preview websocket timed out');"
" process.exit(1);"
"}, 20000);"
""
"const ws = new WebSocket('ws://127.0.0.1:18080/api/ws/preview');"
""
"ws.onopen = () => {"
" ws.send(JSON.stringify({"
" projectId: 1,"
" content: '# CI smoke test\\n\\nWebSocket preview should return a PDF.',"
" options: {"
" pageSize: 'a4',"
" documentFont: 'latin-modern',"
" fontSizePt: 11,"
" lineStretch: 1.1,"
" },"
" }));"
"};"
""
"ws.onmessage = async (event) => {"
" if (event.data instanceof Blob) {"
" const buf = Buffer.from(await event.data.arrayBuffer());"
" if (buf.length < 100) {"
" console.error('preview pdf payload too small');"
" process.exit(1);"
" }"
" clearTimeout(timeout);"
" ws.close();"
" process.exit(0);"
" }"
""
" console.error('unexpected preview response', String(event.data));"
" process.exit(1);"
"};"
""
"ws.onerror = () => {"
" console.error('preview websocket error');"
" process.exit(1);"
"};"
) -join "`n"
& $nodeExe -e $wsScript
} finally {
if ($server -and -not $server.HasExited) {
Stop-Process -Id $server.Id -Force
}
}
- name: Validate staged Tauri resources
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$resourceRoot = "artifacts/artifacts/markpad/src-tauri/resources"
$requiredDirs = @("backend", "runtime", "bin")
foreach ($dir in $requiredDirs) {
$path = Join-Path $resourceRoot $dir
if (-not (Test-Path $path)) {
throw "Missing required resource directory: $path"
}
$fileCount = (Get-ChildItem -Path $path -Recurse -File -ErrorAction Stop | Measure-Object).Count
if ($fileCount -lt 1) {
throw "Resource directory is empty: $path"
}
}
Write-Host "Tauri resource validation passed"
- name: Build NSIS installer (.exe)
run: pnpm --filter @workspace/markpad exec tauri build --config src-tauri/tauri.conf.json --bundles nsis --target ${{ matrix.rust_target }}
- name: Upload installer artifacts
uses: actions/upload-artifact@v7
with:
name: markpad-windows-installer-${{ matrix.arch }}-${{ github.sha }}
path: |
artifacts/artifacts/markpad/src-tauri/target/${{ matrix.rust_target }}/release/bundle/nsis/*.exe
artifacts/artifacts/markpad/src-tauri/target/${{ matrix.rust_target }}/release/bundle/nsis/*.zip
if-no-files-found: error
- name: Publish GitHub Release (tags only)
if: startsWith(github.ref, 'refs/tags/v')
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
generate_release_notes: true
files: |
artifacts/artifacts/markpad/src-tauri/target/${{ matrix.rust_target }}/release/bundle/nsis/*.exe
fail_on_unmatched_files: true