Skip to content

Commit bdfb882

Browse files
Add Foundry publish workflow + versioned tags
1 parent 53c6847 commit bdfb882

File tree

4 files changed

+219
-36
lines changed

4 files changed

+219
-36
lines changed

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ DSPY_MODEL_NAME=nvidia/nemotron-3-nano-30b-a3b:free
4141
# ============================================================================
4242
# These map to GitHub Actions secrets/variables used by:
4343
# - .github/workflows/publish-platform-images.yml
44+
# - .github/workflows/publish-foundry.yml
4445
# - .github/workflows/railway-preview-smoke.yml
4546
# - .github/workflows/railway-production-smoke.yml
4647
#
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
name: publish-foundry
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- ".github/workflows/publish-foundry.yml"
7+
- "Dockerfile"
8+
- "openapi.foundry.json"
9+
- "scripts/deploy/foundry_openapi.py"
10+
- "src/api/**"
11+
- "src/common/**"
12+
- "src/serving/**"
13+
push:
14+
tags:
15+
- "v*"
16+
workflow_dispatch:
17+
inputs:
18+
release_version:
19+
description: "Optional release version (e.g. 0.1.2). When set, publishes versioned tag to Foundry."
20+
required: false
21+
type: string
22+
23+
permissions:
24+
contents: read
25+
26+
jobs:
27+
publish:
28+
name: Build and push image
29+
runs-on: ubuntu-latest
30+
timeout-minutes: 45
31+
32+
steps:
33+
- name: Checkout
34+
uses: actions/checkout@v4
35+
36+
- name: Setup Python
37+
uses: actions/setup-python@v5
38+
with:
39+
python-version: "3.13"
40+
41+
- name: Setup uv
42+
uses: astral-sh/setup-uv@v5
43+
44+
- name: Generate + validate Foundry OpenAPI artifact
45+
run: |
46+
uv run python scripts/deploy/foundry_openapi.py \
47+
--generate \
48+
--spec-path openapi.foundry.json \
49+
--server-url http://localhost:5000
50+
uv run python scripts/deploy/foundry_openapi.py \
51+
--spec-path openapi.foundry.json \
52+
--server-url http://localhost:5000
53+
git diff --exit-code -- openapi.foundry.json
54+
55+
- name: Setup Docker Buildx
56+
uses: docker/setup-buildx-action@v3
57+
58+
- name: Resolve tags
59+
id: meta
60+
env:
61+
FOUNDRY_REGISTRY_HOST: ${{ secrets.FOUNDRY_REGISTRY_HOST }}
62+
FOUNDRY_DOCKER_IMAGE_NAME: ${{ secrets.FOUNDRY_DOCKER_IMAGE_NAME }}
63+
run: |
64+
set -euo pipefail
65+
66+
short_sha="${GITHUB_SHA::12}"
67+
publish="false"
68+
release_version=""
69+
70+
if [[ "${GITHUB_REF_TYPE:-}" == "tag" && "${GITHUB_REF_NAME:-}" == v* ]]; then
71+
publish="true"
72+
release_version="${GITHUB_REF_NAME#v}"
73+
fi
74+
75+
if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" && -n "${{ inputs.release_version }}" ]]; then
76+
publish="true"
77+
release_version="${{ inputs.release_version }}"
78+
fi
79+
80+
if [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then
81+
image_ref="local/foundry"
82+
tag="${image_ref}:pr-${short_sha}"
83+
elif [[ "${publish}" == "true" ]]; then
84+
: "${FOUNDRY_REGISTRY_HOST:?missing FOUNDRY_REGISTRY_HOST secret}"
85+
: "${FOUNDRY_DOCKER_IMAGE_NAME:?missing FOUNDRY_DOCKER_IMAGE_NAME secret}"
86+
image_ref="${FOUNDRY_REGISTRY_HOST}/${FOUNDRY_DOCKER_IMAGE_NAME}"
87+
tag="${image_ref}:${release_version}"
88+
else
89+
# Manual build without release_version still validates the image, but does not push.
90+
image_ref="local/foundry"
91+
tag="${image_ref}:manual-${short_sha}"
92+
fi
93+
94+
{
95+
echo "image_ref=${image_ref}"
96+
echo "tag=${tag}"
97+
echo "publish=${publish}"
98+
echo "release_version=${release_version}"
99+
} >> "${GITHUB_OUTPUT}"
100+
101+
- name: Serialize Foundry OpenAPI label
102+
id: openapi
103+
run: |
104+
value="$(uv run python -c 'import json; print(json.dumps(json.load(open("openapi.foundry.json", encoding="utf-8")), separators=(",", ":")))')"
105+
{
106+
echo "value<<EOF"
107+
echo "${value}"
108+
echo "EOF"
109+
} >> "${GITHUB_OUTPUT}"
110+
111+
- name: Login to Foundry registry
112+
if: steps.meta.outputs.publish == 'true'
113+
env:
114+
FOUNDRY_ARTIFACT_REPOSITORY_RID: ${{ secrets.FOUNDRY_ARTIFACT_REPOSITORY_RID }}
115+
FOUNDRY_TOKEN: ${{ secrets.FOUNDRY_TOKEN }}
116+
FOUNDRY_REGISTRY_HOST: ${{ secrets.FOUNDRY_REGISTRY_HOST }}
117+
run: |
118+
set -euo pipefail
119+
: "${FOUNDRY_ARTIFACT_REPOSITORY_RID:?missing FOUNDRY_ARTIFACT_REPOSITORY_RID secret}"
120+
: "${FOUNDRY_TOKEN:?missing FOUNDRY_TOKEN secret}"
121+
: "${FOUNDRY_REGISTRY_HOST:?missing FOUNDRY_REGISTRY_HOST secret}"
122+
echo "${FOUNDRY_TOKEN}" | docker login -u "${FOUNDRY_ARTIFACT_REPOSITORY_RID}" --password-stdin "${FOUNDRY_REGISTRY_HOST}"
123+
124+
- name: Build (and maybe push)
125+
env:
126+
OPENAPI_JSON: ${{ steps.openapi.outputs.value }}
127+
TAG: ${{ steps.meta.outputs.tag }}
128+
PUBLISH: ${{ steps.meta.outputs.publish }}
129+
run: |
130+
set -euo pipefail
131+
132+
if [[ "${PUBLISH}" == "true" ]]; then
133+
docker buildx build \
134+
--platform linux/amd64 \
135+
--build-arg SERVER_OPENAPI="${OPENAPI_JSON}" \
136+
--tag "${TAG}" \
137+
--provenance=false \
138+
--sbom=false \
139+
--push \
140+
.
141+
else
142+
docker buildx build \
143+
--platform linux/amd64 \
144+
--build-arg SERVER_OPENAPI="${OPENAPI_JSON}" \
145+
--tag "${TAG}" \
146+
--provenance=false \
147+
--sbom=false \
148+
--load \
149+
.
150+
fi
151+
152+
- name: Validate pushed image metadata (Foundry)
153+
if: steps.meta.outputs.publish == 'true'
154+
env:
155+
TAG: ${{ steps.meta.outputs.tag }}
156+
run: |
157+
set -euo pipefail
158+
docker pull "${TAG}"
159+
uv run python scripts/deploy/foundry_openapi.py \
160+
--spec-path openapi.foundry.json \
161+
--image-ref "${TAG}" \
162+
--server-url http://localhost:5000
163+
164+
- name: Validate image metadata (PR/manual)
165+
if: steps.meta.outputs.publish != 'true'
166+
env:
167+
TAG: ${{ steps.meta.outputs.tag }}
168+
run: |
169+
set -euo pipefail
170+
uv run python scripts/deploy/foundry_openapi.py \
171+
--spec-path openapi.foundry.json \
172+
--image-ref "${TAG}" \
173+
--server-url http://localhost:5000
174+
175+
- name: Foundry runtime smoke test (PORT=5000)
176+
env:
177+
TAG: ${{ steps.meta.outputs.tag }}
178+
run: |
179+
set -euo pipefail
180+
181+
container_id="$(docker run -d --rm -e PORT=5000 -e DSPY_PROVIDER=local -p 15000:5000 "${TAG}")"
182+
trap 'docker rm -f "${container_id}" >/dev/null 2>&1 || true' RETURN
183+
184+
for _ in $(seq 1 40); do
185+
if curl -fsS "http://127.0.0.1:15000/health" >/dev/null 2>&1; then
186+
echo "Health check passed"
187+
exit 0
188+
fi
189+
sleep 1
190+
done
191+
192+
echo "Health check failed"
193+
docker logs "${container_id}" || true
194+
exit 1
195+
196+
- name: Workflow summary
197+
if: always()
198+
run: |
199+
set -euo pipefail
200+
{
201+
echo "## Foundry Docker Publish"
202+
echo ""
203+
echo "- Event: \`${GITHUB_EVENT_NAME}\`"
204+
echo "- Image: \`${{ steps.meta.outputs.tag }}\`"
205+
if [[ "${{ steps.meta.outputs.publish }}" == "true" ]]; then
206+
echo "- Publish: yes"
207+
echo "- Release version: \`${{ steps.meta.outputs.release_version }}\`"
208+
else
209+
echo "- Publish: no (build + validate only)"
210+
fi
211+
} >> "${GITHUB_STEP_SUMMARY}"
212+

.github/workflows/publish-platform-images.yml

Lines changed: 4 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ on:
99
workflow_dispatch:
1010
inputs:
1111
release_version:
12-
description: "Optional release version (e.g. 0.1.2). When set, publishes versioned tags to Foundry + GHCR."
12+
description: "Optional release version (e.g. 0.1.2). When set, publishes versioned tags to GHCR."
1313
required: false
1414
type: string
1515

@@ -41,45 +41,30 @@ jobs:
4141
id: tags
4242
env:
4343
GHCR_IMAGE: ghcr.io/${{ github.repository }}
44-
FOUNDRY_REGISTRY_HOST: ${{ secrets.FOUNDRY_REGISTRY_HOST }}
45-
FOUNDRY_DOCKER_IMAGE_NAME: ${{ secrets.FOUNDRY_DOCKER_IMAGE_NAME }}
4644
run: |
4745
set -euo pipefail
4846
4947
image_tag="$(date -u +%Y%m%d-%H%M%S)-${GITHUB_SHA::7}"
5048
short_sha="${GITHUB_SHA::12}"
5149
52-
publish_foundry="false"
5350
release_version=""
5451
5552
if [[ "${GITHUB_REF_TYPE:-}" == "tag" && "${GITHUB_REF_NAME:-}" == v* ]]; then
56-
publish_foundry="true"
5753
release_version="${GITHUB_REF_NAME#v}"
5854
fi
5955
6056
if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" && -n "${{ inputs.release_version }}" ]]; then
61-
publish_foundry="true"
6257
release_version="${{ inputs.release_version }}"
6358
fi
6459
65-
foundry_image=""
66-
if [[ "${publish_foundry}" == "true" ]]; then
67-
: "${FOUNDRY_REGISTRY_HOST:?missing FOUNDRY_REGISTRY_HOST secret}"
68-
: "${FOUNDRY_DOCKER_IMAGE_NAME:?missing FOUNDRY_DOCKER_IMAGE_NAME secret}"
69-
foundry_image="${FOUNDRY_REGISTRY_HOST}/${FOUNDRY_DOCKER_IMAGE_NAME}"
70-
fi
71-
7260
{
7361
echo "ghcr_image=${GHCR_IMAGE}"
74-
echo "foundry_image=${foundry_image}"
7562
echo "image_tag=${image_tag}"
76-
echo "publish_foundry=${publish_foundry}"
7763
echo "release_version=${release_version}"
7864
echo "tags<<EOF"
79-
if [[ "${publish_foundry}" == "true" ]]; then
65+
if [[ -n "${release_version}" ]]; then
8066
echo "${GHCR_IMAGE}:${release_version}"
8167
echo "${GHCR_IMAGE}:sha-${short_sha}"
82-
echo "${foundry_image}:${release_version}"
8368
else
8469
echo "${GHCR_IMAGE}:${image_tag}"
8570
echo "${GHCR_IMAGE}:main-${short_sha}"
@@ -114,20 +99,7 @@ jobs:
11499
username: ${{ github.actor }}
115100
password: ${{ secrets.GITHUB_TOKEN }}
116101

117-
- name: Login to Foundry registry
118-
if: steps.tags.outputs.publish_foundry == 'true'
119-
env:
120-
FOUNDRY_ARTIFACT_REPOSITORY_RID: ${{ secrets.FOUNDRY_ARTIFACT_REPOSITORY_RID }}
121-
FOUNDRY_TOKEN: ${{ secrets.FOUNDRY_TOKEN }}
122-
FOUNDRY_REGISTRY_HOST: ${{ secrets.FOUNDRY_REGISTRY_HOST }}
123-
run: |
124-
set -euo pipefail
125-
: "${FOUNDRY_ARTIFACT_REPOSITORY_RID:?missing FOUNDRY_ARTIFACT_REPOSITORY_RID secret}"
126-
: "${FOUNDRY_TOKEN:?missing FOUNDRY_TOKEN secret}"
127-
: "${FOUNDRY_REGISTRY_HOST:?missing FOUNDRY_REGISTRY_HOST secret}"
128-
echo "${FOUNDRY_TOKEN}" | docker login -u "${FOUNDRY_ARTIFACT_REPOSITORY_RID}" --password-stdin "${FOUNDRY_REGISTRY_HOST}"
129-
130-
- name: Build once and publish to GHCR + Foundry
102+
- name: Build once and publish to GHCR
131103
env:
132104
TAGS: ${{ steps.tags.outputs.tags }}
133105
run: |
@@ -158,11 +130,8 @@ jobs:
158130
echo ""
159131
echo "- Event: \`${GITHUB_EVENT_NAME}\`"
160132
echo "- GHCR image: \`${{ steps.tags.outputs.ghcr_image }}\`"
161-
if [[ "${{ steps.tags.outputs.publish_foundry }}" == "true" ]]; then
162-
echo "- Foundry image: \`${{ steps.tags.outputs.foundry_image }}\`"
133+
if [[ -n "${{ steps.tags.outputs.release_version }}" ]]; then
163134
echo "- Release version: \`${{ steps.tags.outputs.release_version }}\`"
164-
else
165-
echo "- Foundry publish: skipped"
166135
fi
167136
echo "- Immutable tag: \`${{ steps.tags.outputs.image_tag }}\`"
168137
echo "- Tags:"

.github/workflows/release-version.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,14 @@ jobs:
8888
git tag -a "${tag}" "${sha}" -m "Release ${tag}"
8989
git push origin "refs/tags/${tag}"
9090
91-
- name: Trigger publish for release tag
91+
- name: Trigger publishes for release tag
9292
if: steps.exists.outputs.tag_exists != 'true'
9393
run: |
9494
set -euo pipefail
9595
tag="${{ steps.version.outputs.tag }}"
9696
# Tags pushed by workflows do not reliably trigger tag-push workflows, so dispatch explicitly.
9797
gh workflow run publish-platform-images.yml --repo "${REPO}" --ref "${tag}"
98+
gh workflow run publish-foundry.yml --repo "${REPO}" --ref "${tag}"
9899
99100
- name: Workflow summary
100101
run: |

0 commit comments

Comments
 (0)