Skip to content

Commit 6ca8e97

Browse files
authored
Merge pull request #390 from OpenMS/claude/singularity-github-hosting-eH5Gh
Publish prebuilt Apptainer SIFs to GHCR via ORAS
2 parents bce2e27 + 1a17170 commit 6ca8e97

3 files changed

Lines changed: 113 additions & 4 deletions

File tree

.github/workflows/build-and-test.yml

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,81 @@ jobs:
259259
if: always()
260260
run: apptainer instance stop openms-test || true
261261

262+
- name: Upload validated SIF artifact (push events only)
263+
if: success() && github.event_name != 'pull_request'
264+
uses: actions/upload-artifact@v4
265+
with:
266+
name: openms-streamlit-${{ matrix.variant }}-sif
267+
path: /tmp/openms.sif
268+
retention-days: 1
269+
if-no-files-found: error
270+
271+
publish-apptainer:
272+
# Publish the validated SIF (already health-checked above) to GHCR as an
273+
# OCI artifact via ORAS, in a sibling package: ghcr.io/<owner>/<repo>/sif.
274+
# Keeping it separate from the docker image package keeps tag lists clean
275+
# and lets HPC users `apptainer pull oras://...` without the 5-15 min
276+
# on-the-fly OCI->SIF conversion the docker:// path requires.
277+
needs: test-apptainer
278+
if: github.event_name != 'pull_request'
279+
runs-on: ubuntu-latest
280+
permissions:
281+
contents: read
282+
packages: write
283+
strategy:
284+
fail-fast: false
285+
matrix:
286+
variant: [full, simple]
287+
steps:
288+
- name: Download validated SIF artifact
289+
uses: actions/download-artifact@v4
290+
with:
291+
name: openms-streamlit-${{ matrix.variant }}-sif
292+
path: /tmp
293+
294+
- name: Install apptainer
295+
uses: eWaterCycle/setup-apptainer@v2
296+
with:
297+
apptainer-version: 1.3.4
298+
299+
- name: Compute SIF tags
300+
id: meta
301+
uses: docker/metadata-action@v5
302+
with:
303+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/sif
304+
tags: |
305+
type=ref,event=branch,suffix=-${{ matrix.variant }}
306+
type=ref,event=tag,suffix=-${{ matrix.variant }}
307+
type=sha,prefix=,suffix=-${{ matrix.variant }}
308+
type=raw,value=latest,enable=${{ matrix.variant == 'full' && github.event_name == 'push' && github.ref == 'refs/heads/main' }}
309+
310+
- name: Log in to GHCR for ORAS push
311+
env:
312+
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
313+
run: |
314+
# apptainer reads its auth from ~/.apptainer/remote.yaml, NOT from
315+
# ~/.docker/config.json — so docker/login-action won't work here.
316+
# Login and push must both run as the runner user (no sudo) so they
317+
# share the same $HOME and therefore the same auth file.
318+
echo "$GHCR_TOKEN" | apptainer registry login \
319+
--username "${{ github.actor }}" \
320+
--password-stdin \
321+
oras://ghcr.io
322+
323+
- name: Push SIF to each computed tag
324+
run: |
325+
# `apptainer push` accepts ONE destination per invocation; iterate
326+
# over the newline-separated tag list from docker/metadata-action.
327+
# tr lowercase is belt-and-braces — metadata-action already
328+
# lowercases, but GHCR is strict about case in OCI refs.
329+
set -euo pipefail
330+
while IFS= read -r tag; do
331+
[ -z "$tag" ] && continue
332+
tag_lc="$(echo "$tag" | tr '[:upper:]' '[:lower:]')"
333+
echo "Pushing SIF to oras://${tag_lc}"
334+
apptainer push /tmp/openms.sif "oras://${tag_lc}"
335+
done <<< "${{ steps.meta.outputs.tags }}"
336+
262337
test-nginx:
263338
needs: build
264339
runs-on: ubuntu-latest

.github/workflows/ghcr-cleanup.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,29 @@ jobs:
5151
tag-selection: untagged
5252
cut-off: 7d
5353
dry-run: ${{ github.event.inputs.dry-run || 'false' }}
54+
55+
cleanup-sif-images:
56+
runs-on: ubuntu-latest
57+
permissions:
58+
packages: write
59+
steps:
60+
- name: Delete old commit-tagged SIFs (keep semver + main + latest)
61+
uses: snok/container-retention-policy@v3.0.1
62+
with:
63+
account: ${{ github.repository_owner }}
64+
token: ${{ secrets.GITHUB_TOKEN }}
65+
image-names: ${{ github.event.repository.name }}/sif
66+
image-tags: "!v*-full !v*-simple !main-full !main-simple !latest"
67+
tag-selection: tagged
68+
cut-off: 30d
69+
dry-run: ${{ github.event.inputs.dry-run || 'false' }}
70+
71+
- name: Delete untagged SIF manifests
72+
uses: snok/container-retention-policy@v3.0.1
73+
with:
74+
account: ${{ github.repository_owner }}
75+
token: ${{ secrets.GITHUB_TOKEN }}
76+
image-names: ${{ github.event.repository.name }}/sif
77+
tag-selection: untagged
78+
cut-off: 7d
79+
dry-run: ${{ github.event.inputs.dry-run || 'false' }}

README.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -139,17 +139,25 @@ This repository contains two Dockerfiles.
139139
## 🛰️ Run with Apptainer / Singularity (HPC)
140140
141141
Apptainer (formerly Singularity) is the dominant container runtime on HPC
142-
clusters. Pull the OCI image from GHCR, convert it to a SIF, and run it as
143-
your user — no root, no `--writable-tmpfs` required:
142+
clusters. CI publishes prebuilt SIFs to GHCR via ORAS, so you can pull a
143+
ready-to-run image with no on-the-fly OCI→SIF conversion and run it as your
144+
user — no root, no `--writable-tmpfs` required:
144145
145146
```bash
146-
apptainer pull docker://ghcr.io/openms/streamlit-template:latest
147+
apptainer pull --name openms-streamlit-template.sif \
148+
oras://ghcr.io/openms/streamlit-template/sif:latest
147149
apptainer run \
148150
--bind /path/to/data:/mounted-data:ro \
149151
--bind /path/to/workspaces:/workspaces-streamlit-template \
150-
streamlit-template_latest.sif
152+
openms-streamlit-template.sif
151153
```
152154

155+
Available tags follow the same scheme as the Docker images: `latest`,
156+
`main-full`, `main-simple`, `v*-full`, `v*-simple`, and per-commit SHAs.
157+
If a tag hasn't been prebuilt yet (e.g. a PR branch), fall back to on-the-fly
158+
conversion: `apptainer pull docker://ghcr.io/openms/streamlit-template:<tag>`.
159+
Requires apptainer 1.1+ or singularity-ce 3.10+ for the `oras://` transport.
160+
153161
The entrypoint auto-detects the read-only root filesystem (set by apptainer's
154162
default isolation) and switches its runtime state — Redis data directory,
155163
nginx config, PID files — to `/tmp/openms-runtime-$$`, which is always

0 commit comments

Comments
 (0)