Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .config/mise/tasks/cli/qualify-mise
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
#MISE description="Qualify the candidate CLI with isolated mise uv and pipx installs"
set -euo pipefail

native="${NATIVE_DIST:-$PWD/dist/native}"
receipts="${QUALIFICATION_DIR:-$PWD/.dev/qualification}"
if [[ ${RELEASE_TAG:-} == python-v* ]]; then
native="$receipts/published-native"
fi
python -m scripts.qualify_mise \
packages/morphir/dist/morphir-*.whl \
"$native"/morphir_python_native-*.whl \
packages/morphir-tools/dist/morphir_python-*.whl \
--report "$receipts/mise-install.json"
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,12 @@ jobs:
# A python-v release qualifies the published native wheel instead.
RELEASE_TAG: ${{ inputs.release-tag }}

- name: Qualify mise uv and pipx candidate installs
if: ${{ !startsWith(inputs.release-tag, 'morphir-v') }}
run: mise run cli:qualify-mise
env:
RELEASE_TAG: ${{ inputs.release-tag }}

- name: Upload morphir distributions
uses: actions/upload-artifact@v6
with:
Expand Down
73 changes: 59 additions & 14 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ jobs:
needs: qualify
if: >-
github.event_name == 'workflow_dispatch' ||
startsWith(github.ref, 'refs/tags/native-v')
startsWith(github.ref, 'refs/tags/native-v') ||
startsWith(github.ref, 'refs/tags/python-v')
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -66,19 +67,19 @@ jobs:
uses: actions/checkout@v6

- name: Reuse the qualification wheel
if: matrix.reuse
if: matrix.reuse && !startsWith(github.ref, 'refs/tags/python-v')
uses: actions/download-artifact@v7
with:
name: ${{ matrix.reuse }}
path: dist/reused/

- name: Keep only the reused wheel
if: matrix.reuse
if: matrix.reuse && !startsWith(github.ref, 'refs/tags/python-v')
shell: bash
run: mkdir -p dist/native && cp dist/reused/*.whl dist/native/

- name: Build the wheel
if: ${{ !matrix.reuse }}
if: ${{ !matrix.reuse && !startsWith(github.ref, 'refs/tags/python-v') }}
uses: PyO3/maturin-action@v1
env:
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.target == 'aarch64-apple-darwin' && '11.0' || '10.12' }}
Expand All @@ -90,6 +91,22 @@ jobs:
manylinux: ${{ matrix.manylinux || 'off' }}
args: --release --locked --out ../../dist/native

- name: Set up Python 3.14
uses: actions/setup-python@v6
with:
python-version: '3.14'

- name: Set up uv
uses: astral-sh/setup-uv@v7

- name: Download the pinned published native wheel for a CLI release
if: startsWith(github.ref, 'refs/tags/python-v')
shell: bash
run: |
pin="$(python -c 'from scripts.qualify_native import tools_pin; print(tools_pin())')"
uvx --from pip==26.2.2 pip download --no-deps --only-binary=:all: \
--dest dist/native "morphir-python-native==$pin"

- name: Audit the Linux baseline and bundled libraries
if: matrix.manylinux != ''
shell: bash
Expand All @@ -101,14 +118,6 @@ jobs:
tr '\n' ' ' < audit.txt | tr -s ' ' \
| grep -q 'consistent with the following platform tag: "${{ matrix.platform }}"'

- name: Set up Python 3.14
uses: actions/setup-python@v6
with:
python-version: '3.14'

- name: Set up uv
uses: astral-sh/setup-uv@v7

- name: Download the qualified core and tools wheels
uses: actions/download-artifact@v7
with:
Expand All @@ -125,6 +134,31 @@ jobs:
python scripts/qualify_native.py install \
dist/pure/morphir-*.whl "$wheel" dist/pure/morphir_python-*.whl

# The Linux reuse already passed this in qualify. Other platforms and
# CLI releases exercise the package-manager path with their exact wheels.
- name: Set up mise without installing development toolchains
if: ${{ !matrix.reuse || startsWith(github.ref, 'refs/tags/python-v') }}
uses: jdx/mise-action@v3
with:
version: '2026.10.20'
install: false

- name: Qualify mise uv and actual pipx installs
if: ${{ !matrix.reuse || startsWith(github.ref, 'refs/tags/python-v') }}
shell: bash
run: |
python -m scripts.qualify_mise \
dist/pure/morphir-*.whl dist/native/morphir_python_native-*.whl \
dist/pure/morphir_python-*.whl --report dist/mise-install.json

- name: Upload mise qualification evidence
if: ${{ !matrix.reuse || startsWith(github.ref, 'refs/tags/python-v') }}
uses: actions/upload-artifact@v6
with:
name: mise-install-${{ matrix.platform }}
path: dist/mise-install.json
if-no-files-found: error

- name: Upload the qualified wheel
uses: actions/upload-artifact@v6
with:
Expand All @@ -135,7 +169,9 @@ jobs:
native-candidates:
name: Verify native release candidates
needs: [qualify, native-wheels]
if: needs.native-wheels.result == 'success'
if: >-
needs.native-wheels.result == 'success' &&
!startsWith(github.ref, 'refs/tags/python-v')
runs-on: ubuntu-latest
steps:
- name: Checkout repository
Expand Down Expand Up @@ -228,6 +264,8 @@ jobs:
"$bundle/morphir-python-binding-$version.wasm.sha256" dist/bundle/
cp "$bundle/release.json" "dist/bundle/morphir-python-binding-$version.release.json"
cp dist/bundle-check/.dev/qualification/bundle*.json dist/bundle/
# Seal the final asset names, including the renamed descriptor.
(cd dist/bundle && sha256sum -- morphir-python-binding-* bundle*.json > SHA256SUMS)

- name: Upload the release set
uses: actions/upload-artifact@v6
Expand Down Expand Up @@ -256,10 +294,16 @@ jobs:
name: extension-release
path: dist/

- name: Verify the final release assets
run: |
cd dist
sha256sum --strict --check SHA256SUMS

- name: Create the GitHub release
uses: softprops/action-gh-release@v2
with:
files: dist/*
fail_on_unmatched_files: true
generate_release_notes: true
prerelease: true

Expand Down Expand Up @@ -293,7 +337,7 @@ jobs:
github.event_name == 'push' &&
github.repository == 'finos/morphir-python' &&
startsWith(github.ref, 'refs/tags/python-v')
needs: qualify
needs: [qualify, native-wheels]
runs-on: ubuntu-latest
permissions:
id-token: write
Expand Down Expand Up @@ -336,6 +380,7 @@ jobs:
uses: softprops/action-gh-release@v2
with:
files: dist/*
fail_on_unmatched_files: true
generate_release_notes: true
draft: false
prerelease: ${{ contains(github.ref, 'alpha') || contains(github.ref, 'beta') || contains(github.ref, 'rc') }}
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,21 @@ The library and CLI are separate distributions:
Install just the library and SDK:

```bash
pip install morphir==0.0.2
# or: uv add morphir==0.0.2
pip install morphir==0.1.0
# or: uv add morphir==0.1.0
```

Install the CLI, which also installs the library:

```bash
pip install morphir-python==0.0.3
# or: uv add morphir-python==0.0.3
python -m pip install --only-binary=:all: morphir-python==0.1.0
# or: uv add morphir-python==0.1.0
```

For an isolated command-line installation, use the
[mise/uv or mise/pipx configuration](docs/installation.md#install-the-cli-with-mise).
mise manages Python and the CLI environment for you.

Python 3.14 or later is required. The CLI inspects and converts IR, compiles a
supported subset of Python to Morphir IR, and generates Python from Morphir IR.
The compiler runs in process from prebuilt wheels; you do not need Rust. See the
Expand Down
80 changes: 79 additions & 1 deletion docs/installation.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,78 @@
# Install Morphir Python

Use CPython 3.14. Choose a package according to what your application needs.
Use CPython 3.14. For command-line use, mise manages the interpreter and an
isolated CLI environment for you. For Python application development, install
the packages into your application's environment.

## Install the CLI with mise

With [mise](https://mise.jdx.dev/getting-started.html) installed, copy this into
your project's `mise.toml`:

<!-- example: examples/install/mise-uv.toml -->
```toml
[tools]
python = "3.14"
uv = "0.12.10"
"pypi:morphir-python" = { version = "0.1.0", uvx_args = "--no-build" }
```
<!-- /example -->

Then run:

```bash
mise trust
mise install
mise exec -- morphir-python --help
```

This uses mise's `pypi:` backend with uv, installs the published package from
PyPI, and exposes `morphir-python`. `--no-build` refuses source distributions:
the native compiler must have a compatible wheel, so installation never needs
a Rust toolchain. This does not install from a GitHub source checkout or a
standalone executable archive. GitHub Releases also retain the CLI wheel and
source archive; see [the release process](releasing.md).

To use actual pipx instead of uv, use this alternative `mise.toml`:

<!-- example: examples/install/mise-pipx.toml -->
```toml
[tool_alias]
pipx = "pypi:pipx"

[tools]
python = "3.14"
uv = "0.12.10"
pipx = { version = "1.18.4", python = "3.14", expose = ["pipx"] }
"pipx:morphir-python" = { version = "0.1.0", python = "3.14", uvx = false, pipx_args = "--pip-args='--only-binary=:all:'" }
```
<!-- /example -->

Run the same three commands above. The alias installs pipx itself through the
portable PyPI backend using uv, including on Windows, and gives mise the correct
dependency ordering. The CLI still installs through actual pipx:
`uvx = false` selects pipx, `python = "3.14"`
on the tool selects its interpreter, and `--only-binary=:all:` enforces the same
wheel-only policy. Choose one configuration. `pipx:` and `pypi:` are accepted
backend names, but mise treats them as separate installed tools. The
[mise backend documentation](https://mise.jdx.dev/dev-tools/backends/pypi.html)
explains these options.

For a global installation, put the chosen entries in your global mise
configuration, normally `~/.config/mise/config.toml` on macOS/Linux, then run
`mise install`. `mise config ls` shows the configuration files in use. With mise shell
activation enabled, run `morphir-python` directly. Without shell activation,
`mise exec -- morphir-python ...` works from a directory using the configuration.
The CLI's environment is separate from your project's Python dependencies.

The examples require mise 2026.10.20 or later. The versions are intentionally
pinned; update the CLI version when adopting another release. Pinning the CLI
does not freeze every transitive dependency. The version-only examples use the
installer flags above; they do not claim a fully locked dependency graph.

## Install Python packages

Choose a package according to what your application needs.

| Package | Install for | What it installs |
| --- | --- | --- |
Expand Down Expand Up @@ -61,3 +133,9 @@ whether the interpreter is free-threaded. If `morphir-python` cannot be found,
activate the environment and check its `bin` or `Scripts` directory. A model error
such as `PY004` is a compiler diagnostic; reinstalling packages will not make
unsupported syntax compile.

For mise, check `mise which morphir-python` and run with `mise exec --` if the
command is absent from your shell. If its managed interpreter was removed or
replaced, reinstall with `mise install --force pypi:morphir-python` or the
`pipx:` name you configured. A wheel-only refusal is an explicit unsupported
environment, not an instruction to install Rust.
78 changes: 78 additions & 0 deletions docs/releasing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Release Python packages and the WASM extension

The package versions are independent. A maintainer approves tags and publication.
The [Release workflow](https://github.com/finos/morphir-python/actions/workflows/release.yml)
qualifies the tagged commit before publishing. Its manual run qualifies a
candidate, including the platform matrix, without publishing a release.

| Tag | Published output | Installation |
| --- | --- | --- |
| `morphir-v<version>` | Core wheel and source archive on PyPI and GitHub Releases | Python package installer |
| `native-v<version>` | Five native compiler wheels and a source archive on PyPI | Exact dependency of the CLI |
| `python-v<version>` | CLI wheel and source archive on PyPI and GitHub Releases | mise `pypi:`/`pipx:`, uv or pip |
| `extension/python/v<version>` | WASM, SHA-256 checksum, release descriptor, qualification reports and `SHA256SUMS` on GitHub Releases | Morphir extension host |

The CLI release is a Python distribution. mise installs it from PyPI into an
isolated environment; GitHub release assets are distribution mirrors, not
standalone executables. The [installation guide](installation.md) gives the
tested configurations. The WASM guest is a separate artifact and does not
install through the CLI's Python package backend.

## Prepare a release

1. Set the package version and its corresponding constants and dependency pins.
Keep the native crate, native package metadata and the CLI's exact native
requirement consistent. Run `mise run check`.
2. Run the Release workflow manually on the candidate branch. Inspect every
required job, the platform install checks and qualification receipts. Missing,
failed, cancelled or skipped required evidence blocks publication.
3. Release the core library and native package before a CLI release that needs
their new versions. The CLI gate requires published matching dependencies and
refuses native source changes after its pinned `native-v` tag.
4. After maintainer approval, push the corresponding version tag at the reviewed
commit. A manual run does not authorize or perform publication. Check the
tagged run and the resulting PyPI files or GitHub release assets.

## What qualification proves

Normal CI checks a fresh candidate installation with both mise/uv and actual
mise/pipx on Linux. It reuses the existing core, CLI and native wheels. The
candidate install is restricted to an offline wheel directory, with dependencies
resolved under the workspace release constraints, so PyPI cannot substitute a
published package with the same version for the candidate.

Native releases and manual release qualification cover Linux x86_64/aarch64,
macOS arm64/x86_64 and Windows x86_64. A CLI release also runs these platforms,
using its pinned published native wheels instead of rebuilding Rust. Every
selected platform must pass before the CLI publishes. The reused Linux wheel
already passes the same installer checks in the qualification workflow.

Each installer has separate temporary mise configuration, state, cache and
package environments outside the checkout. Tests check installed versions,
compile and generate both IR v3 and draft v4, recompile generated source and run
the generated model with Rust absent from `PATH`. Receipts retain candidate and
dependency hashes. This complements the existing API, source archive, license,
vulnerability and host checks.

The WASM gate checks the tag's compiler version, source commit, descriptor digest
and checksum contents. Publication rechecks `SHA256SUMS` for the final release
asset names, including the renamed release descriptor.

## Reproduce the installer checks

Check the published configurations without changing your global mise setup:

```bash
uv run python -m scripts.qualify_mise --report .dev/qualification/mise-released.json
```

For candidates, first build the distributions using the existing tasks, then
run `mise run cli:qualify-mise`. CI passes the existing Linux native producer's
wheel; a `python-v` release uses the published-native directory qualified by
`native:qualify`. The script also accepts three explicit wheel paths for another
platform, as shown in the workflow.

Keep successful run IDs and the artifact receipts with the release record.
For a failed publication, inspect which files were accepted before retrying.
Do not replace a published version's bytes or move a released tag. Correct the
source, qualify a new version and publish a new tag.
8 changes: 8 additions & 0 deletions examples/install/mise-pipx.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[tool_alias]
pipx = "pypi:pipx"

[tools]
python = "3.14"
uv = "0.12.10"
pipx = { version = "1.18.4", python = "3.14", expose = ["pipx"] }
"pipx:morphir-python" = { version = "0.1.0", python = "3.14", uvx = false, pipx_args = "--pip-args='--only-binary=:all:'" }
4 changes: 4 additions & 0 deletions examples/install/mise-uv.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[tools]
python = "3.14"
uv = "0.12.10"
"pypi:morphir-python" = { version = "0.1.0", uvx_args = "--no-build" }
2 changes: 2 additions & 0 deletions openspec/changes/add-process-mep-extension/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: html-design
created: 2026-10-28
Loading
Loading