Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f98c224
chore(ci): 🔧 exempt container.yml from the workflow-commands hook
robertodr Oct 21, 2026
e03ebbe
feat(ci): ✨ add the GHCR image retention policy
robertodr Oct 21, 2026
dd21d7c
feat(ci): ✨ prune stale container images from GHCR daily
robertodr Oct 21, 2026
2ad0b8f
feat(container): ✨ add the HPCCM recipe for the MPI base image
robertodr Oct 21, 2026
a9d212a
feat(container): ✨ add the monoprop image and its smoke test
robertodr Oct 21, 2026
5067639
feat(ci): ✨ build and publish the container images
robertodr Oct 21, 2026
beb15b3
docs(clusters): 📝 document the container image and MareNostrum5 usage
robertodr Oct 22, 2026
bb9dfec
fix(ci): 🐛 skip Dependabot runs and keep latest off pre-releases
robertodr Oct 22, 2026
7e874d3
docs(clusters): 📝 narrow the pr-N promise and note ACC core counts
robertodr Oct 22, 2026
1fa4839
fix(container): 🔒 require the mpi4py wheel
robertodr Oct 22, 2026
cf18de3
fix(ci): 🐛 recognise image indexes without a mediaType
robertodr Oct 22, 2026
7c4f8a6
Apply batched suggestions from code review
robertodr Oct 22, 2026
63af57c
fix(ci): 🐛 protect children of young indexes and close caught HTTP er…
robertodr Oct 22, 2026
c224ceb
ci(cleanup): 👷 run the GHCR cleanup on ubuntu-slim
robertodr Oct 22, 2026
735549c
Apply suggestion from @robertodr
robertodr Oct 22, 2026
b0277da
Update marenostrum5.mdx
robertodr Oct 22, 2026
b9b60a7
Apply batched suggestions from code review
robertodr Oct 22, 2026
14c1c22
Merge branch 'main' into feat/hpc-container
robertodr Oct 22, 2026
32e9b52
fix(container): 🔇 ask PMIx for native authentication
robertodr Oct 22, 2026
e37149f
docs(clusters): 📝 fix the MareNostrum5 job script and placement check
robertodr Oct 22, 2026
0610963
Merge branch 'main' into feat/hpc-container
robertodr Oct 23, 2026
c509a58
Merge branch 'main' into feat/hpc-container
robertodr Oct 23, 2026
80a3282
Merge branch 'main' into feat/hpc-container
robertodr Oct 26, 2026
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
47 changes: 47 additions & 0 deletions .github/workflows/cleanup-images.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Clean up container images

on:
schedule:
- cron: "0 3 * * *" # daily at 03:00 UTC
workflow_dispatch:
inputs:
dry_run:
description: Only log the image versions that would be deleted
type: boolean
default: true

concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false

permissions: {}

jobs:
cleanup:
name: Delete stale GHCR image versions
if: github.repository == 'Algorithmiq/monoprop'
runs-on: ubuntu-slim
permissions:
contents: read
packages: write
pull-requests: read

steps:
- uses: actions/checkout@v7.0.1
with:
sparse-checkout: tools/cleanup-ghcr.py
sparse-checkout-cone-mode: false

# tools/cleanup-ghcr.py needs only the standard library of Python >= 3.11, which
# ubuntu-slim's system python3 provides.
- name: Delete stale image versions
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }}
run: |
if [[ "$DRY_RUN" == "true" ]]; then
mode=--dry-run
else
mode=--no-dry-run
fi
python3 tools/cleanup-ghcr.py "$mode"
287 changes: 287 additions & 0 deletions .github/workflows/container.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
name: Container images

on:
workflow_dispatch:
release:
types:
- published
pull_request:
paths:
- .github/workflows/container.yml
- tools/container/**
types:
- opened
- reopened
- synchronize
- ready_for_review
push:
branches:
- main

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions: {}

env:
COLUMNS: 120
FORCE_COLOR: 3
HPCCM_VERSION: "26.5.0"
REGISTRY: ghcr.io
BASE_IMAGE_NAME: ghcr.io/algorithmiq/monoprop-base
APP_IMAGE_NAME: ghcr.io/algorithmiq/monoprop

jobs:
sdist:
name: Build source distribution
# Fork and Dependabot PRs get a read-only token and cannot push to GHCR, and the app build pulls the base image from it.
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]'
runs-on: ubuntu-26.04
permissions:
contents: read
outputs:
file: ${{ steps.sdist.outputs.file }}
version: ${{ steps.sdist.outputs.version }}

steps:
- uses: actions/checkout@v7.0.1
with:
fetch-depth: 0 # such that setuptools_scm can do its job correctly

- name: Install the latest version of uv
uses: astral-sh/setup-uv@v10.3.0

- name: Build SDist
id: sdist
run: |
uv tool run --from build pyproject-build --verbose --sdist --installer uv
file="$(basename dist/monoprop-*.tar.gz)"
version="${file#monoprop-}"
version="${version%.tar.gz}"
echo "file=$file" >> "$GITHUB_OUTPUT"
echo "version=$version" >> "$GITHUB_OUTPUT"

- uses: actions/upload-artifact@v7
with:
name: container-sdist
path: dist/monoprop-*.tar.gz

base:
name: Resolve or build the base image
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]'
runs-on: ubuntu-26.04
timeout-minutes: 180
permissions:
contents: read
packages: write
outputs:
ref: ${{ steps.hash.outputs.ref }}

steps:
- uses: actions/checkout@v7.0.1

- name: Install the latest version of uv
uses: astral-sh/setup-uv@v10.3.0

- name: Render the base Dockerfile
run: |
mkdir -p build/container/base
uvx "hpccm==${HPCCM_VERSION}" --recipe tools/container/base.py --format docker \
> build/container/base/Dockerfile

- uses: actions/upload-artifact@v7
with:
name: base-dockerfile
path: build/container/base/Dockerfile

# The tag hashes the rendered Dockerfile, not the recipe, so an hpccm upgrade that
# changes the output also yields a new base image.
- name: Compute the content-addressed tag
id: hash
run: |
hash="$(sha256sum build/container/base/Dockerfile | cut -c1-12)"
echo "ref=${BASE_IMAGE_NAME}:${hash}" >> "$GITHUB_OUTPUT"

- name: Log in to the container registry
uses: docker/login-action@v4.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4.4.1

- name: Check whether the base image exists
id: exists
env:
REF: ${{ steps.hash.outputs.ref }}
run: |
if docker buildx imagetools inspect "$REF" > /dev/null 2>&1; then
echo "exists=true" >> "$GITHUB_OUTPUT"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
fi

- name: Extract metadata for the base image
id: meta
uses: docker/metadata-action@v6.2.0
with:
images: ${{ env.BASE_IMAGE_NAME }}
flavor: |
latest=false
tags: |
type=semver,pattern={{version}}
type=ref,event=tag
type=ref,event=branch
type=ref,event=pr
labels: |
org.opencontainers.image.vendor=Algorithmiq
org.opencontainers.image.description=MPI base image (UCX, PMIx, Open MPI) for monoprop

- name: Build and push the base image
if: steps.exists.outputs.exists != 'true'
uses: docker/build-push-action@v7.4.0
with:
context: build/container/base
file: build/container/base/Dockerfile
no-cache: true
push: true
tags: ${{ steps.hash.outputs.ref }}
labels: ${{ steps.meta.outputs.labels }}

# Cleanup keeps versions by their release, main or pr-N tags, so the base image
# carries the tags of every event that used it.
- name: Tag the base image for this event
env:
REF: ${{ steps.hash.outputs.ref }}
TAGS: ${{ steps.meta.outputs.tags }}
run: |
args=()
while IFS= read -r tag; do
[[ -n "$tag" ]] && args+=(--tag "$tag")
done <<< "$TAGS"
if (( ${#args[@]} == 0 )); then
exit 0
fi
docker buildx imagetools create "${args[@]}" "$REF"
# A retag must not create a new manifest: the hash tag would lose its pins.
expected="$(docker buildx imagetools inspect "$REF" --format '{{json .Manifest}}' | jq -r .digest)"
while IFS= read -r tag; do
[[ -z "$tag" ]] && continue
actual="$(docker buildx imagetools inspect "$tag" --format '{{json .Manifest}}' | jq -r .digest)"
if [[ "$actual" != "$expected" ]]; then
echo "::error::$tag points at $actual, expected $expected"
exit 1
fi
done <<< "$TAGS"

app:
name: Build, check and push the monoprop image
needs:
- sdist
- base
runs-on: ubuntu-26.04
timeout-minutes: 90
permissions:
contents: read
packages: write
pull-requests: write

steps:
- uses: actions/checkout@v7.0.1

- uses: actions/download-artifact@v8
with:
name: container-sdist
path: build/container/app

- name: Log in to the container registry
uses: docker/login-action@v4.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4.4.1

- name: Extract metadata for the monoprop image
id: meta
uses: docker/metadata-action@v6.2.0
with:
images: ${{ env.APP_IMAGE_NAME }}
flavor: |
latest=false
tags: |
type=semver,pattern={{version}}
type=ref,event=tag
type=ref,event=branch
type=ref,event=pr
type=sha,prefix=sha-,format=short
type=raw,value=latest,enable=${{ github.event_name == 'release' && !github.event.release.prerelease }}
labels: |
org.opencontainers.image.vendor=Algorithmiq
org.opencontainers.image.base.name=${{ needs.base.outputs.ref }}

- name: Build the image
uses: docker/build-push-action@v7.4.0
with:
context: build/container/app
file: tools/container/Dockerfile
build-args: |
BASE_IMAGE=${{ needs.base.outputs.ref }}
SDIST=${{ needs.sdist.outputs.file }}
no-cache: true
load: true
push: false
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

- name: Smoke-test the image
env:
IMAGE: ${{ fromJSON(steps.meta.outputs.json).tags[0] }}
VERSION: ${{ needs.sdist.outputs.version }}
run: |
docker run --rm --shm-size=1g \
-v "$PWD/tools/container/smoke-test.sh:/smoke-test.sh:ro" \
"$IMAGE" bash /smoke-test.sh "$VERSION"

# Same inputs as the build above, so Buildx serves every layer from its cache.
- name: Push the image
uses: docker/build-push-action@v7.4.0
with:
context: build/container/app
file: tools/container/Dockerfile
build-args: |
BASE_IMAGE=${{ needs.base.outputs.ref }}
SDIST=${{ needs.sdist.outputs.file }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

- name: Prepare pull instructions
if: github.event_name == 'pull_request'
env:
TAGS: ${{ steps.meta.outputs.tags }}
run: |
{
echo "Container image pushed to GHCR. Pull it with Docker or Singularity:"
echo
while IFS= read -r tag; do
[[ -z "$tag" ]] && continue
echo '```bash'
echo "docker pull $tag"
echo "singularity pull monoprop_${tag##*:}.sif docker://$tag"
echo '```'
echo
done <<< "$TAGS"
} > pull-instructions.md

- name: Post pull instructions
if: github.event_name == 'pull_request'
uses: marocchino/sticky-pull-request-comment@v3.0.5
with:
header: container-pull-instructions
path: pull-instructions.md
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ shared engine; Majorana against Pauli is a runtime `Basis`, and Python exposes `

Public headers `cpp/include/monoprop/`, implementation `cpp/monoprop/`, Python API
`src/monoprop/`, binding template `src/monoprop/bindings/binder.h`, generators
`tools/generate-*.py`, sibling distributions `packages/*`. The repository root is the `uv`
`tools/generate-*.py`, container recipes `tools/container/` (see `building.mdx`), sibling
distributions `packages/*`. The repository root is the `uv`
workspace's `monoprop` package.

`bindings.cpp` and `_dispatch.py` are generated -- never edit them. To change the C++ API: edit
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ pip install monoprop # or: uv add monoprop
The prebuilt PyPI wheels are single-process (built **without** MPI). For multi-rank
runs, or to build the C++ library and executables, build from source (see below).

For HPC systems without outbound network, an MPI-enabled container image is published to
`ghcr.io/algorithmiq/monoprop`; see the
[HPC clusters documentation](https://docs.monoprop.algorithmiq.tech/clusters).

## Quick example

Back-propagate a Majorana observable through a one-gate circuit:
Expand Down
Loading
Loading