Add Docker image provenance metadata (#24090)

Co-authored-by: Kangyan-Zhou <zky314343421@gmail.com>
This commit is contained in:
Alec
2026-04-30 21:40:42 -07:00
committed by GitHub
co-authored by Kangyan-Zhou
parent 9d84268705
commit 9d95783603
4 changed files with 407 additions and 8 deletions
@@ -45,6 +45,10 @@ jobs:
if: github.repository == 'sgl-project/sglang'
environment: ${{ inputs.use_environment || null }}
runs-on: x64-docker-build-node
env:
TAG_CONFIG: ${{ inputs.tag_config }}
SGL_VERSION: ${{ inputs.sgl_version }}
IMAGE_REPO: ${{ inputs.image_repo }}
outputs:
digest-cu129: ${{ steps.build-cu129.outputs.digest }}
digest-cu130: ${{ steps.build-cu130.outputs.digest }}
@@ -60,6 +64,22 @@ jobs:
with:
ref: ${{ inputs.checkout_ref || github.ref }}
- name: Compute Docker build metadata args
run: |
set -euo pipefail
BUILD_COMMIT="$(git rev-parse HEAD)"
BUILD_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
for CUDA_VARIANT in cu129 cu130; do
python3 scripts/ci/utils/docker_build_metadata_args.py \
--cuda "${CUDA_VARIANT}" \
--tag-config "${TAG_CONFIG}" \
--image-repo "${IMAGE_REPO}" \
--sgl-version "${SGL_VERSION}" \
--build-commit "${BUILD_COMMIT}" \
--build-url "${BUILD_URL}" \
> "/tmp/docker-metadata-${CUDA_VARIANT}.args"
done
- name: Free disk space
uses: jlumbroso/free-disk-space@main
with:
@@ -90,9 +110,10 @@ jobs:
id: build-cu129
run: |
VERSION_ARG=""
if [ -n "${{ inputs.sgl_version }}" ]; then
VERSION_ARG="--build-arg SGL_VERSION=${{ inputs.sgl_version }}"
if [ -n "${SGL_VERSION}" ]; then
VERSION_ARG="--build-arg SGL_VERSION=${SGL_VERSION}"
fi
mapfile -t METADATA_ARGS < /tmp/docker-metadata-cu129.args
docker buildx build \
--target ${{ inputs.docker_target }} \
@@ -103,6 +124,7 @@ jobs:
--build-arg BUILD_TYPE=all \
--build-arg GRACE_BLACKWELL=0 \
--build-arg INSTALL_FLASHINFER_JIT_CACHE=1 \
"${METADATA_ARGS[@]}" \
${VERSION_ARG} \
${{ inputs.extra_build_args }} \
--metadata-file /tmp/metadata-cu129.json \
@@ -117,9 +139,10 @@ jobs:
id: build-cu130
run: |
VERSION_ARG=""
if [ -n "${{ inputs.sgl_version }}" ]; then
VERSION_ARG="--build-arg SGL_VERSION=${{ inputs.sgl_version }}"
if [ -n "${SGL_VERSION}" ]; then
VERSION_ARG="--build-arg SGL_VERSION=${SGL_VERSION}"
fi
mapfile -t METADATA_ARGS < /tmp/docker-metadata-cu130.args
docker buildx build \
--target ${{ inputs.docker_target }} \
@@ -130,6 +153,7 @@ jobs:
--build-arg BUILD_TYPE=all \
--build-arg GRACE_BLACKWELL=0 \
--build-arg INSTALL_FLASHINFER_JIT_CACHE=1 \
"${METADATA_ARGS[@]}" \
${VERSION_ARG} \
${{ inputs.extra_build_args }} \
--metadata-file /tmp/metadata-cu130.json \
@@ -144,6 +168,10 @@ jobs:
if: github.repository == 'sgl-project/sglang'
environment: ${{ inputs.use_environment || null }}
runs-on: arm-docker-build-node
env:
TAG_CONFIG: ${{ inputs.tag_config }}
SGL_VERSION: ${{ inputs.sgl_version }}
IMAGE_REPO: ${{ inputs.image_repo }}
outputs:
digest-cu129: ${{ steps.build-cu129.outputs.digest }}
digest-cu130: ${{ steps.build-cu130.outputs.digest }}
@@ -159,6 +187,22 @@ jobs:
with:
ref: ${{ inputs.checkout_ref || github.ref }}
- name: Compute Docker build metadata args
run: |
set -euo pipefail
BUILD_COMMIT="$(git rev-parse HEAD)"
BUILD_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
for CUDA_VARIANT in cu129 cu130; do
python3 scripts/ci/utils/docker_build_metadata_args.py \
--cuda "${CUDA_VARIANT}" \
--tag-config "${TAG_CONFIG}" \
--image-repo "${IMAGE_REPO}" \
--sgl-version "${SGL_VERSION}" \
--build-commit "${BUILD_COMMIT}" \
--build-url "${BUILD_URL}" \
> "/tmp/docker-metadata-${CUDA_VARIANT}.args"
done
- name: Prune Docker to reclaim disk space
run: |
docker buildx prune --filter "until=72h" -f
@@ -178,9 +222,10 @@ jobs:
id: build-cu129
run: |
VERSION_ARG=""
if [ -n "${{ inputs.sgl_version }}" ]; then
VERSION_ARG="--build-arg SGL_VERSION=${{ inputs.sgl_version }}"
if [ -n "${SGL_VERSION}" ]; then
VERSION_ARG="--build-arg SGL_VERSION=${SGL_VERSION}"
fi
mapfile -t METADATA_ARGS < /tmp/docker-metadata-cu129.args
docker buildx build \
--target ${{ inputs.docker_target }} \
@@ -191,6 +236,7 @@ jobs:
--build-arg BUILD_TYPE=all \
--build-arg GRACE_BLACKWELL=1 \
--build-arg INSTALL_FLASHINFER_JIT_CACHE=1 \
"${METADATA_ARGS[@]}" \
${VERSION_ARG} \
${{ inputs.extra_build_args }} \
--metadata-file /tmp/metadata-cu129.json \
@@ -205,9 +251,10 @@ jobs:
id: build-cu130
run: |
VERSION_ARG=""
if [ -n "${{ inputs.sgl_version }}" ]; then
VERSION_ARG="--build-arg SGL_VERSION=${{ inputs.sgl_version }}"
if [ -n "${SGL_VERSION}" ]; then
VERSION_ARG="--build-arg SGL_VERSION=${SGL_VERSION}"
fi
mapfile -t METADATA_ARGS < /tmp/docker-metadata-cu130.args
docker buildx build \
--target ${{ inputs.docker_target }} \
@@ -218,6 +265,7 @@ jobs:
--build-arg BUILD_TYPE=all \
--build-arg GRACE_BLACKWELL=1 \
--build-arg INSTALL_FLASHINFER_JIT_CACHE=1 \
"${METADATA_ARGS[@]}" \
${VERSION_ARG} \
${{ inputs.extra_build_args }} \
--metadata-file /tmp/metadata-cu130.json \
+30
View File
@@ -639,6 +639,21 @@ RUN --mount=type=cache,target=/root/.cache/pip \
# Set workspace directory
WORKDIR /sgl-workspace/sglang
# Keep build provenance at the end so metadata changes do not invalidate build layers.
ARG SGLANG_BUILD_COMMIT=unknown
ARG SGLANG_BUILD_URL=
ARG SGLANG_IMAGE_TAG=local/sglang:dev
ENV SGLANG_BUILD_COMMIT=${SGLANG_BUILD_COMMIT:-unknown} \
SGLANG_BUILD_URL=${SGLANG_BUILD_URL:-} \
SGLANG_IMAGE_TAG=${SGLANG_IMAGE_TAG:-local/sglang:dev}
LABEL org.opencontainers.image.source="https://github.com/sgl-project/sglang" \
org.opencontainers.image.revision="${SGLANG_BUILD_COMMIT}" \
org.opencontainers.image.version="${SGLANG_IMAGE_TAG}" \
org.opencontainers.image.url="${SGLANG_BUILD_URL}" \
ai.sglang.build.commit="${SGLANG_BUILD_COMMIT}" \
ai.sglang.build.url="${SGLANG_BUILD_URL}" \
ai.sglang.image.tag="${SGLANG_IMAGE_TAG}"
########################################################
########## Runtime Image ##############################
########################################################
@@ -782,5 +797,20 @@ RUN ln -sf /usr/lib/$(uname -m)-linux-gnu/libmlx5.so.1 /usr/lib/$(uname -m)-linu
WORKDIR /sgl-workspace/sglang
# Keep build provenance at the end so metadata changes do not invalidate build layers.
ARG SGLANG_BUILD_COMMIT=unknown
ARG SGLANG_BUILD_URL=
ARG SGLANG_IMAGE_TAG=local/sglang:dev
ENV SGLANG_BUILD_COMMIT=${SGLANG_BUILD_COMMIT:-unknown} \
SGLANG_BUILD_URL=${SGLANG_BUILD_URL:-} \
SGLANG_IMAGE_TAG=${SGLANG_IMAGE_TAG:-local/sglang:dev}
LABEL org.opencontainers.image.source="https://github.com/sgl-project/sglang" \
org.opencontainers.image.revision="${SGLANG_BUILD_COMMIT}" \
org.opencontainers.image.version="${SGLANG_IMAGE_TAG}" \
org.opencontainers.image.url="${SGLANG_BUILD_URL}" \
ai.sglang.build.commit="${SGLANG_BUILD_COMMIT}" \
ai.sglang.build.url="${SGLANG_BUILD_URL}" \
ai.sglang.image.tag="${SGLANG_IMAGE_TAG}"
# Default command
CMD ["/bin/bash"]
@@ -0,0 +1,119 @@
import argparse
import datetime
import json
import sys
MOVING_TAGS = {"dev", "dev-cu13", "latest"}
def render_tag_template(tag: str, version: str, date: str, short_sha: str) -> str:
return (
tag.replace("{version}", version)
.replace("{date}", date)
.replace("{short_sha}", short_sha)
)
def is_moving_tag(tag: str) -> bool:
return tag in MOVING_TAGS or tag.startswith("latest-")
def select_tag(
tag_config: str, cuda: str, version: str, date: str, short_sha: str
) -> str:
entries = json.loads(tag_config)
for entry in entries:
if entry.get("cuda") != cuda:
continue
tags = [
render_tag_template(tag, version, date, short_sha)
for tag in entry.get("tags", [])
]
if not tags:
raise ValueError(f"No tags configured for CUDA variant {cuda}")
for tag in tags:
if not is_moving_tag(tag):
return tag
return tags[0]
raise ValueError(f"CUDA variant {cuda} not found in tag_config")
def build_arg_tokens(
*,
cuda: str,
tag_config: str,
image_repo: str,
version: str,
build_commit: str,
build_url: str,
date: str,
short_sha: str,
) -> list[str]:
image_tag = select_tag(tag_config, cuda, version, date, short_sha)
build_args = {
"SGLANG_BUILD_COMMIT": build_commit,
"SGLANG_BUILD_URL": build_url,
"SGLANG_IMAGE_TAG": f"{image_repo}:{image_tag}",
}
tokens = []
for key, value in build_args.items():
tokens.extend(["--build-arg", f"{key}={value}"])
return tokens
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Emit docker build arguments for SGLang image metadata."
)
parser.add_argument("--cuda", required=True, help="CUDA variant from tag_config.")
parser.add_argument("--tag-config", required=True, help="Docker tag JSON config.")
parser.add_argument("--image-repo", required=True, help="Docker image repository.")
parser.add_argument("--sgl-version", default="", help="SGLang release version.")
parser.add_argument(
"--build-commit",
required=True,
help="Commit checked out for the Docker build.",
)
parser.add_argument("--build-url", default="", help="CI run URL.")
parser.add_argument(
"--date",
default=datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%d"),
help="Date used for {date} tag templates.",
)
parser.add_argument(
"--short-sha",
default="",
help="Short SHA used for {short_sha}; defaults to build commit prefix.",
)
return parser.parse_args()
def main() -> int:
args = parse_args()
short_sha = args.short_sha or args.build_commit[:8]
try:
tokens = build_arg_tokens(
cuda=args.cuda,
tag_config=args.tag_config,
image_repo=args.image_repo,
version=args.sgl_version,
build_commit=args.build_commit,
build_url=args.build_url,
date=args.date,
short_sha=short_sha,
)
except (json.JSONDecodeError, ValueError) as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
print("\n".join(tokens))
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,202 @@
import importlib.util
import json
import subprocess
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[4]
CI_REGISTER_PATH = REPO_ROOT / "python" / "sglang" / "test" / "ci" / "ci_register.py"
HELPER_PATH = REPO_ROOT / "scripts" / "ci" / "utils" / "docker_build_metadata_args.py"
DOCKERFILE_PATH = REPO_ROOT / "docker" / "Dockerfile"
WORKFLOW_PATH = REPO_ROOT / ".github" / "workflows" / "_docker-build-and-publish.yml"
def _load_module(name, path):
spec = importlib.util.spec_from_file_location(name, path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
register_cpu_ci = _load_module("ci_register", CI_REGISTER_PATH).register_cpu_ci
register_cpu_ci(est_time=0, suite="stage-a-test-cpu")
class TestDockerBuildMetadataArgs(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.helper = _load_module("docker_build_metadata_args", HELPER_PATH)
def run_helper(
self,
*,
cuda: str,
tag_config: list[dict[str, object]],
image_repo: str = "lmsysorg/sglang",
version: str = "0.6.0",
build_commit: str = "abcdef1234567890",
build_url: str = "https://github.com/sgl-project/sglang/actions/runs/1",
date: str = "20260429",
) -> list[str]:
result = subprocess.run(
[
"python3",
str(HELPER_PATH),
"--cuda",
cuda,
"--tag-config",
json.dumps(tag_config),
"--image-repo",
image_repo,
"--sgl-version",
version,
"--build-commit",
build_commit,
"--build-url",
build_url,
"--date",
date,
],
check=True,
stdout=subprocess.PIPE,
text=True,
)
return result.stdout.splitlines()
@staticmethod
def option_values(args: list[str], option: str) -> list[str]:
return [args[i + 1] for i, arg in enumerate(args[:-1]) if arg == option]
def build_args(self, args: list[str]) -> dict[str, str]:
values = {}
for value in self.option_values(args, "--build-arg"):
key, arg_value = value.split("=", 1)
values[key] = arg_value
return values
def test_release_metadata_prefers_versioned_tag(self):
args = self.run_helper(
cuda="cu129",
tag_config=[
{"cuda": "cu129", "tags": ["v{version}", "latest"]},
{"cuda": "cu130", "tags": ["v{version}-cu130", "latest-cu130"]},
],
)
self.assertEqual(
self.build_args(args),
{
"SGLANG_BUILD_COMMIT": "abcdef1234567890",
"SGLANG_BUILD_URL": (
"https://github.com/sgl-project/sglang/actions/runs/1"
),
"SGLANG_IMAGE_TAG": "lmsysorg/sglang:v0.6.0",
},
)
def test_runtime_metadata_uses_custom_repo_and_runtime_tag(self):
args = self.run_helper(
cuda="cu130",
image_repo="lmsysorg/sglang-staging",
tag_config=[
{"cuda": "cu129", "tags": ["v{version}-runtime", "latest-runtime"]},
{
"cuda": "cu130",
"tags": ["v{version}-cu130-runtime", "latest-cu130-runtime"],
},
],
)
self.assertEqual(
self.build_args(args)["SGLANG_IMAGE_TAG"],
"lmsysorg/sglang-staging:v0.6.0-cu130-runtime",
)
def test_dev_nightly_metadata_prefers_unique_tag_from_checked_out_commit(self):
args = self.run_helper(
cuda="cu129",
version="",
build_commit="1234567890abcdef",
tag_config=[
{"cuda": "cu129", "tags": ["dev", "nightly-dev-{date}-{short_sha}"]},
{
"cuda": "cu130",
"tags": ["dev-cu13", "nightly-dev-cu13-{date}-{short_sha}"],
},
],
)
self.assertEqual(
self.build_args(args)["SGLANG_IMAGE_TAG"],
"lmsysorg/sglang:nightly-dev-20260429-12345678",
)
self.assertEqual(
self.build_args(args)["SGLANG_BUILD_COMMIT"],
"1234567890abcdef",
)
def test_custom_dev_tag_is_treated_as_specific(self):
args = self.run_helper(
cuda="cu130",
version="",
tag_config=[
{"cuda": "cu129", "tags": ["dev-my-test"]},
{"cuda": "cu130", "tags": ["dev-cu13-my-test"]},
],
)
self.assertEqual(
self.build_args(args)["SGLANG_IMAGE_TAG"],
"lmsysorg/sglang:dev-cu13-my-test",
)
def test_missing_cuda_entry_fails(self):
with self.assertRaisesRegex(ValueError, "cu130"):
self.helper.select_tag(
json.dumps([{"cuda": "cu129", "tags": ["v{version}"]}]),
"cu130",
"0.6.0",
"20260429",
"abcdef12",
)
def test_final_dockerfile_stages_embed_metadata_contract(self):
dockerfile = DOCKERFILE_PATH.read_text()
framework_stage = dockerfile.split("FROM framework AS framework_final", 1)[
1
].split("FROM nvidia/cuda:${CUDA_VERSION}-cudnn-devel-ubuntu24.04 AS runtime")[
0
]
runtime_stage = dockerfile.split(
"FROM nvidia/cuda:${CUDA_VERSION}-cudnn-devel-ubuntu24.04 AS runtime", 1
)[1]
for stage in (framework_stage, runtime_stage):
for expected in (
"ARG SGLANG_BUILD_COMMIT=unknown",
"ARG SGLANG_BUILD_URL=",
"ARG SGLANG_IMAGE_TAG=local/sglang:dev",
"SGLANG_BUILD_COMMIT=${SGLANG_BUILD_COMMIT:-unknown}",
"SGLANG_BUILD_URL=${SGLANG_BUILD_URL:-}",
"SGLANG_IMAGE_TAG=${SGLANG_IMAGE_TAG:-local/sglang:dev}",
'org.opencontainers.image.source="https://github.com/sgl-project/sglang"',
'org.opencontainers.image.revision="${SGLANG_BUILD_COMMIT}"',
'org.opencontainers.image.version="${SGLANG_IMAGE_TAG}"',
'org.opencontainers.image.url="${SGLANG_BUILD_URL}"',
'ai.sglang.build.commit="${SGLANG_BUILD_COMMIT}"',
'ai.sglang.build.url="${SGLANG_BUILD_URL}"',
'ai.sglang.image.tag="${SGLANG_IMAGE_TAG}"',
):
self.assertIn(expected, stage)
def test_shared_docker_workflow_uses_checked_out_commit(self):
workflow = WORKFLOW_PATH.read_text()
self.assertIn("git rev-parse HEAD", workflow)
self.assertIn("scripts/ci/utils/docker_build_metadata_args.py", workflow)
self.assertIn("mapfile -t METADATA_ARGS", workflow)
self.assertIn('"${METADATA_ARGS[@]}"', workflow)
if __name__ == "__main__":
unittest.main()