Docker Build & Push #118
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: Docker Build & Push | |
| on: | |
| push: | |
| branches: | |
| - main | |
| paths: | |
| - 'docker/Dockerfile' | |
| schedule: | |
| # Every 12 hours: midnight and noon UTC | |
| - cron: '0 0,12 * * *' | |
| workflow_dispatch: | |
| inputs: | |
| variant: | |
| description: 'Build variant' | |
| required: true | |
| type: choice | |
| options: | |
| - primary | |
| - cu129-arm64 | |
| - cu13-arm64 | |
| - debug | |
| image_tag: | |
| description: 'Tag mode: latest, dev (rolling + timestamped), or custom' | |
| required: true | |
| type: choice | |
| options: | |
| - dev | |
| - latest | |
| - custom | |
| custom_tag: | |
| description: 'Custom tag name (only used when image_tag is custom)' | |
| required: false | |
| default: '' | |
| type: string | |
| dockerfile: | |
| description: 'Path to Dockerfile (e.g. docker/Dockerfile)' | |
| required: false | |
| default: 'docker/Dockerfile' | |
| type: string | |
| simulate_schedule: | |
| description: '[DEBUG] Simulate a scheduled build (runs upstream check)' | |
| required: false | |
| type: boolean | |
| default: false | |
| jobs: | |
| check-upstream: | |
| # Only run on schedule (or simulated schedule) — check if upstream repos have new commits | |
| if: github.event_name == 'schedule' || inputs.simulate_schedule == true | |
| runs-on: ubuntu-latest | |
| outputs: | |
| should_build: ${{ steps.check.outputs.should_build }} | |
| steps: | |
| - name: Restore last known upstream SHAs | |
| uses: actions/cache/restore@v4 | |
| with: | |
| path: last_upstream_shas.txt | |
| key: upstream-shas- | |
| restore-keys: upstream-shas- | |
| - name: Check for new upstream commits | |
| id: check | |
| run: | | |
| SHOULD_BUILD=false | |
| # Check sglang (sglang-miles branch) | |
| SGLANG_SHA=$(curl -s -H "Accept: application/vnd.github.sha" \ | |
| "https://api.github.com/repos/sgl-project/sglang/commits/sglang-miles") | |
| # Check Megatron-LM (main branch) | |
| MEGATRON_SHA=$(curl -s -H "Accept: application/vnd.github.sha" \ | |
| "https://api.github.com/repos/NVIDIA/Megatron-LM/commits/main") | |
| echo "sglang HEAD: ${SGLANG_SHA}" | |
| echo "megatron HEAD: ${MEGATRON_SHA}" | |
| # Compare against last known SHAs stored in cache | |
| CACHE_FILE="last_upstream_shas.txt" | |
| if [ -f "$CACHE_FILE" ]; then | |
| LAST_SGLANG=$(sed -n '1p' "$CACHE_FILE") | |
| LAST_MEGATRON=$(sed -n '2p' "$CACHE_FILE") | |
| else | |
| LAST_SGLANG="" | |
| LAST_MEGATRON="" | |
| fi | |
| if [ "$SGLANG_SHA" != "$LAST_SGLANG" ] || [ "$MEGATRON_SHA" != "$LAST_MEGATRON" ]; then | |
| SHOULD_BUILD=true | |
| echo "Upstream change detected" | |
| else | |
| echo "No upstream changes" | |
| fi | |
| # Save current SHAs for next run | |
| echo "${SGLANG_SHA}" > "$CACHE_FILE" | |
| echo "${MEGATRON_SHA}" >> "$CACHE_FILE" | |
| echo "should_build=${SHOULD_BUILD}" >> "$GITHUB_OUTPUT" | |
| - name: Save upstream SHAs to cache | |
| if: steps.check.outputs.should_build == 'true' | |
| uses: actions/cache/save@v4 | |
| with: | |
| path: last_upstream_shas.txt | |
| key: upstream-shas-${{ github.run_id }} | |
| build-and-push: | |
| needs: [check-upstream] | |
| # schedule: only build if upstream changed | |
| # push/dispatch: check-upstream skipped → build | |
| if: | | |
| always() && | |
| (needs.check-upstream.result == 'skipped' || needs.check-upstream.outputs.should_build == 'true') | |
| runs-on: self-hosted | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| with: | |
| driver-opts: | | |
| image=moby/buildkit:latest | |
| network=host | |
| - name: Install Python + dependencies | |
| run: | | |
| apt-get update && apt-get install -y python3 python3-pip | |
| pip3 install --break-system-packages typer | |
| - name: Login to Docker Hub | |
| uses: docker/login-action@v3 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Build and push | |
| run: | | |
| python3 docker/build.py \ | |
| --variant ${{ inputs.variant || 'primary' }} \ | |
| --image-tag ${{ inputs.image_tag || 'dev' }} \ | |
| --dockerfile ${{ inputs.dockerfile || 'docker/Dockerfile' }} \ | |
| ${{ inputs.custom_tag && format('--custom-tag {0}', inputs.custom_tag) || '' }} \ | |
| --push | |
| - name: Point latest to current dev | |
| if: github.event_name == 'schedule' || inputs.simulate_schedule == true | |
| run: | | |
| docker buildx imagetools create -t radixark/miles:latest radixark/miles:dev | |
| - name: Prune old dev tags | |
| if: github.event_name == 'schedule' | |
| run: | | |
| KEEP=20 | |
| IMAGE=radixark/miles | |
| # Authenticate with Docker Hub API | |
| TOKEN=$(curl -s -X POST "https://hub.docker.com/v2/users/login/" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"username":"${{ secrets.DOCKERHUB_USERNAME }}","password":"${{ secrets.DOCKERHUB_TOKEN }}"}' \ | |
| | jq -r .token) | |
| # Collect all dev-YYYYMMDDHHMM tags (paginated) | |
| DEV_TAGS="" | |
| URL="https://hub.docker.com/v2/repositories/${IMAGE}/tags/?page_size=100" | |
| while [ "$URL" != "null" ] && [ -n "$URL" ]; do | |
| RESP=$(curl -s -H "Authorization: JWT ${TOKEN}" "$URL") | |
| PAGE_TAGS=$(echo "$RESP" | jq -r '.results[].name' | grep -E '^dev-[0-9]{12}$' || true) | |
| DEV_TAGS="${DEV_TAGS}${PAGE_TAGS}"$'\n' | |
| URL=$(echo "$RESP" | jq -r '.next') | |
| done | |
| # Sort and find tags to delete | |
| SORTED=$(echo "$DEV_TAGS" | grep -v '^$' | sort) | |
| COUNT=$(echo "$SORTED" | wc -l | tr -d ' ') | |
| echo "Found ${COUNT} dev tags" | |
| if [ "$COUNT" -le "$KEEP" ]; then | |
| echo "Nothing to prune (keep=${KEEP})" | |
| exit 0 | |
| fi | |
| DELETE_COUNT=$((COUNT - KEEP)) | |
| TO_DELETE=$(echo "$SORTED" | head -n "$DELETE_COUNT") | |
| echo "Pruning ${DELETE_COUNT} old dev tags..." | |
| echo "$TO_DELETE" | while read -r TAG; do | |
| [ -z "$TAG" ] && continue | |
| HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \ | |
| -H "Authorization: JWT ${TOKEN}" \ | |
| "https://hub.docker.com/v2/repositories/${IMAGE}/tags/${TAG}/") | |
| if [ "$HTTP_CODE" = "204" ]; then | |
| echo " Deleted ${TAG}" | |
| else | |
| echo " Failed to delete ${TAG} (HTTP ${HTTP_CODE})" | |
| fi | |
| done | |
| build-and-push-dev-glm: | |
| needs: [build-and-push] | |
| # Only rebuild dev-glm when the dev image was built (schedule, push to main, or dispatch with image_tag=dev) | |
| if: needs.build-and-push.result == 'success' && (github.event_name == 'schedule' || inputs.simulate_schedule == true) | |
| runs-on: self-hosted | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| with: | |
| driver-opts: | | |
| image=moby/buildkit:latest | |
| network=host | |
| - name: Login to Docker Hub | |
| uses: docker/login-action@v3 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Build and push dev-glm | |
| run: | | |
| docker buildx build \ | |
| -f docker/glm5/Dockerfile.dev-glm \ | |
| -t radixark/miles:dev-glm \ | |
| --push \ | |
| . |