release: bump MarkPad and MarkPDF CLI to 0.1.2 #47
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |