Skip to content
Merged
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
103 changes: 102 additions & 1 deletion .github/workflows/hub-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ on:
push:
branches: [main, "feat/**", "fix/**", harness-integration]

# A newer push supersedes any older run for the same branch. In particular,
# an older main build must not finish later and move `latest` backwards.
concurrency:
group: hub-ci-${{ github.ref }}
cancel-in-progress: true

jobs:
# The fast deterministic suite: levels/config parsing, validator, prompts,
# temporal, KB, roles, and the /v1 OpenAI-compat contract (test_bridge) that
Expand Down Expand Up @@ -37,5 +43,100 @@ jobs:
steps:
- uses: actions/checkout@v4

# HUB_BUILD_REVISION must be the real commit: validate_config() rejects
# the Dockerfile's "unknown" default, so an image built without it builds
# fine and then refuses to start.
- name: Build image
run: docker build .
run: |
docker build --build-arg HUB_BUILD_REVISION="${GITHUB_SHA}" -t hub-ci-candidate .

# Building is not evidence the image runs: validate_config() executes at
# import, so a container that starts and answers /health is what proves
# the packaged configuration is actually serviceable.
- name: Verify the image starts and serves /health
run: |
docker run -d --name hub-smoke -p 8080:8080 hub-ci-candidate
for i in $(seq 1 30); do
if curl -fsS http://localhost:8080/health >/dev/null 2>&1; then
echo "healthy after ${i}s"
docker rm -f hub-smoke >/dev/null
exit 0
fi
sleep 1
done
echo "image never served /health:" >&2
docker logs hub-smoke >&2
docker rm -f hub-smoke >/dev/null
exit 1

# Preserve the tested bytes for the separately permissioned publish job.
# Pull requests prove the build and startup path but do not upload or push.
- name: Export the tested image
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
run: docker save --output /tmp/hub-ci-candidate.tar hub-ci-candidate

- name: Transfer the tested image to the publish job
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: actions/upload-artifact@v4
with:
name: hub-ci-candidate-${{ github.sha }}
path: /tmp/hub-ci-candidate.tar
retention-days: 1
if-no-files-found: error

# Only reviewed main commits may publish. This job receives the exact image
# that passed /health; it never rebuilds the source tree.
publish:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: [unit-and-contract, docker-build]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
outputs:
published-image: ${{ steps.publish.outputs.image }}
steps:
- name: Download the tested image
uses: actions/download-artifact@v4
with:
name: hub-ci-candidate-${{ github.sha }}
path: /tmp

- name: Load and verify the tested image
run: |
docker load --input /tmp/hub-ci-candidate.tar
revision="$(docker image inspect --format \
'{{index .Config.Labels "org.opencontainers.image.revision"}}' \
hub-ci-candidate)"
test "${revision}" = "${GITHUB_SHA}"

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

# Publish the exact local image that passed /health. Tags are convenient
# lookup names and can move; deployments must pin the recorded digest.
- name: Publish the tested image and record its immutable digest
id: publish
env:
IMAGE_NAME: ghcr.io/${{ github.repository }}
run: |
docker tag hub-ci-candidate "${IMAGE_NAME}:${GITHUB_SHA}"
docker tag hub-ci-candidate "${IMAGE_NAME}:latest"
docker push "${IMAGE_NAME}:${GITHUB_SHA}"
docker push "${IMAGE_NAME}:latest"
Comment thread
pmanko marked this conversation as resolved.

digest="$(docker buildx imagetools inspect \
"${IMAGE_NAME}:${GITHUB_SHA}" --format '{{.Manifest.Digest}}')"
case "${digest}" in
sha256:????????????????????????????????????????????????????????????????) ;;
*) echo "Could not resolve the published manifest digest: ${digest}" >&2; exit 1 ;;
esac

immutable_image="${IMAGE_NAME}@${digest}"
echo "image=${immutable_image}" >> "${GITHUB_OUTPUT}"
echo "### Published tested Hub image" >> "${GITHUB_STEP_SUMMARY}"
echo "\`${immutable_image}\`" >> "${GITHUB_STEP_SUMMARY}"
Loading