name: Release
# Trigger after CI succeeds to avoid releasing before checks finish
on:
workflow_run:
workflows: ["CI"]
types: [completed]
branches:
- main
workflow_dispatch:
# Prevent fork to access secrets (release workflow should only run on main repo)
concurrency:
group: releases
cancel-in-progress: false
env:
PYTHON_VERSION: "3.14"
jobs:
# Skip tests - they already passed in CI before merge to main and in Main Branch Validation
# This workflow focuses on building and publishing the release
validate:
if: |
github.repository == 'kpeacocke/souschef'
&& ((github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'main')
|| (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main'))
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: write # Need write to create tags
actions: read # Needed to verify CodeQL run status
outputs:
tag: ${{ steps.get-tag.outputs.tag }}
version: ${{ steps.get-tag.outputs.version }}
tag-exists: ${{ steps.check-tag.outputs.exists }}
pypi-exists: ${{ steps.check-pypi.outputs.exists }}
docker-exists: ${{ steps.check-docker.outputs.exists }}
docker-mcp-exists: ${{ steps.check-docker-mcp.outputs.exists }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: kpeacocke/souschef # Explicitly hardcode to prevent fork execution
ref: main # Always checkout main branch
fetch-depth: 0
fetch-tags: true
- name: Verify code is from main branch
env:
BRANCH: ${{ github.event.workflow_run.head_branch || github.ref_name }}
run: |
if [[ -z "$BRANCH" ]] || [[ "$BRANCH" != "main" ]]; then
printf 'ERROR: Release workflow can only run on main branch, got: %s\n' "$BRANCH" >&2
exit 1
fi
echo "Verified: code is from main branch"
- name: Ensure CodeQL scan succeeded for this commit (best-effort)
if: github.event_name == 'workflow_run'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: kpeacocke/souschef # Explicitly hardcode repository
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
echo "Checking CodeQL status for $HEAD_SHA"
api_url="https://api.github.com/repos/${REPO}/actions/workflows/codeql.yml/runs?head_sha=${HEAD_SHA}"
run_json=$(curl -s --proto '=https' -H "Authorization: Bearer ${GH_TOKEN}" -H "Accept: application/vnd.github+json" "$api_url") || true
conclusion=$(echo "$run_json" | jq -r '.workflow_runs[0].conclusion // "none"')
if [ "$conclusion" != "success" ]; then
echo "Warning: CodeQL workflow not successful for $HEAD_SHA (conclusion=$conclusion). Proceeding because Main Branch Validation succeeded."
else
echo "CodeQL succeeded for $HEAD_SHA"
fi
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Determine version from pyproject.toml
id: get-tag
run: |
VERSION=$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['tool']['poetry']['version'])")
TAG="v${VERSION}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "tag=$TAG" >> $GITHUB_OUTPUT
echo "Version from pyproject.toml: $VERSION"
echo "Release tag: $TAG"
- name: Check remote for existing tag
id: check-tag
env:
TAG: ${{ steps.get-tag.outputs.tag }}
run: |
if git ls-remote --tags origin "$TAG" | grep -q "refs/tags/${TAG}$"; then
echo "exists=true" >> $GITHUB_OUTPUT
echo "Tag $TAG already exists on remote"
else
echo "exists=false" >> $GITHUB_OUTPUT
echo "Tag $TAG does not exist on remote"
fi
- name: Check PyPI for existing version
id: check-pypi
env:
VERSION: ${{ steps.get-tag.outputs.version }}
run: |
set -e
exists=$(curl -fsSL --proto '=https' https://pypi.org/pypi/souschef/json | jq -r --arg v "$VERSION" '.releases[$v] | if . == null then "false" else "true" end') || exists=false
echo "exists=$exists" >> $GITHUB_OUTPUT
if [ "$exists" = "true" ]; then
echo "Version $VERSION already exists on PyPI"
else
echo "Version $VERSION not found on PyPI"
fi
- name: Check Docker registry for existing image
id: check-docker
env:
VERSION: ${{ steps.get-tag.outputs.version }}
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Check if the Docker image with this tag exists in GHCR
IMAGE="ghcr.io/${{ github.repository_owner }}/souschef:$VERSION"
if docker manifest inspect "$IMAGE" >/dev/null 2>&1 || \
curl -fsSL -H "Authorization: Bearer $GHCR_TOKEN" \
"https://ghcr.io/v2/${{ github.repository_owner }}/souschef/manifests/$VERSION" >/dev/null 2>&1; then
echo "exists=true" >> $GITHUB_OUTPUT
echo "Docker image $IMAGE already exists"
else
echo "exists=false" >> $GITHUB_OUTPUT
echo "Docker image $IMAGE not found"
fi
- name: Check Docker registry for existing MCP image
id: check-docker-mcp
env:
VERSION: ${{ steps.get-tag.outputs.version }}
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Check if the MCP Docker image with this tag exists in GHCR
IMAGE="ghcr.io/${{ github.repository_owner }}/souschef-mcp:$VERSION"
if docker manifest inspect "$IMAGE" >/dev/null 2>&1 || \
curl -fsSL -H "Authorization: Bearer $GHCR_TOKEN" \
"https://ghcr.io/v2/${{ github.repository_owner }}/souschef-mcp/manifests/$VERSION" >/dev/null 2>&1; then
echo "exists=true" >> $GITHUB_OUTPUT
echo "Docker MCP image $IMAGE already exists"
else
echo "exists=false" >> $GITHUB_OUTPUT
echo "Docker MCP image $IMAGE not found"
fi
build-package:
runs-on: ubuntu-latest
timeout-minutes: 10
needs: validate
# Always build the package - needed for all downstream jobs
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Checkout from main branch of main repo (never from fork). Safe because
# workflow only runs after CI succeeds on main branch. The tag does not
# exist yet (created later in create-release), so using the tag reference
# here would cause checkout failures.
repository: kpeacocke/souschef # Explicitly hardcode to prevent fork execution
ref: main # Always checkout main branch, not fork refs
fetch-depth: 0
persist-credentials: false # No credentials needed for build-only job
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install Poetry
uses: snok/install-poetry@76e04a911780d5b312d89783f7b1cd627778900a # v1.4.1
with:
version: latest
virtualenvs-create: true
virtualenvs-in-project: true
- name: Cache Poetry dependencies
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: .venv
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
restore-keys: |
${{ runner.os }}-poetry-
- name: Install dependencies
run: poetry install --no-interaction
- name: Install Twine (for package validation)
run: poetry run python -m pip install --upgrade pip twine
- name: Build Python package
run: poetry build
- name: Validate package with twine
run: poetry run twine check dist/*
- name: Upload package artifacts
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: release-packages
path: dist/
retention-days: 7
publish-pypi:
runs-on: ubuntu-latest
timeout-minutes: 10
needs: [validate, build-package, create-release]
# Only skip if version already exists on PyPI
if: needs.validate.outputs.pypi-exists != 'true'
environment:
name: pypi
url: https://pypi.org/project/souschef/
permissions:
contents: read
id-token: write # Required for PyPI Trusted Publishing (OIDC)
deployments: write # For deployment status updates
steps:
- name: Start deployment
uses: bobheadxi/deployments@648679e8e4915b27893bd7dbc35cb504dc915bc8 # v1.5.0
id: deployment
with:
step: start
token: ${{ secrets.GITHUB_TOKEN }}
env: pypi
ref: ${{ needs.validate.outputs.tag }}
- name: Download package artifacts
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: release-packages
path: dist/
- name: Publish package to PyPI
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
with:
attestations: true
- name: Update deployment status (success)
uses: bobheadxi/deployments@648679e8e4915b27893bd7dbc35cb504dc915bc8 # v1.5.0
if: success()
with:
step: finish
token: ${{ secrets.GITHUB_TOKEN }}
status: ${{ job.status }}
deployment_id: ${{ steps.deployment.outputs.deployment_id }}
env: pypi
env_url: https://pypi.org/project/souschef/${{ needs.validate.outputs.version }}/
- name: Update deployment status (failure)
uses: bobheadxi/deployments@648679e8e4915b27893bd7dbc35cb504dc915bc8 # v1.5.0
if: failure()
with:
step: finish
token: ${{ secrets.GITHUB_TOKEN }}
status: ${{ job.status }}
deployment_id: ${{ steps.deployment.outputs.deployment_id }}
env: pypi
create-release:
runs-on: ubuntu-latest
timeout-minutes: 5
needs: [validate, build-package]
# Only skip if tag already exists
if: needs.validate.outputs.tag-exists != 'true'
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: kpeacocke/souschef # Explicitly hardcode to prevent fork execution
ref: main
fetch-depth: 0
fetch-tags: true
# persist-credentials: true is implicit and needed to push tags
- name: Create git tag and GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ needs.validate.outputs.tag }}
VERSION: ${{ needs.validate.outputs.version }}
run: |
# Create and push tag (tag main HEAD explicitly)
git tag "$TAG"
git push origin "$TAG"
# Create GitHub release with auto-generated notes
gh release create "$TAG" \
--title "Release $VERSION" \
--generate-notes \
--latest
- name: Download package artifacts
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: release-packages
path: dist/
- name: Upload packages to GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ needs.validate.outputs.tag }}
run: |
echo "Attaching packages to release $TAG..."
gh release upload "$TAG" dist/*
build-and-push-docker-ui:
runs-on: ubuntu-latest
needs: [validate, publish-pypi, create-release]
# Only skip if Docker image already exists
if: needs.validate.outputs.docker-exists != 'true'
timeout-minutes: 30
permissions:
contents: read
packages: write
id-token: write
deployments: write # For deployment status updates
outputs:
image-digest: ${{ steps.build.outputs.digest }}
image-pushed: ${{ steps.build.outputs.digest != '' }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: kpeacocke/souschef # Explicitly hardcode to prevent fork execution
ref: ${{ needs.validate.outputs.tag }} # Checkout the immutable release tag
fetch-depth: 0
fetch-tags: true
- name: Start Docker deployment
uses: bobheadxi/deployments@648679e8e4915b27893bd7dbc35cb504dc915bc8 # v1.5.0
id: docker-deployment
with:
step: start
token: ${{ secrets.GITHUB_TOKEN }}
env: docker
ref: ${{ needs.validate.outputs.tag }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
with:
platforms: linux/amd64,linux/arm64
- name: Generate build metadata
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: |
ghcr.io/${{ github.repository_owner }}/souschef
docker.io/${{ secrets.DOCKERHUB_USERNAME }}/souschef
tags: |
type=semver,pattern={{version}},value=${{ needs.validate.outputs.version }}
type=semver,pattern={{major}}.{{minor}},value=${{ needs.validate.outputs.version }}
type=semver,pattern={{major}},value=${{ needs.validate.outputs.version }}
type=raw,value=latest
labels: |
org.opencontainers.image.title=SousChef
org.opencontainers.image.description=Web UI for SousChef Chef to Ansible converter
- name: Login to GitHub Container Registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker UI image
id: build
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: .
file: Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=ui
cache-to: type=gha,mode=max,scope=ui
build-args: |
PYTHON_VERSION=3.14.3
POETRY_VERSION=2.3.2
provenance: true
sbom: true
- name: Verify image digest
run: |
DIGEST="${{ steps.build.outputs.digest }}"
if [ -z "$DIGEST" ]; then
echo "ERROR: Image digest is empty"
exit 1
fi
echo "Image pushed successfully with digest: $DIGEST"
- name: Update Docker deployment status (success)
uses: bobheadxi/deployments@648679e8e4915b27893bd7dbc35cb504dc915bc8 # v1.5.0
if: success()
with:
step: finish
token: ${{ secrets.GITHUB_TOKEN }}
status: success
deployment_id: ${{ steps.docker-deployment.outputs.deployment_id }}
env: docker
env_url: https://ghcr.io/${{ github.repository_owner }}/souschef:${{ needs.validate.outputs.version }}
- name: Update Docker deployment status (failure)
uses: bobheadxi/deployments@648679e8e4915b27893bd7dbc35cb504dc915bc8 # v1.5.0
if: failure()
with:
step: finish
token: ${{ secrets.GITHUB_TOKEN }}
status: failure
deployment_id: ${{ steps.docker-deployment.outputs.deployment_id }}
env: docker
build-and-push-docker-mcp:
runs-on: ubuntu-latest
needs: [validate, publish-pypi, create-release]
# Only skip if Docker MCP image already exists
if: needs.validate.outputs.docker-mcp-exists != 'true'
timeout-minutes: 30
permissions:
contents: read
packages: write
id-token: write
deployments: write
outputs:
image-digest: ${{ steps.build.outputs.digest }}
image-pushed: ${{ steps.build.outputs.digest != '' }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: kpeacocke/souschef
ref: ${{ needs.validate.outputs.tag }} # Checkout the immutable release tag
fetch-depth: 0
fetch-tags: true
- name: Start Docker MCP deployment
uses: bobheadxi/deployments@648679e8e4915b27893bd7dbc35cb504dc915bc8 # v1.5.0
id: docker-mcp-deployment
with:
step: start
token: ${{ secrets.GITHUB_TOKEN }}
env: docker-mcp
ref: ${{ needs.validate.outputs.tag }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
with:
platforms: linux/amd64,linux/arm64
- name: Generate build metadata for MCP
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: |
ghcr.io/${{ github.repository_owner }}/souschef-mcp
docker.io/${{ secrets.DOCKERHUB_USERNAME }}/souschef-mcp
tags: |
type=semver,pattern={{version}},value=${{ needs.validate.outputs.version }}
type=semver,pattern={{major}}.{{minor}},value=${{ needs.validate.outputs.version }}
type=semver,pattern={{major}},value=${{ needs.validate.outputs.version }}
type=raw,value=latest
labels: |
org.opencontainers.image.title=SousChef MCP Server
org.opencontainers.image.description=MCP server for SousChef Chef to Ansible converter
- name: Login to GitHub Container Registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker MCP image
id: build
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: .
file: Dockerfile.mcp
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=mcp
cache-to: type=gha,mode=max,scope=mcp
build-args: |
PYTHON_VERSION=3.14.3
POETRY_VERSION=2.3.2
provenance: true
sbom: true
- name: Verify MCP image digest
run: |
DIGEST="${{ steps.build.outputs.digest }}"
if [ -z "$DIGEST" ]; then
echo "ERROR: MCP image digest is empty"
exit 1
fi
echo "MCP image pushed successfully with digest: $DIGEST"
- name: Update Docker MCP deployment status (success)
uses: bobheadxi/deployments@648679e8e4915b27893bd7dbc35cb504dc915bc8 # v1.5.0
if: success()
with:
step: finish
token: ${{ secrets.GITHUB_TOKEN }}
status: success
deployment_id: ${{ steps.docker-mcp-deployment.outputs.deployment_id }}
env: docker-mcp
env_url: https://ghcr.io/${{ github.repository_owner }}/souschef-mcp:${{ needs.validate.outputs.version }}
- name: Update Docker MCP deployment status (failure)
uses: bobheadxi/deployments@648679e8e4915b27893bd7dbc35cb504dc915bc8 # v1.5.0
if: failure()
with:
step: finish
token: ${{ secrets.GITHUB_TOKEN }}
status: failure
deployment_id: ${{ steps.docker-mcp-deployment.outputs.deployment_id }}
env: docker-mcp
scan-docker-image-ui:
runs-on: ubuntu-latest
timeout-minutes: 15
needs: [validate, build-and-push-docker-ui]
if: needs.build-and-push-docker-ui.result == 'success' && needs.build-and-push-docker-ui.outputs.image-pushed == 'true'
permissions:
packages: read
security-events: write
steps:
- name: Login to GitHub Container Registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Run Trivy vulnerability scan
uses: aquasecurity/trivy-action@c1824fd6edce30d7ab345a9989de00bbd46ef284 # v0.34.0
with:
image-ref: ghcr.io/${{ github.repository_owner }}/souschef:${{ needs.validate.outputs.version }}
format: sarif
output: trivy-results.sarif
severity: CRITICAL,HIGH
ignore-unfixed: true
exit-code: '0'
- name: Upload Trivy results to GitHub Security tab
uses: github/codeql-action/upload-sarif@9e907b5e64f6b83e7804b09294d44122997950d6 # v4
if: always()
with:
sarif_file: trivy-results.sarif
category: trivy-image-scan
- name: Display vulnerability report
if: always()
run: |
echo "Vulnerability scan results:"
echo "See GitHub Security tab for detailed report"
scan-docker-image-mcp:
runs-on: ubuntu-latest
timeout-minutes: 15
needs: [validate, build-and-push-docker-mcp]
if: needs.build-and-push-docker-mcp.result == 'success' && needs.build-and-push-docker-mcp.outputs.image-pushed == 'true'
permissions:
packages: read
security-events: write
steps:
- name: Login to GitHub Container Registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Run Trivy vulnerability scan on MCP image
uses: aquasecurity/trivy-action@c1824fd6edce30d7ab345a9989de00bbd46ef284 # v0.34.0
with:
image-ref: ghcr.io/${{ github.repository_owner }}/souschef-mcp:${{ needs.validate.outputs.version }}
format: sarif
output: trivy-results-mcp.sarif
severity: CRITICAL,HIGH
ignore-unfixed: true
exit-code: '0'
- name: Upload Trivy MCP results to GitHub Security tab
uses: github/codeql-action/upload-sarif@9e907b5e64f6b83e7804b09294d44122997950d6 # v4
if: always()
with:
sarif_file: trivy-results-mcp.sarif
category: trivy-mcp-image-scan
- name: Display MCP vulnerability report
if: always()
run: |
echo "MCP vulnerability scan results:"
echo "See GitHub Security tab for detailed report"
test-docker-image-ui:
runs-on: ubuntu-latest
timeout-minutes: 15
needs: [validate, build-and-push-docker-ui]
if: needs.build-and-push-docker-ui.result == 'success' && needs.build-and-push-docker-ui.outputs.image-pushed == 'true'
permissions:
packages: read
steps:
- name: Login to GitHub Container Registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Run container and verify it starts
run: |
set -e
# Pull the image first to ensure it's available
docker pull "ghcr.io/${{ github.repository_owner }}/souschef:${{ needs.validate.outputs.version }}"
# Test if container can start (with 10-second timeout)
timeout 10 docker run --rm \
-e STREAMLIT_SERVER_HEADLESS=true \
-e STREAMLIT_BROWSER_GATHER_USAGE_STATS=false \
"ghcr.io/${{ github.repository_owner }}/souschef:${{ needs.validate.outputs.version }}" \
/bin/true || true
echo "Container started successfully"
- name: Verify health check works
run: |
set -e
# Override the ENTRYPOINT to avoid streamlit run being prepended
OUTPUT=$(docker run --rm --entrypoint="" \
"ghcr.io/${{ github.repository_owner }}/souschef:${{ needs.validate.outputs.version }}" \
python -m souschef.ui.health_check)
echo "Health check output: $OUTPUT"
if echo "$OUTPUT" | grep -q "healthy"; then
echo "Health check passed"
else
echo "Health check failed"
exit 1
fi
test-docker-image-mcp:
runs-on: ubuntu-latest
timeout-minutes: 15
needs: [validate, build-and-push-docker-mcp]
if: needs.build-and-push-docker-mcp.result == 'success' && needs.build-and-push-docker-mcp.outputs.image-pushed == 'true'
permissions:
packages: read
steps:
- name: Login to GitHub Container Registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Pull and verify MCP image
run: |
set -e
docker pull "ghcr.io/${{ github.repository_owner }}/souschef-mcp:${{ needs.validate.outputs.version }}"
echo "MCP image pulled successfully"
- name: Test MCP server starts
run: |
set -e
# Test that the MCP server can start and respond
# Use timeout to prevent hanging, expect it to wait for stdio input
timeout 5 docker run --rm -i \
"ghcr.io/${{ github.repository_owner }}/souschef-mcp:${{ needs.validate.outputs.version }}" \
</dev/null || EXIT_CODE=$?
# Exit code 124 means timeout (expected for stdio server waiting for input)
# Exit code 0 means it exited cleanly
if [ "${EXIT_CODE:-0}" -eq 124 ] || [ "${EXIT_CODE:-0}" -eq 0 ]; then
echo "MCP server test passed (exit code: ${EXIT_CODE:-0})"
else
echo "MCP server test failed with unexpected exit code: ${EXIT_CODE:-0}"
exit 1
fi
- name: Verify MCP entrypoint
run: |
set -e
# Test that the MCP server module is accessible
docker run --rm --entrypoint="" \
"ghcr.io/${{ github.repository_owner }}/souschef-mcp:${{ needs.validate.outputs.version }}" \
python -c "import souschef.server; print('MCP server module verified')"
echo "MCP entrypoint verified"
notify-docker-deployment:
runs-on: ubuntu-latest
timeout-minutes: 5
needs: [validate, build-and-push-docker-ui, build-and-push-docker-mcp, scan-docker-image-ui, scan-docker-image-mcp, test-docker-image-ui, test-docker-image-mcp]
if: |
needs.validate.result == 'success'
&& (needs.build-and-push-docker-ui.result == 'success' || needs.build-and-push-docker-mcp.result == 'success')
&& (needs.scan-docker-image-ui.result == 'success' || needs.scan-docker-image-ui.result == 'skipped')
&& (needs.scan-docker-image-mcp.result == 'success' || needs.scan-docker-image-mcp.result == 'skipped')
&& (needs.test-docker-image-ui.result == 'success' || needs.test-docker-image-ui.result == 'skipped')
&& (needs.test-docker-image-mcp.result == 'success' || needs.test-docker-image-mcp.result == 'skipped')
permissions:
contents: read
issues: write
steps:
- name: Create deployment notification issue
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const version = "${{ needs.validate.outputs.version }}";
const registry = "ghcr.io";
const owner = "${{ github.repository_owner }}";
const uiDigest = "${{ needs.build-and-push-docker-ui.outputs.image-digest }}";
const mcpDigest = "${{ needs.build-and-push-docker-mcp.outputs.image-digest }}";
const tag = "${{ needs.validate.outputs.tag }}";
const versionParts = version.split('.');
const majorMinor = versionParts.length >= 2
? versionParts[0] + "." + versionParts[1]
: versionParts[0];
const major = versionParts[0];
let body = "## Docker Images Published\n\n";
if (uiDigest) {
body += "### UI Image (Streamlit)\n\n";
body += "**Image:** `" + registry + "/" + owner + "/souschef:" + version + "`\n\n";
body += "**Digest:** " + uiDigest + "\n\n";
body += "**Pull Commands:**\n";
body += "```bash\n";
body += "docker pull " + registry + "/" + owner + "/souschef:" + version + "\n";
body += "docker pull " + registry + "/" + owner + "/souschef:latest\n";
body += "```\n\n";
}
if (mcpDigest) {
body += "### MCP Server Image\n\n";
body += "**Image:** `" + registry + "/" + owner + "/souschef-mcp:" + version + "`\n\n";
body += "**Digest:** " + mcpDigest + "\n\n";
body += "**Pull Commands:**\n";
body += "```bash\n";
body += "docker pull " + registry + "/" + owner + "/souschef-mcp:" + version + "\n";
body += "docker pull " + registry + "/" + owner + "/souschef-mcp:latest\n";
body += "```\n\n";
}
body += "**Available Tags:** `" + version + "`, `" + majorMinor + "`, `" + major + "`, `latest`\n\n";
body += "**Container Registry:** [GitHub Container Registry](https://github.com/${{ github.repository }}/packages)\n\n";
body += "**Security:** Vulnerability scan results available in [GitHub Security](https://github.com/${{ github.repository }}/security/dependabot)\n\n";
body += "**Related Release:** " + tag + "\n\n";
body += "## Build Information\n";
body += "- **Release Pipeline:** ${{ github.workflow }} #${{ github.run_number }}\n";
body += "- **Release Workflow Run:** ${{ github.run_id }}\n";
body += "- **Release Commit:** ${{ github.sha }}";
github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: "Docker Images Published: " + tag,
body: body,
labels: ['deployment', 'docker', 'release']
});
release-complete:
runs-on: ubuntu-latest
timeout-minutes: 5
needs: [validate, build-package, publish-pypi, create-release, build-and-push-docker-ui, build-and-push-docker-mcp, scan-docker-image-ui, scan-docker-image-mcp, test-docker-image-ui, test-docker-image-mcp, notify-docker-deployment]
if: always()
permissions:
contents: read
steps:
- name: Release Pipeline Summary
run: |
cat > /tmp/summary.md << 'EOF'
# Release Pipeline Complete
## Pipeline Status
- **Validation:** ${{ needs.validate.result }}
- **Build Package:** ${{ needs.build-package.result }}
- **PyPI Publish:** ${{ needs.publish-pypi.result }}
- **Create Release:** ${{ needs.create-release.result }}
- **Docker UI Build & Push:** ${{ needs.build-and-push-docker-ui.result }}
- **Docker MCP Build & Push:** ${{ needs.build-and-push-docker-mcp.result }}
- **Docker UI Security Scan:** ${{ needs.scan-docker-image-ui.result }}
- **Docker MCP Security Scan:** ${{ needs.scan-docker-image-mcp.result }}
- **Docker UI Test:** ${{ needs.test-docker-image-ui.result }}
- **Docker MCP Test:** ${{ needs.test-docker-image-mcp.result }}
- **Docker Notification:** ${{ needs.notify-docker-deployment.result }}
## Release Information
- **Version:** ${{ needs.validate.outputs.version || 'N/A' }}
- **Tag:** ${{ needs.validate.outputs.tag || 'N/A' }}
- **UI Image Digest:** ${{ needs.build-and-push-docker-ui.outputs.image-digest || 'No image pushed' }}
- **MCP Image Digest:** ${{ needs.build-and-push-docker-mcp.outputs.image-digest || 'No image pushed' }}
## Deployments
- **PyPI:** https://pypi.org/project/souschef/${{ needs.validate.outputs.version || 'latest' }}/
- **GitHub Release:** https://github.com/${{ github.repository }}/releases/tag/${{ needs.validate.outputs.tag || 'latest' }}
echo "- **UI Container Image:** ghcr.io/${{ github.repository_owner }}/souschef:${{ needs.validate.outputs.version || 'latest' }}"
- **MCP Container Image:** ghcr.io/${{ github.repository_owner }}/souschef-mcp:${{ needs.validate.outputs.version || 'latest' }}
## Next Steps
- PyPI package and Docker images are deployed
- See GitHub Release for artifacts and release notes
- See GitHub Security tab for Docker vulnerability scan results
- Pull the UI image: `docker pull ghcr.io/${{ github.repository_owner }}/souschef:${{ needs.validate.outputs.version || 'latest' }}`
- Pull the MCP image: `docker pull ghcr.io/${{ github.repository_owner }}/souschef-mcp:${{ needs.validate.outputs.version || 'latest' }}`
## Workflow Run Details
- **Workflow:** ${{ github.workflow }}
- **Run ID:** ${{ github.run_id }}
- **Run Number:** ${{ github.run_number }}
EOF
cat /tmp/summary.md >> $GITHUB_STEP_SUMMARY
- name: Report failures
if: |
needs.validate.result == 'failure'
|| needs.build-package.result == 'failure'
|| needs.publish-pypi.result == 'failure'
|| needs.create-release.result == 'failure'
|| needs.build-and-push-docker-ui.result == 'failure'
|| needs.build-and-push-docker-mcp.result == 'failure'
|| needs.scan-docker-image-ui.result == 'failure'
|| needs.scan-docker-image-mcp.result == 'failure'
|| needs.test-docker-image-ui.result == 'failure'
|| needs.test-docker-image-mcp.result == 'failure'
run: |
echo "### Release Pipeline Failed" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Stage | Status |" >> $GITHUB_STEP_SUMMARY
echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Validation | ${{ needs.validate.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Build Package | ${{ needs.build-package.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| PyPI Publish | ${{ needs.publish-pypi.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Create Release | ${{ needs.create-release.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Docker UI Build & Push | ${{ needs.build-and-push-docker-ui.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Docker MCP Build & Push | ${{ needs.build-and-push-docker-mcp.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Docker MCP Build & Push | ${{ needs.build-and-push-docker-mcp.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Docker UI Scan | ${{ needs.scan-docker-image-ui.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Docker MCP Scan | ${{ needs.scan-docker-image-mcp.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Docker UI Test | ${{ needs.test-docker-image-ui.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Docker MCP Test | ${{ needs.test-docker-image-mcp.result }} |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Please review the failed jobs above."
exit 1