Skip to content

Release

Release #97

Workflow file for this run

name: Release
on:
schedule:
- cron: "17 3 * * *"
workflow_dispatch:
inputs:
release_kind:
description: "Release kind to publish."
required: true
default: beta
type: choice
options:
- beta
- stable
target_ref:
description: "Branch, tag, or commit SHA to release for manual runs."
required: true
default: main
type: string
date:
description: "Release date. Defaults to today's UTC date."
required: false
type: string
correction:
description: "Stable only: optional correction number. Leave blank to use vYYYY.M.D or the next available correction."
required: false
type: string
permissions:
contents: write
packages: write
concurrency:
group: release-${{ github.event_name == 'schedule' && 'beta-main' || format('{0}-{1}', inputs.release_kind, inputs.target_ref) }}
cancel-in-progress: false
jobs:
release:
name: Publish ${{ github.event_name == 'schedule' && 'beta' || inputs.release_kind }} release
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.event_name == 'schedule' && 'main' || inputs.target_ref }}
fetch-depth: 0
- name: Fetch release tags
run: git fetch --force --tags origin
- name: Compute release tag
id: release
env:
RELEASE_KIND: ${{ github.event_name == 'schedule' && 'beta' || inputs.release_kind }}
RELEASE_DATE: ${{ github.event.inputs.date || '' }}
RELEASE_CORRECTION: ${{ github.event.inputs.correction || '' }}
run: ./scripts/release/compute_tag.sh
- name: Skip when there are no new changes
if: steps.release.outputs.should_release != 'true'
run: |
{
echo "## ${{ steps.release.outputs.kind }} release skipped"
echo
echo "No commits found since the latest reachable release tag: \`${{ steps.release.outputs.latest_reachable_release_tag }}\`."
} >> "$GITHUB_STEP_SUMMARY"
- name: Set up Docker Buildx
if: steps.release.outputs.should_release == 'true'
uses: docker/setup-buildx-action@v4
- name: Log in to GHCR
if: steps.release.outputs.should_release == 'true'
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and publish Docker image
id: app_image
if: steps.release.outputs.should_release == 'true'
uses: docker/build-push-action@v7
with:
context: .
file: docker/backend.Dockerfile
platforms: linux/amd64
push: true
tags: ghcr.io/deep-mi/neurocade:${{ steps.release.outputs.tag }}
build-args: |
NEUROCADE_VERSION=${{ steps.release.outputs.version }}
labels: |
org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.revision=${{ steps.release.outputs.head_sha }}
org.opencontainers.image.version=${{ steps.release.outputs.version }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: mode=max
sbom: true
- name: Verify anonymous image access
if: steps.release.outputs.should_release == 'true'
env:
RELEASE_IMAGE: ghcr.io/deep-mi/neurocade:${{ steps.release.outputs.tag }}
run: |
anonymous_config="$RUNNER_TEMP/docker-anonymous"
mkdir -p "$anonymous_config"
if ! DOCKER_CONFIG="$anonymous_config" docker pull --platform linux/amd64 "$RELEASE_IMAGE"; then
echo "The GHCR package must be public before the GitHub release can be published." >&2
echo "Make the neurocade package public in the Deep-MI package settings, then rerun this workflow." >&2
exit 1
fi
- name: Install uv and rootless Apptainer
if: steps.release.outputs.should_release == 'true'
uses: astral-sh/setup-uv@v7
with:
version: "0.8.17"
- name: Install Apptainer
if: steps.release.outputs.should_release == 'true'
run: |
curl -fsSL -o "$RUNNER_TEMP/apptainer.deb" \
https://github.com/apptainer/apptainer/releases/download/v1.3.6/apptainer_1.3.6_amd64.deb
echo "2723b2928cfc30edf687723c49556ec4e013f0bf7cdb43a5a76bca7bd3c70792 $RUNNER_TEMP/apptainer.deb" | sha256sum -c -
sudo apt-get update
sudo apt-get install -y "$RUNNER_TEMP/apptainer.deb"
- name: Build release bridge artifact
if: steps.release.outputs.should_release == 'true'
run: |
bridge_dist="$RUNNER_TEMP/bridge-dist"
mkdir -p "$bridge_dist"
uv build --wheel --out-dir "$bridge_dist" packages/neurocade-runtime-tools
bridge_wheel="$(find "$bridge_dist" -maxdepth 1 -type f -name '*.whl' -print -quit)"
test -n "$bridge_wheel"
bridge_name="$(basename "$bridge_wheel")"
(cd "$bridge_dist" && sha256sum "$bridge_name" > "$bridge_name.sha256")
echo "BRIDGE_WHEEL=$bridge_wheel" >> "$GITHUB_ENV"
echo "BRIDGE_WHEEL_NAME=$bridge_name" >> "$GITHUB_ENV"
- name: Install release bridge
if: steps.release.outputs.should_release == 'true'
run: |
uv venv --python 3.12 "$RUNNER_TEMP/bridge-venv"
uv pip install --python "$RUNNER_TEMP/bridge-venv/bin/python" "$BRIDGE_WHEEL"
"$RUNNER_TEMP/bridge-venv/bin/python" -c 'import secrets; print(secrets.token_urlsafe(32))' > "$RUNNER_TEMP/bridge-token"
chmod 600 "$RUNNER_TEMP/bridge-token"
- name: Smoke-test published image
if: steps.release.outputs.should_release == 'true'
env:
RELEASE_IMAGE: ghcr.io/deep-mi/neurocade:${{ steps.release.outputs.tag }}
RELEASE_VERSION: ${{ steps.release.outputs.version }}
run: |
container=neurocade-release-smoke
mkdir -p "$RUNNER_TEMP/neurocade-data" "$RUNNER_TEMP/tool-images"
"$RUNNER_TEMP/bridge-venv/bin/neurocade-runtime-bridge" serve \
--runtime docker --data-root "$RUNNER_TEMP/neurocade-data" --image-dir "$RUNNER_TEMP/tool-images" \
--host 0.0.0.0 --port 18765 --token-file "$RUNNER_TEMP/bridge-token" &
bridge_pid=$!
trap 'docker rm -f "$container" >/dev/null 2>&1 || true; kill "$bridge_pid" >/dev/null 2>&1 || true' EXIT
./scripts/release/wait_for_http.sh http://127.0.0.1:18765/v1/health 30 1 \
"Authorization: Bearer $(<"$RUNNER_TEMP/bridge-token")"
docker run --detach --name "$container" \
--user "$(id -u):$(id -g)" \
--add-host host.docker.internal:host-gateway \
-v "$RUNNER_TEMP/neurocade-data:/data" \
-v "$RUNNER_TEMP/bridge-token:/run/neurocade/bridge-token:ro" \
-e DEPLOYMENT_PROFILE=local \
-e LOCAL_AUTH_ENABLED=true \
-e HOST_DATA_DIR=/data \
-e NEUROCADE_RUNTIME=docker \
-e NEUROCADE_BRIDGE_URL=http://host.docker.internal:18765 \
-e NEUROCADE_BRIDGE_TOKEN_FILE=/run/neurocade/bridge-token \
-e NEUROCADE_GPU_MODE=cpu \
"$RELEASE_IMAGE"
healthy=false
for _ in {1..30}; do
if docker exec "$container" python -c \
'import urllib.request; urllib.request.urlopen("http://127.0.0.1:8000/api/app/healthz", timeout=2).read()'
then
healthy=true
break
fi
sleep 2
done
if [[ "$healthy" != "true" ]]; then
docker logs "$container"
exit 1
fi
docker exec "$container" python -c '
import json
import urllib.request
with urllib.request.urlopen("http://127.0.0.1:8000/api/app/frontend-config", timeout=2) as response:
config = json.load(response)
assert response.headers["Cache-Control"] == "no-store"
assert config["local_auth_enabled"] is True
'
test "$(docker exec "$container" printenv NEUROCADE_BUILD_VERSION)" = "$RELEASE_VERSION"
- name: Convert exact release image to application SIF
if: steps.release.outputs.should_release == 'true'
env:
RELEASE_IMAGE: ghcr.io/deep-mi/neurocade:${{ steps.release.outputs.tag }}
RELEASE_DIGEST: ${{ steps.app_image.outputs.digest }}
RELEASE_VERSION: ${{ steps.release.outputs.version }}
run: |
app_sif="neurocade-app-${RELEASE_VERSION}-amd64.sif"
apptainer pull "$app_sif" "docker://${RELEASE_IMAGE}@${RELEASE_DIGEST}"
sha256sum "$app_sif" > "$app_sif.sha256"
chmod 0444 "$app_sif"
echo "APP_SIF=$app_sif" >> "$GITHUB_ENV"
- name: Create and validate Apptainer release manifest
if: steps.release.outputs.should_release == 'true'
env:
RELEASE_TAG: ${{ steps.release.outputs.tag }}
RELEASE_VERSION: ${{ steps.release.outputs.version }}
run: |
./scripts/release/release_manifest.py create \
--tag "$RELEASE_TAG" \
--version "$RELEASE_VERSION" \
--sif "$APP_SIF" \
--bridge "$BRIDGE_WHEEL_NAME" \
--output neurocade-release.json
./scripts/release/release_manifest.py read neurocade-release.json
- name: Smoke-test application SIF as non-root
if: steps.release.outputs.should_release == 'true'
run: |
mkdir -p "$RUNNER_TEMP/sif-data" "$RUNNER_TEMP/sif-images"
"$RUNNER_TEMP/bridge-venv/bin/neurocade-runtime-bridge" serve \
--runtime apptainer --data-root "$RUNNER_TEMP/sif-data" --image-dir "$RUNNER_TEMP/sif-images" \
--host 127.0.0.1 --port 18766 --token-file "$RUNNER_TEMP/bridge-token" &
bridge_pid=$!
./scripts/release/wait_for_http.sh http://127.0.0.1:18766/v1/health 30 1 \
"Authorization: Bearer $(<"$RUNNER_TEMP/bridge-token")"
apptainer exec --cleanenv --no-home --containall \
--bind "$RUNNER_TEMP/sif-data:/data" \
--bind "$RUNNER_TEMP/bridge-token:/run/neurocade/bridge-token:ro" \
--env NEUROCADE_RUNTIME=apptainer \
--env NEUROCADE_BRIDGE_URL=http://127.0.0.1:18766 \
--env NEUROCADE_BRIDGE_TOKEN_FILE=/run/neurocade/bridge-token \
--env NEUROCADE_GPU_MODE=cpu --env HOST_DATA_DIR=/data \
"$APP_SIF" python -m uvicorn api_service.main:app --host 127.0.0.1 --port 18000 &
app_pid=$!
trap 'kill "$app_pid" "$bridge_pid" >/dev/null 2>&1 || true' EXIT
./scripts/release/wait_for_http.sh http://127.0.0.1:18000/api/app/healthz 30 2
- name: Tag release commit
if: steps.release.outputs.should_release == 'true'
env:
RELEASE_TAG: ${{ steps.release.outputs.tag }}
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag -a "$RELEASE_TAG" -m "NeuroCade $RELEASE_TAG"
git push origin "refs/tags/$RELEASE_TAG"
- name: Publish GitHub release
if: steps.release.outputs.should_release == 'true'
env:
GH_TOKEN: ${{ github.token }}
RELEASE_KIND: ${{ steps.release.outputs.kind }}
RELEASE_TAG: ${{ steps.release.outputs.tag }}
RELEASE_VERSION: ${{ steps.release.outputs.version }}
RELEASE_PRERELEASE: ${{ steps.release.outputs.prerelease }}
run: |
args=(
"$RELEASE_TAG"
--repo "$GITHUB_REPOSITORY"
--generate-notes
)
if [[ "$RELEASE_PRERELEASE" == "true" ]]; then
args+=(--title "NeuroCade ${RELEASE_VERSION}" --prerelease --latest=false)
else
args+=(--title "NeuroCade ${RELEASE_VERSION}" --latest)
fi
gh release create "${args[@]}" \
"$APP_SIF" \
"$APP_SIF.sha256" \
"$BRIDGE_WHEEL" \
"$BRIDGE_WHEEL.sha256" \
neurocade-release.json
- name: Publish channel tag
if: steps.release.outputs.should_release == 'true'
env:
RELEASE_IMAGE: ghcr.io/deep-mi/neurocade:${{ steps.release.outputs.tag }}
CHANNEL_IMAGE: ghcr.io/deep-mi/neurocade:${{ steps.release.outputs.prerelease == 'true' && 'beta' || 'latest' }}
run: docker buildx imagetools create --tag "$CHANNEL_IMAGE" "$RELEASE_IMAGE"
- name: Summarize release
if: steps.release.outputs.should_release == 'true'
run: |
{
echo "## ${{ steps.release.outputs.kind }} release published"
echo
echo "- Tag: \`${{ steps.release.outputs.tag }}\`"
echo "- Commit: \`${{ steps.release.outputs.head_sha }}\`"
echo "- Docker image: \`ghcr.io/deep-mi/neurocade:${{ steps.release.outputs.tag }}\`"
echo "- Application SIF: \`$APP_SIF\`"
echo "- Release manifest: \`neurocade-release.json\`"
} >> "$GITHUB_STEP_SUMMARY"