app-release #2
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
| # Release pipeline for the Rust MarkItDown suite (app/): the standalone CLI | |
| # binary, the MCP server binary, and the Tauri desktop installers — built | |
| # NATIVELY on each OS/arch (no cross-compilation, so what we ship is what the | |
| # runner actually compiled and ran). | |
| # | |
| # Coverage: | |
| # Linux x86_64 (ubuntu-22.04) | |
| # Linux aarch64 (ubuntu-24.04-arm — GitHub-hosted Arm64 runner) | |
| # Windows x86_64 (windows-latest) | |
| # macOS x86_64 Intel (macos-13) | |
| # macOS aarch64 Apple Silicon (macos-14) | |
| # | |
| # Trigger: push a tag like `app-v0.1.0` (kept distinct from the Python | |
| # package's own tags), or run manually via workflow_dispatch (which produces | |
| # downloadable artifacts but does not create a GitHub Release). | |
| # | |
| # GitHub-runner limitations handled (context-aware): | |
| # * Tests run BEFORE building, and a headless smoke test runs AFTER, so a | |
| # platform only publishes artifacts that have been proven to start and | |
| # convert a real fixture on that exact OS/arch. | |
| # * No display server: the GUI is never launched — only built. The desktop | |
| # unit tests cover the command layer. | |
| # * No network / no Python engine needed: all tests are offline; the | |
| # python_fallback suite is cfg(unix) + shell stubs. | |
| # * macOS builds are unsigned (no certs in CI). They run locally after a | |
| # Gatekeeper override; signing/notarization can be added later via secrets. | |
| # * Arm64 Linux AppImage tooling is flaky on CI, so that arch ships a .deb | |
| # only; x86_64 Linux ships both .deb and AppImage. | |
| name: app-release | |
| on: | |
| push: | |
| # Match the common release-tag forms so a normal `vX.Y.Z` works too, not | |
| # only the `app-v*` prefix. IMPORTANT: a tag only triggers a workflow if | |
| # the file exists in the *commit the tag points to* — create the tag on a | |
| # commit that already contains this file (push the workflow first, then tag). | |
| tags: | |
| - "app-v*" | |
| - "v*" | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: "Tag/name for the Release published by a manual run (a prerelease is created)." | |
| required: false | |
| default: "app-dev" | |
| concurrency: | |
| group: app-release-${{ github.ref }} | |
| cancel-in-progress: false | |
| permissions: | |
| contents: write # needed to create/update the GitHub Release | |
| defaults: | |
| run: | |
| shell: bash | |
| jobs: | |
| build: | |
| name: build (${{ matrix.label }}) | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # `compress: true` UPX-packs the standalone CLI/MCP binaries. | |
| # macOS is excluded: UPX's Mach-O / Apple-Silicon support is | |
| # unreliable and packed binaries are killed by Gatekeeper, so | |
| # compressing them would regress "it must run". Linux/Windows pack | |
| # cleanly and we re-run the smoke test on the packed binary. | |
| - os: ubuntu-22.04 | |
| label: linux-x86_64 | |
| target: x86_64-unknown-linux-gnu | |
| bundles: deb,appimage | |
| compress: true | |
| - os: ubuntu-24.04-arm | |
| label: linux-aarch64 | |
| target: aarch64-unknown-linux-gnu | |
| bundles: deb | |
| compress: true | |
| - os: windows-latest | |
| label: windows-x86_64 | |
| target: x86_64-pc-windows-msvc | |
| bundles: msi,nsis | |
| compress: true | |
| # macOS x86_64 (Intel artifacts): CROSS-COMPILED on the Apple | |
| # Silicon runner instead of the Intel `macos-13` runner. Intel | |
| # macOS hosted runners are scarce / being phased down and jobs can | |
| # sit queued for hours; macos-14 (arm64) is plentiful. We install | |
| # Rosetta 2 so the x86_64 binary still actually runs in the smoke | |
| # test. dmg/app packaging is arch-agnostic (hdiutil runs natively). | |
| - os: macos-14 | |
| label: macos-x86_64 | |
| target: x86_64-apple-darwin | |
| bundles: app,dmg | |
| compress: false | |
| rosetta: true | |
| - os: macos-14 | |
| label: macos-aarch64 | |
| target: aarch64-apple-darwin | |
| bundles: app,dmg | |
| compress: false | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install Tauri system dependencies (Linux only) | |
| if: runner.os == 'Linux' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y \ | |
| libwebkit2gtk-4.1-dev build-essential curl wget file \ | |
| libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev | |
| - name: Install Rust (stable) | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ matrix.target }} | |
| - name: Cache Rust build | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: | | |
| app | |
| app/desktop/src-tauri | |
| key: ${{ matrix.target }} | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: npm | |
| cache-dependency-path: app/desktop/package-lock.json | |
| # ---- gate: prove the code works on THIS os/arch before shipping ------ | |
| - name: Test core / CLI / MCP workspace | |
| working-directory: app | |
| run: cargo test --workspace --locked | |
| - name: Test desktop backend | |
| working-directory: app/desktop/src-tauri | |
| run: cargo test --locked | |
| # ---- build standalone CLI + MCP (release, native target) ------------ | |
| - name: Build CLI + MCP (release) | |
| working-directory: app | |
| run: cargo build --release --locked --target ${{ matrix.target }} -p markitdown-cli -p markitdown-mcp | |
| # ---- compress the standalone binaries (UPX) ------------------------- | |
| # The release profile already does LTO + strip + opt-level="s". UPX | |
| # --best --lzma roughly halves the on-disk size on top of that. It | |
| # self-extracts into memory at launch (a few ms; fine for a CLI / a | |
| # long-lived MCP server). Skipped on macOS (see matrix note). | |
| - name: Install UPX | |
| if: matrix.compress && runner.os == 'Linux' | |
| run: sudo apt-get update && sudo apt-get install -y upx-ucl | |
| - name: Install UPX (Windows) | |
| if: matrix.compress && runner.os == 'Windows' | |
| run: choco install upx -y | |
| - name: Compress binaries (UPX --best --lzma) | |
| if: matrix.compress | |
| run: | | |
| set -euo pipefail | |
| EXE=""; [[ "$RUNNER_OS" == "Windows" ]] && EXE=".exe" | |
| BIN="app/target/${{ matrix.target }}/release" | |
| for b in markitdown markitdown-mcp; do | |
| before=$(wc -c < "$BIN/$b$EXE") | |
| upx --best --lzma "$BIN/$b$EXE" | |
| after=$(wc -c < "$BIN/$b$EXE") | |
| echo "$b: $before -> $after bytes ($(( 100 - after * 100 / before ))% smaller)" | |
| done | |
| # x86_64 binaries are cross-built on the Apple Silicon runner; Rosetta 2 | |
| # lets us actually run them in the smoke test below. | |
| - name: Install Rosetta 2 | |
| if: matrix.rosetta | |
| run: softwareupdate --install-rosetta --agree-to-license | |
| # Smoke test runs AFTER compression, so it validates exactly the bytes | |
| # that ship — a packed binary that can't start fails the release here. | |
| - name: Smoke test released binaries | |
| run: | | |
| set -euo pipefail | |
| EXE=""; [[ "$RUNNER_OS" == "Windows" ]] && EXE=".exe" | |
| BIN="app/target/${{ matrix.target }}/release" | |
| # Capture to files before grepping: under `set -o pipefail` a | |
| # `cli | grep -q` pipeline fails when grep closes the pipe early. | |
| "$BIN/markitdown$EXE" --version | |
| "$BIN/markitdown$EXE" --list-formats > formats.txt | |
| grep -qi PDF formats.txt | |
| "$BIN/markitdown$EXE" packages/markitdown/tests/test_files/test.docx > docx.txt | |
| grep -q AutoGen docx.txt | |
| printf '%s\n%s\n%s\n' \ | |
| '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"ci","version":"0"}}}' \ | |
| '{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}' \ | |
| '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \ | |
| > mcp_req.txt | |
| "$BIN/markitdown-mcp$EXE" < mcp_req.txt > mcp_resp.txt | |
| grep -q convert_to_markdown mcp_resp.txt | |
| echo "binary smoke OK on ${{ matrix.label }}" | |
| # ---- install frontend deps + build the Tauri desktop installers ----- | |
| - name: Install frontend dependencies | |
| working-directory: app/desktop | |
| run: npm ci | |
| - name: Build desktop app (Tauri) | |
| working-directory: app/desktop | |
| # beforeBuildCommand in tauri.conf.json runs `npm run build` (the | |
| # frontend) automatically. Unsigned on macOS (no CI certs). | |
| run: npm run tauri build -- --target ${{ matrix.target }} --bundles ${{ matrix.bundles }} | |
| # ---- collect everything into out/ ----------------------------------- | |
| - name: Package artifacts | |
| run: | | |
| set -euo pipefail | |
| TRIPLE="${{ matrix.target }}" | |
| LABEL="${{ matrix.label }}" | |
| EXE=""; [[ "$RUNNER_OS" == "Windows" ]] && EXE=".exe" | |
| mkdir -p out staging | |
| cp "app/target/$TRIPLE/release/markitdown$EXE" staging/ | |
| cp "app/target/$TRIPLE/release/markitdown-mcp$EXE" staging/ | |
| ( | |
| cd staging | |
| if [[ "$RUNNER_OS" == "Windows" ]]; then | |
| 7z a "../out/markitdown-cli-mcp-$LABEL.zip" "markitdown$EXE" "markitdown-mcp$EXE" | |
| else | |
| tar czf "../out/markitdown-cli-mcp-$LABEL.tar.gz" markitdown markitdown-mcp | |
| fi | |
| ) | |
| # Desktop installers (only the types that exist for this platform). | |
| BUNDLE="app/desktop/src-tauri/target/$TRIPLE/release/bundle" | |
| echo "::group::bundle tree for $LABEL" | |
| [[ -d "$BUNDLE" ]] && find "$BUNDLE" | sort || echo "NO BUNDLE DIR at $BUNDLE" | |
| echo "::endgroup::" | |
| installers=0 | |
| if [[ -d "$BUNDLE" ]]; then | |
| while IFS= read -r -d '' f; do | |
| cp "$f" out/ && installers=$((installers + 1)) | |
| done < <(find "$BUNDLE" -type f \ | |
| \( -name '*.dmg' -o -name '*.AppImage' -o -name '*.deb' \ | |
| -o -name '*.rpm' -o -name '*.msi' -o -name '*.exe' \) -print0) | |
| fi | |
| # macOS: also ship a zipped, ready-to-run .app — guaranteed even if | |
| # the .dmg styling step is flaky on a headless runner. | |
| if [[ "$RUNNER_OS" == "macOS" ]]; then | |
| APP=$(find "$BUNDLE/macos" -maxdepth 1 -name '*.app' -type d 2>/dev/null | head -1 || true) | |
| if [[ -n "$APP" ]]; then | |
| ditto -c -k --keepParent "$APP" "out/MarkItDown-$LABEL.app.zip" | |
| installers=$((installers + 1)) | |
| fi | |
| fi | |
| echo "--- packaged for $LABEL ($installers desktop artifact(s)) ---" | |
| ls -la out | |
| # Fail loudly rather than silently shipping binaries with no app. | |
| if [[ "$installers" -eq 0 ]]; then | |
| echo "::error::no desktop installer/app was produced for $LABEL (check the Build desktop app step)" | |
| exit 1 | |
| fi | |
| - name: Upload artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: markitdown-${{ matrix.label }} | |
| path: out/* | |
| if-no-files-found: error | |
| # Gather every platform's artifacts and publish them as ONE GitHub Release. | |
| # Runs both for `app-v*` tag pushes (normal release) AND for manual | |
| # workflow_dispatch runs (publishes a prerelease named by the `tag` input), | |
| # so the .dmg / installers / binaries are always visible on the Releases | |
| # page — not buried under the Actions run's Artifacts. | |
| release: | |
| name: publish release | |
| needs: build | |
| if: >- | |
| always() && needs.build.result == 'success' && | |
| (startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Download all build artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: dist | |
| merge-multiple: true | |
| - name: List + checksum everything being released | |
| run: | | |
| set -euo pipefail | |
| cd dist | |
| echo "=== assets to publish ===" | |
| ls -la | |
| sha256sum * > SHA256SUMS.txt | |
| cat SHA256SUMS.txt | |
| - name: Resolve release tag | |
| id: rel | |
| run: | | |
| if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then | |
| echo "tag=${{ inputs.tag }}" >> "$GITHUB_OUTPUT" | |
| echo "prerelease=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "tag=${{ github.ref_name }}" >> "$GITHUB_OUTPUT" | |
| echo "prerelease=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Create / update GitHub Release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ steps.rel.outputs.tag }} | |
| prerelease: ${{ steps.rel.outputs.prerelease }} | |
| files: dist/* | |
| generate_release_notes: true | |
| fail_on_unmatched_files: true | |
| body: | | |
| ## MarkItDown (Rust suite) — ${{ steps.rel.outputs.tag }} | |
| Native builds for Linux (x86_64 / aarch64), Windows (x86_64) and | |
| macOS (Intel / Apple Silicon). Each artifact was tested and | |
| smoke-checked on its own OS/arch runner before release. | |
| **Standalone binaries** (no dependencies): | |
| - `markitdown-cli-mcp-linux-x86_64.tar.gz` | |
| - `markitdown-cli-mcp-linux-aarch64.tar.gz` | |
| - `markitdown-cli-mcp-windows-x86_64.zip` | |
| - `markitdown-cli-mcp-macos-x86_64.tar.gz` (Intel) | |
| - `markitdown-cli-mcp-macos-aarch64.tar.gz` (Apple Silicon) | |
| **Desktop app installers:** | |
| - macOS: `*.dmg` + `MarkItDown-macos-*.app.zip` (Intel & Apple Silicon) | |
| - Linux: `*.deb` (x86_64 & aarch64) + `*.AppImage` (x86_64) | |
| - Windows: `*.msi` + `*-setup.exe` | |
| - `SHA256SUMS.txt` — checksums for every asset. | |
| macOS builds are unsigned; right-click → Open (or | |
| `xattr -dr com.apple.quarantine <app>`) on first launch. |