Skip to content

Continuous Integration

Franciszek Ryszka edited this page Aug 9, 2026 · 5 revisions

Continuous Integration

SnipVault ships with three CI pipeline definitions. All build the same installers; pick whichever matches where you host.

File System Builds
.github/workflows/release.yml GitHub Actions (hosted) Windows + macOS (Intel & Apple Silicon)
.gitlab-ci.yml GitLab CI (self-hosted) Windows + macOS (universal) + Linux, plus a GitLab Release
Jenkinsfile Jenkins (self-hosted) Windows + macOS (universal) + Linux in parallel

The macOS constraint applies everywhere: Tauri cannot cross-compile, so a macOS .dmg must be built on real macOS hardware, a Windows installer on Windows, and Linux bundles on Linux. CI just orchestrates one machine per platform.

GitHub Actions

.github/workflows/release.yml runs on version tags (v*) and manual dispatch. It has three jobs:

  1. create-release — reads the version from tauri.conf.json and creates a single draft release for v<version>, outputting its release id.
  2. build — a matrix that compiles each platform and uploads its bundles to that shared release id:
    • windows-latest → .msi + .exe
    • macos-latest with --target aarch64-apple-darwin → Apple Silicon .dmg
    • macos-latest with --target x86_64-apple-darwin → Intel .dmg
  3. publish — un-drafts the release and marks it latest.

The build matrix uses pnpm/action-setup, Node 22 (pnpm 11 needs ≥ 22.13), dtolnay/rust-toolchain, a Rust cache, then tauri-apps/tauri-action. Each build job receives the updater signing secrets (below), so it signs the update artifacts and contributes to a latest.json manifest that the in-app updater reads.

Why create the release first? If the matrix jobs each let tauri-action create the release, they race — GitHub's "find release by tag" lookup ignores drafts, so every parallel job spawns its own draft, splitting the installers across releases and leaving each latest.json missing the other platforms. Creating one draft up front and passing its releaseId to every job makes all platforms land on the same release, so latest.json merges correctly. The final publish job then un-drafts it — no manual step needed.

Release:

git tag v1.2.0 && git push --follow-tags
# or trigger manually:
gh workflow run release.yml --ref main

The workflow publishes the release automatically once all platforms build; there's no manual "un-draft" step.

Server image (GHCR) (added in v2.2.0)

A separate workflow, .github/workflows/docker-image.yml, builds and publishes the sync server container (the SnipVault web app; see Syncing) to the GitHub Container Registry at ghcr.io/franciszekryszka/snippet-vault.

  • It runs on version tags (v*) and manual dispatch, using docker/build-push-action + docker/metadata-action.
  • A real tag push mints both :X.Y.Z (via type=semver) and :latest; a workflow_dispatch from main only updates :latest.
  • The image name is lowercased (registries require it). Package visibility is set once in the GitHub web UI — there's no REST/CLI endpoint for it.

This is independent of the desktop installer build above — a tag push triggers both workflows.

Automated tests (added in v2.4.0)

.github/workflows/test.yml runs the JS/TS test suites on every push to main and every pull request — separate from the installer builds above, which only run on tags. Two jobs:

  • unit — vitest over the pure API validators and the prompt-variable parser in lib/ (fast; no server or database).
  • api — builds the server bundle, boots it on a throwaway SQLite database (via the SNIPVAULT_DB_PATH override), and runs a dependency-free node:test HTTP integration suite against it: the auth gate, CRUD, the newest-wins sync merge, Trash, and library round-trips.

Run both locally with pnpm test (or pnpm test:unit / pnpm test:api individually). These complement the Rust tests (cargo test in src-tauri/) and the dependency Security audit workflow (.github/workflows/audit.yml — pnpm audit + cargo audit, which waives build-time-only transitive advisories via pnpm-workspace.yaml and still fails on runtime-reachable ones).

Self-hosted: GitLab & Jenkins

Both target three machines, each registered as a runner/agent with a tag/label:

Tag / label Machine role Output
windows Windows host .msi + .exe
macos macOS host universal .dmg (Intel + Apple Silicon)
linux Linux host .deb / .AppImage / .rpm

Every runner needs Node ≥ 22 and Rust installed, plus its OS's Tauri prerequisites (see Development Setup). The macOS runner builds a universal binary via rustup target add aarch64-apple-darwin x86_64-apple-darwin + --target universal-apple-darwin.

GitLab specifics

  • Build jobs run on tags and manual ("web") pipelines; broaden the .base rules to build on every push.
  • On a tag, each build job uploads its installer to the project's generic package registry and records an asset-link line.
  • A final release job (on the linux runner) downloads release-cli and creates a GitLab Release linking every installer.
  • If your Linux runner has no outbound internet, pre-install release-cli and replace the download step with its path.
  • Validate edits with CI/CD → Editor → Lint in your GitLab project.

Jenkins specifics

  • A declarative pipeline with a parallel block — Windows (bat), macOS + Linux (sh).
  • Installers are archived per node via archiveArtifacts.
  • Extend the post { success { … } } blocks to publish to a release target if desired.

Code signing

Published binaries are unsigned, so end users see first-launch warnings (see Installation). To sign:

  • macOS — an Apple Developer ID certificate + notarization. Tauri supports signing env vars in the build step.
  • Windows — an OV/EV code-signing certificate.

Store certificates as CI secrets and wire them into the respective build step. This is optional and only affects the OS trust prompts, not functionality.

Update signing (auto-updater)

Separate from the OS code signing above, the in-app auto-updater (added in v1.4.0) requires its own signature so the app only accepts genuine updates. This is a free Tauri/minisign keypair, unrelated to Apple/Windows certificates.

  • Public key lives in src-tauri/tauri.conf.json under plugins.updater.pubkey; the updater endpoint points at the release's latest.json.
  • Private key + password are stored as the GitHub secrets TAURI_SIGNING_PRIVATE_KEY and TAURI_SIGNING_PRIVATE_KEY_PASSWORD, passed to tauri-action in release.yml.
  • bundle.createUpdaterArtifacts is enabled so signed update bundles + latest.json are produced and uploaded to the Release.
  • Generate a keypair with pnpm tauri signer generate. Keep the private key safe — if it's lost, no future release can be signed and already-installed apps (which trust the old public key) can no longer auto-update.
  • To reproduce this on the self-hosted GitLab/Jenkins pipelines, expose the same two variables to the build job.

Clone this wiki locally