Skip to content
Merged
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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ DSPY_MODEL_NAME=nvidia/nemotron-3-nano-30b-a3b:free
# ============================================================================
# These map to GitHub Actions secrets/variables used by:
# - .github/workflows/publish-platform-images.yml
# - .github/workflows/publish-foundry.yml
# - .github/workflows/railway-preview-smoke.yml
# - .github/workflows/railway-production-smoke.yml
#
Expand Down
212 changes: 212 additions & 0 deletions .github/workflows/publish-foundry.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
name: publish-foundry

on:
pull_request:
paths:
- ".github/workflows/publish-foundry.yml"
- "Dockerfile"
- "openapi.foundry.json"
- "scripts/deploy/foundry_openapi.py"
- "src/api/**"
- "src/common/**"
- "src/serving/**"
push:
tags:
- "v*"
workflow_dispatch:
inputs:
release_version:
description: "Optional release version (e.g. 0.1.2). When set, publishes versioned tag to Foundry."
required: false
type: string

permissions:
contents: read

jobs:
publish:
name: Build and push image
runs-on: ubuntu-latest
timeout-minutes: 45

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.13"

- name: Setup uv
uses: astral-sh/setup-uv@v5

- name: Generate + validate Foundry OpenAPI artifact
run: |
uv run python scripts/deploy/foundry_openapi.py \
--generate \
--spec-path openapi.foundry.json \
--server-url http://localhost:5000
uv run python scripts/deploy/foundry_openapi.py \
--spec-path openapi.foundry.json \
--server-url http://localhost:5000
git diff --exit-code -- openapi.foundry.json

- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Resolve tags
id: meta
env:
FOUNDRY_REGISTRY_HOST: ${{ secrets.FOUNDRY_REGISTRY_HOST }}
FOUNDRY_DOCKER_IMAGE_NAME: ${{ secrets.FOUNDRY_DOCKER_IMAGE_NAME }}
run: |
set -euo pipefail

short_sha="${GITHUB_SHA::12}"
publish="false"
release_version=""

if [[ "${GITHUB_REF_TYPE:-}" == "tag" && "${GITHUB_REF_NAME:-}" == v* ]]; then
publish="true"
release_version="${GITHUB_REF_NAME#v}"
fi

if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" && -n "${{ inputs.release_version }}" ]]; then
publish="true"
release_version="${{ inputs.release_version }}"
fi

if [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then
image_ref="local/foundry"
tag="${image_ref}:pr-${short_sha}"
elif [[ "${publish}" == "true" ]]; then
: "${FOUNDRY_REGISTRY_HOST:?missing FOUNDRY_REGISTRY_HOST secret}"
: "${FOUNDRY_DOCKER_IMAGE_NAME:?missing FOUNDRY_DOCKER_IMAGE_NAME secret}"
image_ref="${FOUNDRY_REGISTRY_HOST}/${FOUNDRY_DOCKER_IMAGE_NAME}"
tag="${image_ref}:${release_version}"
else
# Manual build without release_version still validates the image, but does not push.
image_ref="local/foundry"
tag="${image_ref}:manual-${short_sha}"
fi

{
echo "image_ref=${image_ref}"
echo "tag=${tag}"
echo "publish=${publish}"
echo "release_version=${release_version}"
} >> "${GITHUB_OUTPUT}"

- name: Serialize Foundry OpenAPI label
id: openapi
run: |
value="$(uv run python -c 'import json; print(json.dumps(json.load(open("openapi.foundry.json", encoding="utf-8")), separators=(",", ":")))')"
{
echo "value<<EOF"
echo "${value}"
echo "EOF"
} >> "${GITHUB_OUTPUT}"

- name: Login to Foundry registry
if: steps.meta.outputs.publish == 'true'
env:
FOUNDRY_ARTIFACT_REPOSITORY_RID: ${{ secrets.FOUNDRY_ARTIFACT_REPOSITORY_RID }}
FOUNDRY_TOKEN: ${{ secrets.FOUNDRY_TOKEN }}
FOUNDRY_REGISTRY_HOST: ${{ secrets.FOUNDRY_REGISTRY_HOST }}
run: |
set -euo pipefail
: "${FOUNDRY_ARTIFACT_REPOSITORY_RID:?missing FOUNDRY_ARTIFACT_REPOSITORY_RID secret}"
: "${FOUNDRY_TOKEN:?missing FOUNDRY_TOKEN secret}"
: "${FOUNDRY_REGISTRY_HOST:?missing FOUNDRY_REGISTRY_HOST secret}"
echo "${FOUNDRY_TOKEN}" | docker login -u "${FOUNDRY_ARTIFACT_REPOSITORY_RID}" --password-stdin "${FOUNDRY_REGISTRY_HOST}"

- name: Build (and maybe push)
env:
OPENAPI_JSON: ${{ steps.openapi.outputs.value }}
TAG: ${{ steps.meta.outputs.tag }}
PUBLISH: ${{ steps.meta.outputs.publish }}
run: |
set -euo pipefail

if [[ "${PUBLISH}" == "true" ]]; then
docker buildx build \
--platform linux/amd64 \
--build-arg SERVER_OPENAPI="${OPENAPI_JSON}" \
--tag "${TAG}" \
--provenance=false \
--sbom=false \
--push \
.
else
docker buildx build \
--platform linux/amd64 \
--build-arg SERVER_OPENAPI="${OPENAPI_JSON}" \
--tag "${TAG}" \
--provenance=false \
--sbom=false \
--load \
.
fi

- name: Validate pushed image metadata (Foundry)
if: steps.meta.outputs.publish == 'true'
env:
TAG: ${{ steps.meta.outputs.tag }}
run: |
set -euo pipefail
docker pull "${TAG}"
uv run python scripts/deploy/foundry_openapi.py \
--spec-path openapi.foundry.json \
--image-ref "${TAG}" \
--server-url http://localhost:5000

- name: Validate image metadata (PR/manual)
if: steps.meta.outputs.publish != 'true'
env:
TAG: ${{ steps.meta.outputs.tag }}
run: |
set -euo pipefail
uv run python scripts/deploy/foundry_openapi.py \
--spec-path openapi.foundry.json \
--image-ref "${TAG}" \
--server-url http://localhost:5000

- name: Foundry runtime smoke test (PORT=5000)
env:
TAG: ${{ steps.meta.outputs.tag }}
run: |
set -euo pipefail

container_id="$(docker run -d --rm -e PORT=5000 -e DSPY_PROVIDER=local -p 15000:5000 "${TAG}")"
trap 'docker rm -f "${container_id}" >/dev/null 2>&1 || true' RETURN

for _ in $(seq 1 40); do
if curl -fsS "http://127.0.0.1:15000/health" >/dev/null 2>&1; then
echo "Health check passed"
exit 0
fi
sleep 1
done

echo "Health check failed"
docker logs "${container_id}" || true
exit 1

- name: Workflow summary
if: always()
run: |
set -euo pipefail
{
echo "## Foundry Docker Publish"
echo ""
echo "- Event: \`${GITHUB_EVENT_NAME}\`"
echo "- Image: \`${{ steps.meta.outputs.tag }}\`"
if [[ "${{ steps.meta.outputs.publish }}" == "true" ]]; then
echo "- Publish: yes"
echo "- Release version: \`${{ steps.meta.outputs.release_version }}\`"
else
echo "- Publish: no (build + validate only)"
fi
} >> "${GITHUB_STEP_SUMMARY}"

39 changes: 4 additions & 35 deletions .github/workflows/publish-platform-images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ on:
workflow_dispatch:
inputs:
release_version:
description: "Optional release version (e.g. 0.1.2). When set, publishes versioned tags to Foundry + GHCR."
description: "Optional release version (e.g. 0.1.2). When set, publishes versioned tags to GHCR."
required: false
type: string

Expand Down Expand Up @@ -41,45 +41,30 @@ jobs:
id: tags
env:
GHCR_IMAGE: ghcr.io/${{ github.repository }}
FOUNDRY_REGISTRY_HOST: ${{ secrets.FOUNDRY_REGISTRY_HOST }}
FOUNDRY_DOCKER_IMAGE_NAME: ${{ secrets.FOUNDRY_DOCKER_IMAGE_NAME }}
run: |
set -euo pipefail

image_tag="$(date -u +%Y%m%d-%H%M%S)-${GITHUB_SHA::7}"
short_sha="${GITHUB_SHA::12}"

publish_foundry="false"
release_version=""

if [[ "${GITHUB_REF_TYPE:-}" == "tag" && "${GITHUB_REF_NAME:-}" == v* ]]; then
publish_foundry="true"
release_version="${GITHUB_REF_NAME#v}"
fi

if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" && -n "${{ inputs.release_version }}" ]]; then
publish_foundry="true"
release_version="${{ inputs.release_version }}"
fi

foundry_image=""
if [[ "${publish_foundry}" == "true" ]]; then
: "${FOUNDRY_REGISTRY_HOST:?missing FOUNDRY_REGISTRY_HOST secret}"
: "${FOUNDRY_DOCKER_IMAGE_NAME:?missing FOUNDRY_DOCKER_IMAGE_NAME secret}"
foundry_image="${FOUNDRY_REGISTRY_HOST}/${FOUNDRY_DOCKER_IMAGE_NAME}"
fi

{
echo "ghcr_image=${GHCR_IMAGE}"
echo "foundry_image=${foundry_image}"
echo "image_tag=${image_tag}"
echo "publish_foundry=${publish_foundry}"
echo "release_version=${release_version}"
echo "tags<<EOF"
if [[ "${publish_foundry}" == "true" ]]; then
if [[ -n "${release_version}" ]]; then
echo "${GHCR_IMAGE}:${release_version}"
echo "${GHCR_IMAGE}:sha-${short_sha}"
echo "${foundry_image}:${release_version}"
else
echo "${GHCR_IMAGE}:${image_tag}"
echo "${GHCR_IMAGE}:main-${short_sha}"
Expand Down Expand Up @@ -114,20 +99,7 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Login to Foundry registry
if: steps.tags.outputs.publish_foundry == 'true'
env:
FOUNDRY_ARTIFACT_REPOSITORY_RID: ${{ secrets.FOUNDRY_ARTIFACT_REPOSITORY_RID }}
FOUNDRY_TOKEN: ${{ secrets.FOUNDRY_TOKEN }}
FOUNDRY_REGISTRY_HOST: ${{ secrets.FOUNDRY_REGISTRY_HOST }}
run: |
set -euo pipefail
: "${FOUNDRY_ARTIFACT_REPOSITORY_RID:?missing FOUNDRY_ARTIFACT_REPOSITORY_RID secret}"
: "${FOUNDRY_TOKEN:?missing FOUNDRY_TOKEN secret}"
: "${FOUNDRY_REGISTRY_HOST:?missing FOUNDRY_REGISTRY_HOST secret}"
echo "${FOUNDRY_TOKEN}" | docker login -u "${FOUNDRY_ARTIFACT_REPOSITORY_RID}" --password-stdin "${FOUNDRY_REGISTRY_HOST}"

- name: Build once and publish to GHCR + Foundry
- name: Build once and publish to GHCR
env:
TAGS: ${{ steps.tags.outputs.tags }}
run: |
Expand Down Expand Up @@ -158,11 +130,8 @@ jobs:
echo ""
echo "- Event: \`${GITHUB_EVENT_NAME}\`"
echo "- GHCR image: \`${{ steps.tags.outputs.ghcr_image }}\`"
if [[ "${{ steps.tags.outputs.publish_foundry }}" == "true" ]]; then
echo "- Foundry image: \`${{ steps.tags.outputs.foundry_image }}\`"
if [[ -n "${{ steps.tags.outputs.release_version }}" ]]; then
echo "- Release version: \`${{ steps.tags.outputs.release_version }}\`"
else
echo "- Foundry publish: skipped"
fi
echo "- Immutable tag: \`${{ steps.tags.outputs.image_tag }}\`"
echo "- Tags:"
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/release-version.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,14 @@ jobs:
git tag -a "${tag}" "${sha}" -m "Release ${tag}"
git push origin "refs/tags/${tag}"

- name: Trigger publish for release tag
- name: Trigger publishes for release tag
if: steps.exists.outputs.tag_exists != 'true'
run: |
set -euo pipefail
tag="${{ steps.version.outputs.tag }}"
# Tags pushed by workflows do not reliably trigger tag-push workflows, so dispatch explicitly.
gh workflow run publish-platform-images.yml --repo "${REPO}" --ref "${tag}"
gh workflow run publish-foundry.yml --repo "${REPO}" --ref "${tag}"

- name: Workflow summary
run: |
Expand Down