Skip to content
Closed
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
30 changes: 27 additions & 3 deletions .github/workflows/build-flex-arm-wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,27 @@ name: Build Flex aarch64 Wheels

on:
workflow_dispatch:
pull_request:
branches: [main]
paths:
- '.github/workflows/build-flex-arm-wheels.yml'
- 'Dockerfile.build'
- 'packages/flex-acceptance-contract/**'
- 'pyproject.toml'
- 'scripts/artifact_manifest.py'
- 'src/**'
- 'uv.lock'
push:
branches: [main]
tags: ['*']
paths:
- '.github/workflows/build-flex-arm-wheels.yml'
- 'Dockerfile.build'
- 'packages/flex-acceptance-contract/**'
- 'pyproject.toml'
- 'uv.lock'
- 'scripts/artifact_manifest.py'
- 'src/**'
- 'uv.lock'

jobs:
build-flex-arm-wheels:
Expand All @@ -37,11 +50,22 @@ jobs:
cache-to: type=gha,mode=max

- name: List wheels
run: ls -la dist_arm/
run: |
ls -la dist_arm/
python scripts/artifact_manifest.py verify dist_arm \
--connector-version 0.9.1 \
--opentrons-version 9.0.0 \
--robot-server-version 9.0.0 \
--opentrons-source-commit 44b37a2f91520bf2e7245c70bf799d46c8c2d9a5 \
--python-version 3.12 \
--architecture aarch64

- name: Upload flex-arm-wheels artifact
uses: actions/upload-artifact@v7
with:
name: flex-arm-wheels
path: dist_arm/*.whl
path: |
dist_arm/*.whl
dist_arm/runtime-manifest.json
dist_arm/SHA256SUMS
retention-days: 14
72 changes: 57 additions & 15 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ jobs:
strategy:
matrix:
include:
# Controlled mutation and the ARM artifact are validated only against
# the Opentrons 8.8.1 private Protocol Engine state surface.
- python-version: "3.10"
opentrons-version: "8.8.1"
# This is the exact runtime matrix deployed to Flex.
- python-version: "3.12"
opentrons-version: "9.0.0"
opentrons-commit: "44b37a2f91520bf2e7245c70bf799d46c8c2d9a5"

steps:
- uses: actions/checkout@v6
Expand All @@ -41,6 +41,9 @@ jobs:
- name: Test (unit + gRPC simulator)
run: uv run pytest --cov=src --cov-report=term --cov-report=xml --junit-xml=junit.xml

- name: Collect guarded HITL inventory (no hardware execution)
run: uv run pytest tests/integration/hardware --collect-only -q

- name: Upload coverage
uses: actions/upload-artifact@v7
with:
Expand All @@ -59,29 +62,30 @@ jobs:
# the util/ subpackage that robot_server imports.
- name: Install opentrons robot-server ${{ matrix.opentrons-version }}
run: |
VER="${{ matrix.opentrons-version }}"
REF="${{ matrix.opentrons-commit }}"
# robot_server/server_utils are not on PyPI; install with --no-deps to skip the
# monorepo-internal ==0.0.0 dep constraints that can't be satisfied from PyPI.
uv pip install --no-deps \
"robot_server @ git+https://github.com/Opentrons/opentrons.git@v${VER}#subdirectory=robot-server" \
"server_utils @ git+https://github.com/Opentrons/opentrons.git@v${VER}#subdirectory=server-utils"
"robot_server @ git+https://github.com/Opentrons/opentrons.git@${REF}#subdirectory=robot-server" \
"server_utils @ git+https://github.com/Opentrons/opentrons.git@${REF}#subdirectory=server-utils"
# Reinstall opentrons monorepo packages from git so robot_server gets all the
# internal symbols it imports at module load time. The PyPI builds omit or differ
# from the monorepo source (e.g. PREVIEW_IMAGE, opentrons_shared_data.util,
# opentrons_hardware which is not even a dep of the PyPI opentrons package).
uv pip install --no-deps --reinstall \
"opentrons @ git+https://github.com/Opentrons/opentrons.git@v${VER}#subdirectory=api" \
"opentrons_shared_data @ git+https://github.com/Opentrons/opentrons.git@v${VER}#subdirectory=shared-data" \
"opentrons_hardware @ git+https://github.com/Opentrons/opentrons.git@v${VER}#subdirectory=hardware"
"opentrons @ git+https://github.com/Opentrons/opentrons.git@${REF}#subdirectory=api" \
"opentrons_shared_data @ git+https://github.com/Opentrons/opentrons.git@${REF}#subdirectory=shared-data" \
"opentrons_hardware @ git+https://github.com/Opentrons/opentrons.git@${REF}#subdirectory=hardware"
# Install robot_server's non-monorepo deps (versions from its Pipfile.lock).
# aiohttp: needed by the legacy routers in robot_server 8.8.1.
# Versions match the Opentrons 9.0 robot-server lockfile.
uv pip install \
"aiohttp" \
"aiohttp==3.12.14" \
"anyio==4.9.0" \
"fastapi==0.100.0" \
"pydantic==2.11.7" \
"pydantic-settings==2.4.0" \
"python-dotenv==1.0.1" \
"python-multipart==0.0.6" \
"python-multipart==0.0.18" \
"uvicorn==0.27.0.post1" \
"wsproto==1.2.0" \
"sqlalchemy==1.4.51" \
Expand Down Expand Up @@ -109,10 +113,10 @@ jobs:
with:
fetch-depth: 0

- name: Download coverage (Python 3.10)
- name: Download coverage (Python 3.12)
uses: actions/download-artifact@v4
with:
name: coverage-3.10
name: coverage-3.12
path: .

- name: Skip SonarCloud when token is unavailable
Expand Down Expand Up @@ -168,3 +172,41 @@ jobs:
if total == 0:
print(' No open issues.')
"

windows-operator-readiness:
runs-on: windows-latest
steps:
- uses: actions/checkout@v6

- name: Install uv
uses: astral-sh/setup-uv@v8.1.0
with:
version: "0.9.6"
enable-cache: true

- name: Set up operator Python 3.12
run: uv python install 3.12

- name: Install connector and test dependencies
run: uv sync --all-extras

- name: Validate Windows readiness and evidence path
run: uv run pytest -p no:cacheprovider tests/scripts/test_preflight_flex.py tests/test_hitl_evidence.py tests/test_acceptance_manifest.py tests/test_runtime_contract_consistency.py tests/test_asms_unit_operations.py -q

- name: Build a valid offline workflow manifest
run: uv run python -c "import json; from tests.test_acceptance_manifest import _manifest; open('flex-acceptance-ci.json', 'w', encoding='utf-8').write(json.dumps(_manifest()))"

- name: Set up Windows workflow Python 3.12
run: |
uv python install 3.12
uv venv --python 3.12 .venv-workflow
uv pip install --python .venv-workflow\Scripts\python.exe --no-deps -e packages\flex-acceptance-contract -e workflows\flex-system-acceptance
uv pip install --python .venv-workflow\Scripts\python.exe "pytest==9.1.1" "pytest-asyncio==1.4.0"

- name: Execute the native PowerShell workflow entrypoint
run: .\.venv-workflow\Scripts\python.exe -m flex_system_acceptance --validate-only --manifest .\flex-acceptance-ci.json

- name: Import and execute offline workflow phases on Windows Python 3.12
run: |
.\.venv-workflow\Scripts\python.exe -m pytest -p no:cacheprovider `
tests\test_workflow_manifests.py -m flex_workflow_offline -q
30 changes: 27 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,30 @@

## Unreleased

- Aligned `HeaterShakerController` v3.2 with the pinned Opentrons 9.0 API:
finite 0–95 °C targets are accepted while the documentation warns that
below-ambient targets may be physically unreachable.
- Hardened accessory correctness and SiLA conformance: duplicate same-type
modules now fail connector startup with their serial numbers instead of
silently replacing a Feature; Heater-Shaker and Thermocycler autonomous waits
release the connector-wide lock and detect parallel target changes; thermal
values and direction-dependent Thermocycler ramp rates are validated before
actuation; long operations emit measured progress; cancelled profiles
deactivate thermal control. `HeaterShakerController` v3.1 and
`ThermocyclerController` v2.1 add observable `Status` and static `DeviceInfo`
properties while retaining read commands for existing clients. CI now runs
the vendored official SiLA v1.2 semantic XSLT in addition to XSD validation.
- Version 0.9.1 aligns the deployable runtime with Flex Opentrons 9.0.0 on
Python 3.12, packages the co-versioned private robot-server stack, verifies a
checksummed ARM artifact manifest built from an immutable upstream commit, and
rejects cross-release imports before any hardware initialization. Runtime
wheels are resolved strictly from `uv.lock`, without a second unpinned
dependency solve. Deployments
now use versioned immutable releases with completion markers, bounded
no-motion/live health checks, automatic stock robot-server recovery, and
explicit release rollback. The multipart upload parser is upgraded to the
patched 0.0.18 release, and authenticated mutation clients are restricted to
an encrypted loopback tunnel.
- Added a corrected, parameterized AS-MS wash/elution protocol candidate, pinned exact
Azenta and Thermo KingFisher custom-labware definitions, exact-vs-shadow offline
preflight, full exact-bundle Protocol Engine execution coverage, and a staged real-Flex
Expand All @@ -19,9 +43,9 @@
restricted to token-authenticated, audited, non-actuating built-in-resource commands
and serialized with play; protocol-backed runs and custom definitions fail closed.
Stop remains available during state-provider failure, command audit is written in
bounded batches, and the ARM artifact is pinned to the validated Python 3.10/Opentrons
8.8.1 runtime. Python 3.11+ is intentionally outside the supported matrix because
robot-server 8.8.1 does not import reliably on current Python 3.11 patch releases.
bounded batches, and the ARM artifact is pinned to the Python
3.12/Opentrons 9.0.0 runtime observed on the target Flex. Incompatible wheel
ABI and architecture tags are rejected before upload.

All notable changes to this project will be documented in this file.

Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
ARG PYTHON_VERSION=3.10
ARG PYTHON_VERSION=3.12

FROM ghcr.io/astral-sh/uv:python${PYTHON_VERSION}-bookworm AS build

Expand Down
46 changes: 38 additions & 8 deletions Dockerfile.build
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,57 @@
# --target export --output type=local,dest=dist_arm .
#
# The resulting dist_arm/ feeds deploy.sh.
FROM --platform=linux/arm64 ghcr.io/astral-sh/uv:python3.10-bookworm AS wheels
FROM ghcr.io/astral-sh/uv:python3.12-bookworm AS wheels

ARG OPENTRONS_VERSION=9.0.0
ARG OPENTRONS_COMMIT=44b37a2f91520bf2e7245c70bf799d46c8c2d9a5
ARG CONNECTOR_VERSION=0.9.1

ENV UV_LINK_MODE=copy \
UV_PYTHON_DOWNLOADS=never

# libusb is a runtime dep of opentrons; present here only so resolution matches
# the device. Wheels are platform artifacts and do not embed system libs.
RUN apt-get -y update && apt-get -y install --no-install-recommends libusb-1.0-0 && rm -rf /var/lib/apt/lists/*
RUN apt-get -y update && \
apt-get -y install --no-install-recommends git libusb-1.0-0 && \
rm -rf /var/lib/apt/lists/*

WORKDIR /build
ADD pyproject.toml uv.lock README.md /build/
ADD src /build/src
ADD packages/flex-acceptance-contract /build/packages/flex-acceptance-contract
ADD scripts/artifact_manifest.py /build/scripts/artifact_manifest.py

# Download/build wheels for the project and all runtime deps into /wheels.
# robot_server (an Opentrons system package) is NOT on PyPI and is intentionally
# excluded — it is provided by the Flex system site-packages at install time
# (see scripts/install.sh --system-site-packages).
# Build the exact tagged robot_server and its four co-versioned Opentrons
# packages. The public PyPI artifacts do not contain every internal module that
# the embedded robot-server imports, so deployment must not mix a PyPI API wheel
# with whatever robot-server happens to be present in the system image.
RUN uv build --wheel --out-dir /wheels . && \
uv pip compile pyproject.toml --output-file /tmp/req.txt && \
(python -m pip download --no-deps -r /tmp/req.txt --dest /wheels --only-binary :all: || \
python -m pip download --no-deps -r /tmp/req.txt --dest /wheels)
uv export --locked --no-dev --no-hashes --no-emit-project --output-file /tmp/req.txt && \
python -m pip wheel --no-deps -r /tmp/req.txt --wheel-dir /wheels

RUN git clone --depth 1 --branch "v${OPENTRONS_VERSION}" \
https://github.com/Opentrons/opentrons.git /opt/opentrons-source && \
test "$(git -C /opt/opentrons-source rev-parse HEAD)" = "$OPENTRONS_COMMIT" && \
rm -f \
/wheels/opentrons-[0-9]*.whl \
/wheels/opentrons_shared_data-[0-9]*.whl \
/wheels/opentrons_hardware-[0-9]*.whl \
/wheels/server_utils-[0-9]*.whl \
/wheels/robot_server-[0-9]*.whl && \
uv build --wheel --out-dir /wheels /opt/opentrons-source/shared-data && \
uv build --wheel --out-dir /wheels /opt/opentrons-source/api && \
uv build --wheel --out-dir /wheels /opt/opentrons-source/hardware && \
uv build --wheel --out-dir /wheels /opt/opentrons-source/server-utils && \
uv build --wheel --out-dir /wheels /opt/opentrons-source/robot-server && \
python /build/scripts/artifact_manifest.py build /wheels \
--connector-version "$CONNECTOR_VERSION" \
--opentrons-version "$OPENTRONS_VERSION" \
--robot-server-version "$OPENTRONS_VERSION" \
--opentrons-source-commit "$OPENTRONS_COMMIT" \
--python-version "3.12" \
--architecture "aarch64"

FROM scratch AS export
COPY --from=wheels /wheels /
Loading
Loading