| name: Patch Docker Image | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| pr_numbers: | |
| description: "Comma-separated PR numbers to apply (e.g. 18962,19010)" | |
| required: false | |
| default: "" | |
| image_tag: | |
| description: "Base image tag to patch (e.g. dev-x86, dev-x86-cu13)" | |
| required: true | |
| concurrency: | |
| group: patch-docker-${{ inputs.image_tag }} | |
| cancel-in-progress: true | |
| jobs: | |
| patch: | |
| if: github.repository == 'sgl-project/sglang' | |
| runs-on: x64-docker-build-node | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Login to Docker Hub | |
| uses: docker/login-action@v2 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Pull base image and extract commit | |
| run: | | |
| IMAGE="lmsysorg/sglang:${{ inputs.image_tag }}" | |
| docker pull "${IMAGE}" | |
| if BASE_SHA=$(docker run --rm "${IMAGE}" git -C /sgl-workspace/sglang rev-parse HEAD 2>/dev/null); then | |
| echo "Image built from commit: ${BASE_SHA}" | |
| else | |
| BASE_SHA="" | |
| echo "::warning::Image has no .git directory — cannot extract base commit" | |
| fi | |
| echo "BASE_SHA=${BASE_SHA}" >> "$GITHUB_ENV" | |
| - name: Generate patches | |
| run: | | |
| git config --global --add safe.directory "$GITHUB_WORKSPACE" | |
| git fetch origin main | |
| mkdir -p /tmp/patch-ctx | |
| if [ -n "${{ inputs.pr_numbers }}" ]; then | |
| IFS=',' read -ra PRS <<< "${{ inputs.pr_numbers }}" | |
| for pr in "${PRS[@]}"; do | |
| pr=$(echo "${pr}" | xargs) | |
| echo "Fetching PR #${pr}" | |
| git fetch origin "pull/${pr}/head:pr-${pr}" | |
| MERGE_BASE=$(git merge-base origin/main "pr-${pr}") | |
| echo " PR #${pr}: merge-base=${MERGE_BASE}" | |
| git diff "${MERGE_BASE}..pr-${pr}" > "/tmp/patch-ctx/${pr}.patch" | |
| echo " PR #${pr}: $(wc -l < /tmp/patch-ctx/${pr}.patch) lines" | |
| done | |
| elif [ -n "${BASE_SHA}" ]; then | |
| echo "Generating diff: image ${BASE_SHA} → latest main" | |
| git fetch origin "${BASE_SHA}" | |
| git diff "${BASE_SHA}..origin/main" > /tmp/patch-ctx/main.patch | |
| echo " main: $(wc -l < /tmp/patch-ctx/main.patch) lines" | |
| else | |
| echo "::error::No PR numbers specified and image has no .git — cannot generate diff against main" | |
| exit 1 | |
| fi | |
| TOTAL=$(cat /tmp/patch-ctx/*.patch | wc -l) | |
| if [ "${TOTAL}" -eq 0 ]; then | |
| echo "::warning::All patches are empty — image is already up to date" | |
| echo "SKIP_BUILD=true" >> "$GITHUB_ENV" | |
| fi | |
| - name: Build patched image | |
| if: env.SKIP_BUILD != 'true' | |
| run: | | |
| IMAGE="lmsysorg/sglang:${{ inputs.image_tag }}" | |
| cat <<'DOCKERFILE' > /tmp/patch-ctx/Dockerfile | |
| ARG BASE_IMAGE | |
| FROM ${BASE_IMAGE} | |
| COPY *.patch /tmp/patches/ | |
| RUN cd /sgl-workspace/sglang \ | |
| && for p in /tmp/patches/*.patch; do \ | |
| if [ ! -s "${p}" ]; then \ | |
| echo "Skipping ${p} (empty)"; \ | |
| else \ | |
| echo "Applying ${p}..." \ | |
| && patch -p1 --fuzz=2 --no-backup-if-mismatch -f < "${p}" \ | |
| || { echo "ERROR: Failed to apply ${p}"; exit 1; }; \ | |
| fi; \ | |
| done \ | |
| && rm -rf /tmp/patches | |
| DOCKERFILE | |
| docker build \ | |
| --no-cache \ | |
| --build-arg BASE_IMAGE="${IMAGE}" \ | |
| -t "${IMAGE}" \ | |
| /tmp/patch-ctx/ | |
| - name: Push patched image | |
| if: env.SKIP_BUILD != 'true' | |
| run: | | |
| IMAGE="lmsysorg/sglang:${{ inputs.image_tag }}" | |
| docker push "${IMAGE}" | |
| echo "### Patched \`${IMAGE}\`" >> "$GITHUB_STEP_SUMMARY" | |
| echo "- **Base commit:** \`${BASE_SHA:-unknown (no .git)}\`" >> "$GITHUB_STEP_SUMMARY" | |
| echo "- **Source:** ${{ inputs.pr_numbers && format('PRs: {0}', inputs.pr_numbers) || 'latest main' }}" >> "$GITHUB_STEP_SUMMARY" | |