This document describes the CI/CD pipelines for the Open Data Ensemble (ODE) monorepo.
The ODE monorepo uses GitHub Actions for continuous integration and deployment. Each project has its own pipeline that triggers only when relevant files change.
Workflow File: .github/workflows/synkronus-docker.yml
- Push to
main: Builds and publishes themainbranch image - Push to
dev: Builds and publishes thedevbranch image - Published GitHub Release: Publishes stable or pre-release version tags and their corresponding moving pointer
- Pull Requests: Builds but does not publish (validation only)
- Manual Dispatch: Publishes only a commit-specific
sha-{short}tag
The workflow only runs when files in these paths change:
synkronus/**- Any file in the Synkronus project.github/workflows/synkronus-docker.yml- The workflow itself
Images are published to GitHub Container Registry (GHCR):
- Registry:
ghcr.io - Image:
ghcr.io/opendataensemble/synkronus
Image tags are computed by docker/metadata-action. The highest-priority tag per event is also used as the primary tag in manifest verification.
| Event | Tags produced | Published? |
|---|---|---|
| Release published, not prerelease | v{X.Y.Z}, v{X.Y}, v{X}, latest |
Yes |
| Release published, is prerelease | v{X.Y.Z}-{pre}, latest-pre-release |
Yes |
Push → main |
main, sha-{short} |
Yes |
Push → dev |
dev, sha-{short} |
Yes |
workflow_dispatch |
sha-{short} |
Yes |
| Pull request | pr-{number} |
No (build only) |
Choose a deployment tag based on the update channel you want:
| Tag | What it tracks | Recommended use |
|---|---|---|
latest |
Most recently published stable GitHub Release | Production deployments that should auto-update between stable releases |
latest-pre-release |
Most recently published GitHub Release marked pre-release | Demo/staging deployments and Watchtower-managed pre-release testing |
dev |
Tip of the dev branch |
Bleeding-edge integration testing; may contain unpublished work |
main |
Tip of the main branch |
Testing current main between releases |
v{X.Y.Z}-{pre} |
One specific pre-release, such as v1.2.3-alpha.4 |
Reproducible pre-release deployment; does not auto-update |
v{X.Y.Z} |
One specific stable release, such as v1.2.3 |
Reproducible production deployment; does not auto-update |
sha-{short} |
One specific commit | Debugging or exact-build reproduction; does not auto-update |
dev is a branch-head channel, not the published pre-release channel. Publishing a stable release updates latest but does not update latest-pre-release; there is no single tag that tracks the newest release regardless of whether it is stable or pre-release.
Versioned and moving release tags are produced only when a GitHub Release is published. Merely pushing a Git tag is not enough, and the release must be marked as a pre-release for latest-pre-release to move.
workflow_dispatch intentionally produces only sha-{short} so that manual runs cannot accidentally reassign latest, main, dev, or any other pointer tag. The workflow does not currently publish feature-branch tags: automatic push builds are limited to main and dev, while pull requests build without publishing.
Version stamping of non-release builds.
dev,main, and feature-branch builds bake agit describe --tags --alwaysversion (e.g.v1.3.0-7-gabc1234) into the binary via ldflags, so the server/versionendpoint reports a version clearly ahead of the last release. Previously these builds usedgit describe --abbrev=0, which stamped them with the previous release tag — the reason a branch-tracking demo server appeared to run "exactly one version behind" even when the image content was current.
- Multi-platform: Builds for
linux/amd64andlinux/arm64using Buildah - Attestation: Generates SLSA build provenance and pushes it to GHCR
- Metadata: Includes OCI-compliant labels (title, description, vendor, source, revision)
- Verification: After push, the manifest list and per-arch layers are pulled to confirm correctness
The workflow requires these permissions:
contents: read- To checkout the repositorypackages: write- To publish to GHCRid-token: write- For OIDC-based build provenance attestationattestations: write- To push attestation records to GHCR
GITHUB_TOKEN- Automatically provided by GitHub Actions
Workflow File: .github/workflows/synkronus-cli-docker.yml
- Push to
main/devwhensynkronus-cli/**orDockerfile.cli.slimchanges - Pull Requests — build only, no publish
- Release published — versioned tags
- Manual dispatch
- Image:
ghcr.io/opendataensemble/synkronus-cli - Dockerfile:
Dockerfile.cli.slim(pre-built staticsynkbinary, Alpine 3.23)
Tagging follows the same strategy as the Synkronus server image (latest, dev, main, v{X.Y.Z}, sha-{short}, etc.).
Workflow File: .github/workflows/synkronus-cli.yml
Release assets: synkronus-cli-{os}-{arch}.tar.gz attached to GitHub Releases. Custom app CI should pin a release tag via SYNK_CLI_VERSION and download synkronus-cli-linux-amd64.tar.gz.
CI matrix: pull requests build linux-amd64 only; push to main/dev builds all six OS/arch pairs; release attaches all six to the GitHub Release.
Non-interactive CI login: synk login -u USER --password "$SYNK_PASSWORD" (v1.1.2+).
Workflow File: .github/workflows/formulus-android.yml
Builds Android APK for the Formulus React Native application, and builds/consumes Formplayer assets in a single, two‑job workflow.
- Push to
main/dev(formulus or formulus-formplayer changes): Builds Formplayer assets and then a release APK + AAB using those assets - Pull Requests (formulus or formulus-formplayer changes): Builds Formplayer assets and then a debug APK for validation
- Release: Publishes the universal APK and AAB to GitHub Release
The workflow runs when files in these paths change:
formulus/**- Any file in the formulus projectformulus-formplayer/**- Any file in the formulus-formplayer projectpackages/tokens/**- Shared design tokens and build inputs.github/workflows/formulus-android.yml- The workflow itself
The workflow intelligently handles formplayer assets using two jobs:
-
build-formplayer-assetsjob:- Builds
@ode/tokens - Builds Formplayer assets using
pnpm run build:copyinformulus-formplayer - Uploads the built assets from
formulus/android/app/src/main/assets/formplayer_dist/as a GitHub Actions artifact
- Builds
-
build-androidjob (depends on assets job):- Downloads the Formplayer assets artifact into
formulus/android/app/src/main/assets/formplayer_dist/ - Runs
pnpm run vendor:notifeeinformulus/to clone the pinned invertase/notifee commit intothird_party/notifee(gitignored; required for Gradle:notifee_core) - Builds the Android APK (debug for PRs, release for main/dev/release events)
- Downloads the Formplayer assets artifact into
Formplayer assets are not committed to git and are ignored via .gitignore. CI builds always use the assets artifact produced in the same workflow run, ensuring a single, consistent source of truth for each build.
- Pull Requests: Debug APK (unsigned), arm64-v8a only (smoke)
- Push to main/dev: Release ARM-universal APK + ARM AAB (signed) — CI artifacts
- GitHub Release: Release ARM-universal APK + ARM AAB (signed); assets attached to the release
APK and AAB are built in one Gradle invocation (assembleRelease bundleRelease) so Metro/native work is shared. GitHub and Play builds contain armeabi-v7a and arm64-v8a, covering physical ARM devices without carrying x86/x86_64 native libraries in direct downloads. GitHub release/distribution uses the single ARM-universal APK to avoid wrong-ABI selection and cross-ABI versionCode update failures. F-Droid keeps four per-ABI builds—including x86/x86_64—from the same source tag via metadata/build properties; that path is independent of the normal release ABI set. Gradle uses gradle/actions/setup-gradle (daemon left on). Vendored Notifee (formulus/third_party/notifee) is cached across runs (key = hash of vendor-notifee-core.mjs). CI enables parallel workers (local gradle.properties keeps them low for laptops).
NDK ccache is not wired yet: it needs CI-only CMake launcher hooks into React Native’s native build and can flake; revisit if release builds are still too slow after the above.
FORMULUS_RELEASE_KEYSTORE_B64- Base64 encoded keystore fileFORMULUS_RELEASE_STORE_PASSWORD- Keystore passwordFORMULUS_RELEASE_KEY_ALIAS- Key aliasFORMULUS_RELEASE_KEY_PASSWORD- Key password
Formplayer asset building and Android APK building are now handled within the same workflow:
Formplayer or Formulus Changes → build-formplayer-assets job → build-android job (consumes artifact) → APK artifact / Release upload
This ensures:
- No duplicate cross-workflow wiring
- A single workflow owns both asset and APK builds
- Each APK is built against the exact assets produced in the same run
- Formplayer build outputs do not pollute git history
Workflow: .github/workflows/sbom-release.yml
- Runs when a GitHub Release is published and uploads
*.cdx.jsonfiles to that release (alongside other assets such as the Formulus APK fromformulus-android.yml). - Manual test: Actions → SBOM (CycloneDX) → Run workflow; download the
cyclonedx-sbomartifact.
Local generation (requires Node + Go):
node scripts/sbom/generate-sboms.mjs --out sbom-distPoints to the most recent non-prerelease GitHub Release.
docker pull ghcr.io/opendataensemble/synkronus:latestPoints to the most recent prerelease GitHub Release (e.g. -alpha.N, -rc.N).
docker pull ghcr.io/opendataensemble/synkronus:latest-pre-releasedocker pull ghcr.io/opendataensemble/synkronus:v1.0.0Tracks the tip of the dev branch (rebuilt on every push to dev).
docker pull ghcr.io/opendataensemble/synkronus:devTracks the tip of the main branch between releases.
docker pull ghcr.io/opendataensemble/synkronus:mainFeature-branch images are not published automatically. To publish an exact commit for testing, manually dispatch the workflow from that branch and use the resulting sha-{short} tag.
Versioned images are produced by publishing a GitHub Release (the workflow listens for release: [published]).
- Go to Releases → Draft a new release
- Create a new tag following semver, prefixed with
v:- Stable release:
v1.0.0 - Pre-release:
v1.0.0-rc.1,v1.0.1-alpha.7, etc.
- Stable release:
- Select the target commit (typically tip of
mainfor stable, tip ofdevfor pre-release) - Tick Set as a pre-release for alpha/beta/rc tags
- Click Publish release
This will create:
| Release kind | Tags produced |
|---|---|
Stable (v1.0.0) |
v1.0.0, v1.0, v1, latest |
Pre-release (v1.0.0-rc.1) |
v1.0.0-rc.1, latest-pre-release |
Note:
workflow_dispatchis available for manual runs but intentionally does not create any pointer tags (onlysha-{short}) — it is for debugging the build pipeline, not for publishing releases.
By default, GHCR packages inherit the repository's visibility:
- Public repositories → Public images (no authentication needed)
- Private repositories → Private images (authentication required)
For private images:
echo $GITHUB_TOKEN | docker login ghcr.io -u USERNAME --password-stdin- Go to the Actions tab in GitHub
- Select Synkronus Docker Build & Publish
- View recent runs and their status
- Go to the repository main page
- Click Packages in the right sidebar
- Select synkronus
- View all published tags and their details
- Check the Actions tab for error logs
- Common issues:
- Dockerfile syntax errors
- Missing dependencies in build context
- Network issues during dependency download
- Verify the branch name matches the workflow triggers
- Check that the workflow has
packages: writepermission - Ensure the push event (not PR) triggered the workflow
- Verify the image tag exists in GHCR
- For private repos, ensure you're authenticated
- Check image name spelling:
ghcr.io/opendataensemble/synkronus
- Test locally first: Build and test Docker images locally before pushing
- Use feature branches: Create feature branches for experimental changes
- Review build logs: Check Actions logs even for successful builds
- Tag releases properly: Use semantic versioning for releases
- Pin versions in production: Use specific version tags (
v{X.Y.Z}), notlatest - Staging/QA upcoming releases: Use
latest-pre-releaseto track the most recent prerelease (alpha/rc) - Bleeding-edge integration: Use
devfor builds from the tip of thedevbranch - Monitor image sizes: Keep images lean for faster deployments
- Use health checks: Always configure health checks in deployments
The automated asset build process ensures Formulus Android builds always have matching Formplayer assets:
- Developer makes changes to
formulus-formplayer - Opens/updates PR (or pushes to
main/dev) → the Formulus Android workflow:- Runs the
build-formplayer-assetsjob to build Formplayer assets and upload them as an artifact - Runs the
build-androidjob, which downloads the artifact and builds the APK
- Runs the
- No manual work: Assets are automatically built and passed between jobs via artifacts
- No conflicts: Assets are not committed to git, avoiding noisy diffs and merge issues
- Always consistent: Each Android build uses the assets built in that same workflow run
- Clean repository: Built assets live only in CI artifacts and local workspaces, not in version control
For local development, you can manually build and copy assets:
cd formulus-formplayer
pnpm run build:copyThis will:
- Build the formplayer web app
- Clean existing formplayer asset folders in Formulus and copy new assets to Android and iOS paths
- Copy the same bundle to
desktop/public/formplayer_dist/for ODE Desktop
The copy-to-rn step run inside build:copy handles cleaning targets before copy, so no need to run clean-rn-assets separately for a normal refresh.
Workflow file: .github/workflows/ode-desktop.yml
- Pull requests and pushes to
main/devwhen relevant paths change (see below), or manual dispatch. release: published: packages installers and attaches them to the GitHub Release (same pattern asSynkronus CLI; no path filter).
For pull requests / pushes (main, dev), the workflow runs when any of these change:
desktop/**formulus-formplayer/**packages/tokens/**,packages/components/**(formplayer build inputs)formulus/src/webview/FormulusInterfaceDefinition.ts(formplayersync-interfacesource).github/workflows/ode-desktop.yml
Job desktop (not on release)
From desktop/: pnpm lint, pnpm format:check, pnpm test, pnpm typecheck, pnpm codegen:synk-client, then fails if desktop/src/generated drifts from the regenerated OpenAPI client. From desktop/src-tauri/: cargo fmt --check, cargo clippy -D warnings, cargo test.
Job desktop-formplayer-dist (bundling and release flows)
Ubuntu job: installs and builds @ode/tokens, runs pnpm install --frozen-lockfile / pnpm run build in formulus-formplayer, stages build/ → desktop/public/formplayer_dist/, uploads artifact desktop-formplayer-dist (short retention for CI).
Jobs build-desktop-bundles (CI) and release-desktop-bundles (release)
Matrix build (mirrors CLI OS/arch coverage): linux amd64 + arm64, windows amd64 + arm64, darwin amd64 (macos-14) + arm64 (macos-latest). Pull requests build linux-amd64 only (smoke packaging); push to main/dev and release build all six platforms. Each runner installs Node + pnpm, restores formplayer artifact, installs Linux WebKitGTK packages where needed, runs pnpm exec tauri build --target … with a merged config so beforeBuildCommand runs pnpm build only (frontend + Vite output; embedded formplayer is already present). Builds use Swatinem/rust-cache scoped per platform.
CI artifacts
Each matrix cell uploads installers under artifact name ode-desktop-<platform> (files renamed with prefix ode-desktop-<platform>-<original-name>).
Release assets
On release, softprops/action-gh-release attaches those installers for each platform to the published release alongside other assets (CLI, APK, SBOMs, etc.).
Production bundles must include embedded formplayer: locally, pnpm tauri build uses pnpm build:tauri (beforeBuildCommand in tauri.conf.json). In CI/Rust release jobs, formplayer is built once on Ubuntu and copied into desktop/public/formplayer_dist/ before each OS build. Copied assets are gitignored locally (see desktop/README.md).
Potential improvements to the CI/CD pipeline:
- Automated formplayer asset synchronization
- Add automated testing before build
- Implement security scanning (Trivy, Snyk)
- Add deployment to staging environment
- Create release notes automation
- Add Slack/Discord notifications
- Implement rollback mechanisms
- Add performance benchmarking
- Root README - Monorepo overview
- Synkronus DOCKER.md - Docker quick start
- Synkronus DEPLOYMENT.md - Comprehensive deployment guide