Release #97
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" |