---
title: "Conda packaging and releases"
description: "How VIRDX builds and publishes conda packages to Artifactory (mono *-package/*-release workflows, rattler-build multi-output recipes, pixi feature split, ABI pitfalls)."
image: "https://docs.virdx.dev/img/virdx-social-card.png"
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.virdx.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Conda packaging and releases

## Current practice (mono, 2026-08-06)

`virdx/mono` uses a **manual release pattern** with two workflows per conda package:

1. **`<package>-package.yml`** — builds on push to main (paths-filtered), does **not** upload to Artifactory (`upload-artifactory: false`). Validates the package builds cleanly.
2. **`<package>-release.yml`** — manual trigger only (`workflow_dispatch`), builds and uploads to Artifactory (`upload-artifactory: true`).

Example: `vxdata-sdk-package.yml` triggers on pushes to main that touch `packages/vxdata-sdk/src/**`, `conda.recipe/**`, `pyproject.toml`, etc.; `vxdata-sdk-release.yml` is dispatched manually when ready to publish.

Both use `virdx/rattler-build-action@v0.10.4` and derive version from the package's `pyproject.toml` via `nx-tools/read-python-version.js`:

```yaml
- name: Read package version
  run: echo "PACKAGE_VERSION=$(node nx-tools/read-python-version.js packages/vxdata-sdk/pyproject.toml)" >> "$GITHUB_ENV"
- name: Build conda package
  uses: virdx/rattler-build-action@v0.10.4
  with:
recipe-path: packages/vxdata-sdk/conda.recipe/recipe.yaml
upload-artifactory: true # or false for -package.yml
channel-name: virdx
```

**This is NOT the automatic `bump_version.yaml` + `workflow_run` chain** described in older internal docs — that pattern was never adopted in mono. Versioning is manual (edit `pyproject.toml`, merge to main), then dispatch the release workflow when ready.

## Recipes: conda.recipe/recipe.yaml

Package recipes live in `<package>/conda.recipe/recipe.yaml`. Version comes from the `PACKAGE_VERSION` env var set by the workflow (read from `pyproject.toml`), **not** from git tags or `GIT_DESCRIBE_TAG`:

```yaml
context:
  version: ${{ env.get('PACKAGE_VERSION') }}

recipe:
  name: vxdata-sdk
  version: ${{ version }}
```

## Monorepo path dependencies: multi-output recipes

When a package depends on another in-repo package by source path (e.g. `vxdata-sdk` → `vxdata-schemas`), declare the recipe as a **multi-output** recipe that builds both from source in the same `rattler-build` invocation.

Do **not** pin the dependency to a published Artifactory version in `run:` — that requires the same-versioned dependency to already exist on Artifactory before the first build, a chicken-and-egg deadlock.

Instead:

```yaml
outputs:
  - package:
  name: vxdata-schemas
  version: ${{ version }}
source:
  path: ../../vxdata-schemas # relative to conda.recipe/
build: { ... }
requirements: { ... }
tests: { ... }

  - package:
  name: vxdata-sdk
  version: ${{ version }}
source:
  path: ../
requirements:
  run:
    - ${{ pin_subpackage('vxdata-schemas', exact=True) }}
```

`pin_subpackage('vxdata-schemas', exact=True)` pins the consumer to the exact dependency version built in the **same rattler-build run**. No pre-built local channel or Artifactory publish is needed. (See `virdx/mono` PR #121, issues #106/#109; `packages/vxdata-sdk/conda.recipe/recipe.yaml` as of 2026-07-27.)

## Pixi features: default vs dev

The `default` feature (`[tool.pixi.dependencies]`) is auto-included in **every** pixi environment unless `no-default-feature = true`. Put **only** pure runtime dependencies there — the minimal set needed to import and use the package.

Move tooling (ruff, pytest, ty), notebook/script deps (jupyter, boto3, h5py, simpleitk), and docs tools into `[tool.pixi.feature.dev.dependencies]`. Test-only deps go in a `test` feature.

Reference: `mono/apps/inference/pyproject.toml` splits `default` (vxdata-sdk only), `inference` (viseg, diffsim, histo, pytorch-cuda), `dev` (ruff, pytest, ty), and `api` (fastapi, python-kubernetes).

### Recipe `run:` vs pixi features

`conda.recipe/recipe.yaml` `run:` deps are the **shipped conda package's** runtime deps and must mirror what `src/<pkg>/` actually imports — not the pixi dev environment. Audit with:

```bash
grep -rhoE "^(from|import) [a-zA-Z0-9_]+" src/<pkg>/ | sed -E 's/^(from|import) //' | sort -u
```

- Deps used only in `scripts/`, `dashboards/`, `notebooks/`, or tests do **not** belong in recipe `run:`.
- Keep indirect-but-required backends with a comment: `imageio` (for skimage.io.imread), `tensorboard` (for lightning TensorBoardLogger), `ffmpeg` (for cv2.VideoWriter).
- Declare direct imports even if currently transitive; don't rely on another dep pulling them forever.

(See `virdx/histo` PR #149 for a dep audit that caught these patterns.)

## Dependency and ABI pitfalls

### Conda-forge ABI migrations break transitive deps

**Incident 2026-07-03 (mono vxa-sdk)**: `bunx nx affected -t build` failed in vxa-sdk's rattler-build test phase with:

```
ImportError: libjxl.so.0.11: cannot open shared object file
```

**Root cause**: conda-forge published `libjxl 0.12.0` (new soname `.so.0.12`), superseding 0.11.x. Current `rasterio 1.5.0` still links `libjxl.so.0.11`, not yet rebuilt against 0.12. Nothing pinned `libjxl` (transitive dep of rasterio), so a fresh rattler test-env solve floated it to 0.12.0 and broke rasterio's ABI.

**Fix (PR #167)**: add a temporary run-dep pin:

```yaml
- rasterio >=1.5.0,<2
# temporary; conda-forge rasterio links libjxl.so.0.11 while 0.12 is published.
# Drop once rasterio is rebuilt against 0.12.
- libjxl <0.12
```

(Still present in `packages/vxa-sdk/conda.recipe/recipe.yaml` as of 2026-07-03.)

**Lesson**: when a conda-forge dependency publishes a new soname but your direct dependents haven't rebuilt, test environments can float to the incompatible newer version. A temporary upper-bound pin on the transitive dep is standard practice during ABI migration windows. Watch conda-forge rebuild trackers and remove the pin once the direct dep is rebuilt.

### Nx root lockfile fanout affects all projects

**Why a frontend-only PR triggered vxa-sdk CI** (2026-07-03): `main` pushes rebuild only the one changed project (~44s via `nx affected`), so vxa-sdk's latent libjxl issue was invisible. An unrelated frontend PR touched the root `bun.lock`; **Nx treats a root-lockfile change as affecting EVERY project**, so `nx affected -t build` pulled vxa-sdk (and all other Python conda builds normally skipped) into the build set, surfacing the ABI break.

**Lesson**: in an Nx monorepo, a root `bun.lock`, `package-lock.json`, or `pixi.lock` change fans out `affected` to **all** projects, including unrelated language stacks. A JS lockfile bump can trigger full Python conda builds. Always test `nx affected -t build` locally after touching a root lockfile, or expect surprise CI failures in unrelated projects.

## Local build and test

```bash
cd packages/<package>
export PACKAGE_VERSION=$(node ../../nx-tools/read-python-version.js pyproject.toml)
pixi exec --spec 'rattler-build>=0.23' -- rattler-build build --recipe conda.recipe/recipe.yaml --output-dir /tmp/out
pixi exec --spec 'rattler-build>=0.23' -- rattler-build test --package-file /tmp/out/noarch/<package>-*.conda
```

## Sharp edges

- **pip_check failures**: some conda-forge packages have broken metadata (e.g. nipype/etelemetry). Use `pip_check: false` in recipe tests if needed, with a comment explaining why.
- **Artifactory rejects duplicate versions**: every publish must have a unique version. Bump `pyproject.toml` version before dispatching the release workflow.
- **vimgx (not mono)** still uses the legacy `bump_version.yaml` + tag-push-trigger `package.yaml` pattern, which is broken (tags created by `GITHUB_TOKEN` do not trigger `on: push` workflows). New projects should use the mono manual-release pattern.

Source: https://docs.virdx.dev/knowledge/wiki/workstreams/infrastructure/sops/conda-packaging-and-releases/index.mdx
