Current practice (mono, 2026-08-06)
virdx/mono uses a manual release pattern with two workflows per conda package:
<package>-package.yml— builds on push to main (paths-filtered), does not upload to Artifactory (upload-artifactory: false). Validates the package builds cleanly.<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: virdxThis 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-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:
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 reciperun:. - 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 fileRoot 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>-*.condaSharp edges
- pip_check failures: some conda-forge packages have broken metadata (e.g. nipype/etelemetry). Use
pip_check: falsein recipe tests if needed, with a comment explaining why. - Artifactory rejects duplicate versions: every publish must have a unique version. Bump
pyproject.tomlversion before dispatching the release workflow. - vimgx (not mono) still uses the legacy
bump_version.yaml+ tag-push-triggerpackage.yamlpattern, which is broken (tags created byGITHUB_TOKENdo not triggeron: pushworkflows). New projects should use the mono manual-release pattern.