Skip to content

Conda packaging and releases

How VIRDX builds and publishes conda packages to Artifactory (mono *-package/*-release workflows, rattler-build multi-output recipes, pixi feature split, ABI pitfalls).

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:

- 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:

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-sdkvxdata-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:

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:

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:

- 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

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.
Navigation

Type to search…

↑↓ navigate↵ selectEsc close