feat: support release lookup (#18450)
This commit is contained in:
22
.github/workflows/release-docs.yml
vendored
22
.github/workflows/release-docs.yml
vendored
@@ -1,6 +1,8 @@
|
|||||||
name: Release Documentation
|
name: Release Documentation
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
@@ -25,6 +27,11 @@ jobs:
|
|||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Fetch full git history for release index
|
||||||
|
if: github.event_name == 'release'
|
||||||
|
run: |
|
||||||
|
git fetch --prune --unshallow || git fetch --prune --depth=0
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
bash scripts/ci/cuda/ci_install_dependency.sh
|
bash scripts/ci/cuda/ci_install_dependency.sh
|
||||||
@@ -53,10 +60,23 @@ jobs:
|
|||||||
make markdown
|
make markdown
|
||||||
python3 wrap_run_llm.py
|
python3 wrap_run_llm.py
|
||||||
|
|
||||||
|
if [[ "${{ github.event_name }}" == "release" ]]; then
|
||||||
|
python3 release_lookup/generate_index.py --output release_lookup/release_index.json
|
||||||
|
|
||||||
|
# Copy release lookup tool for official docs on published releases.
|
||||||
|
mkdir -p _build/html/release_lookup
|
||||||
|
cp release_lookup/index.html _build/html/release_lookup/
|
||||||
|
cp release_lookup/release_index.json _build/html/release_lookup/
|
||||||
|
fi
|
||||||
|
|
||||||
cd _build/html
|
cd _build/html
|
||||||
|
|
||||||
git clone https://$GITHUB_TOKEN@github.com/sgl-project/sgl-project.github.io.git ../sgl-project.github.io --depth 1
|
git clone https://$GITHUB_TOKEN@github.com/sgl-project/sgl-project.github.io.git ../sgl-project.github.io --depth 1
|
||||||
find ../sgl-project.github.io/ -mindepth 1 -not -path "../sgl-project.github.io/.git*" -not -name CNAME -not -name ".jekyll" -not -name ".nojekyll" -delete
|
if [[ "${{ github.event_name }}" == "release" ]]; then
|
||||||
|
find ../sgl-project.github.io/ -mindepth 1 -not -path "../sgl-project.github.io/.git*" -not -name CNAME -not -name ".jekyll" -not -name ".nojekyll" -delete
|
||||||
|
else
|
||||||
|
find ../sgl-project.github.io/ -mindepth 1 -not -path "../sgl-project.github.io/.git*" -not -path "../sgl-project.github.io/release_lookup*" -not -name CNAME -not -name ".jekyll" -not -name ".nojekyll" -delete
|
||||||
|
fi
|
||||||
cp -r * ../sgl-project.github.io
|
cp -r * ../sgl-project.github.io
|
||||||
cp ../../README.md ../sgl-project.github.io/README.md
|
cp ../../README.md ../sgl-project.github.io/README.md
|
||||||
cd ../sgl-project.github.io
|
cd ../sgl-project.github.io
|
||||||
|
|||||||
39
docs/release_lookup/README.md
Normal file
39
docs/release_lookup/README.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# SGLang Release Lookup Tool
|
||||||
|
|
||||||
|
This tool allows users to find the earliest release that contains a specific PR or commit.
|
||||||
|
It runs entirely in the browser using a static JSON index generated from the git history.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. **Generate the Index**:
|
||||||
|
Run the Python script to generate the `release_index.json` file from your local git repository.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 generate_index.py --output release_index.json
|
||||||
|
```
|
||||||
|
|
||||||
|
This script:
|
||||||
|
- Finds all tags matching `v*` and `gateway-v*`.
|
||||||
|
- Sorts them by creation date.
|
||||||
|
- Traverses the history to find which release first introduced each commit and PR.
|
||||||
|
- Extracts PR numbers from commit messages.
|
||||||
|
|
||||||
|
2. **Open the Tool**:
|
||||||
|
Open `index.html` in your browser.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# You can open it directly if your browser supports local file fetch (Firefox usually does),
|
||||||
|
# or serve it locally:
|
||||||
|
python3 -m http.server
|
||||||
|
# Then go to http://localhost:8000/index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `index.html`: The UI for the lookup tool.
|
||||||
|
- `generate_index.py`: Script to build the index.
|
||||||
|
- `release_index.json`: The index file used by the UI.
|
||||||
|
|
||||||
|
## Logic
|
||||||
|
|
||||||
|
The tool determines the "earliest release" based on the tag creation date. It traverses tags from oldest to newest. Any commit reachable from a tag (that wasn't reachable from a previous tag) is assigned to that release.
|
||||||
222
docs/release_lookup/generate_index.py
Normal file
222
docs/release_lookup/generate_index.py
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Short hash length for commits (7 is git's default short hash)
|
||||||
|
SHORT_HASH_LEN = 8
|
||||||
|
COMMIT_CHUNK_SIZE = 1000
|
||||||
|
|
||||||
|
|
||||||
|
def run_git(cmd):
|
||||||
|
try:
|
||||||
|
output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
|
||||||
|
return output.decode("utf-8", errors="replace").strip()
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"Error running cmd: {cmd}\n{e.output.decode('utf-8', errors='replace')}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def is_stable_release(tag_name):
|
||||||
|
"""Check if tag is a stable release (not rc/alpha/beta)."""
|
||||||
|
# Skip release candidates, alpha, beta versions
|
||||||
|
if re.search(r"(rc|alpha|beta)\d*$", tag_name, re.IGNORECASE):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def get_tags():
|
||||||
|
# Get tags sorted by creator date
|
||||||
|
cmd = [
|
||||||
|
"git",
|
||||||
|
"tag",
|
||||||
|
"--list",
|
||||||
|
"v*",
|
||||||
|
"gateway-v*",
|
||||||
|
"--sort=creatordate",
|
||||||
|
"--format=%(refname:short)|%(creatordate:iso8601)|%(objectname)",
|
||||||
|
]
|
||||||
|
raw = run_git(cmd)
|
||||||
|
tags = []
|
||||||
|
if not raw:
|
||||||
|
return []
|
||||||
|
for line in raw.split("\n"):
|
||||||
|
parts = line.split("|")
|
||||||
|
if len(parts) >= 3:
|
||||||
|
name, date, commit = parts[0], parts[1], parts[2]
|
||||||
|
# Skip non-stable releases (rc, alpha, beta)
|
||||||
|
if not is_stable_release(name):
|
||||||
|
continue
|
||||||
|
tag_type = "gateway" if name.startswith("gateway-") else "main"
|
||||||
|
tags.append(
|
||||||
|
{"name": name, "date": date, "commit": commit, "type": tag_type}
|
||||||
|
)
|
||||||
|
return tags
|
||||||
|
|
||||||
|
|
||||||
|
def extract_pr_num(message):
|
||||||
|
lines = message.strip().split("\n")
|
||||||
|
first_line = lines[0]
|
||||||
|
|
||||||
|
m = re.search(r"\(#(\d+)\)$", first_line)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
|
|
||||||
|
m = re.search(r"Merge pull request #(\d+)", message)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def process_tag_line(tags, commit_map, pr_map, tag_type, tag_to_idx):
|
||||||
|
"""Process a single release line (main or gateway) independently."""
|
||||||
|
seen_commits = set()
|
||||||
|
|
||||||
|
for tag in tags:
|
||||||
|
tag_name = tag["name"]
|
||||||
|
print(f"Processing {tag_name}...")
|
||||||
|
|
||||||
|
commits = run_git(["git", "rev-list", tag_name]).split("\n")
|
||||||
|
|
||||||
|
new_commits = []
|
||||||
|
for c in commits:
|
||||||
|
c = c.strip()
|
||||||
|
if not c:
|
||||||
|
continue
|
||||||
|
if c in seen_commits:
|
||||||
|
continue
|
||||||
|
new_commits.append(c)
|
||||||
|
seen_commits.add(c)
|
||||||
|
|
||||||
|
if not new_commits:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for i in range(0, len(new_commits), COMMIT_CHUNK_SIZE):
|
||||||
|
chunk = new_commits[i : i + COMMIT_CHUNK_SIZE]
|
||||||
|
|
||||||
|
cmd = ["git", "show", "-s", "--format=%H|%B%n--END-COMMIT--"] + chunk
|
||||||
|
raw_logs = run_git(cmd)
|
||||||
|
|
||||||
|
entries = raw_logs.split("--END-COMMIT--\n")
|
||||||
|
for log_entry in entries:
|
||||||
|
if not log_entry.strip():
|
||||||
|
continue
|
||||||
|
parts = log_entry.split("|", 1)
|
||||||
|
if len(parts) < 2:
|
||||||
|
continue
|
||||||
|
sha = parts[0].strip()
|
||||||
|
msg = parts[1].strip()
|
||||||
|
|
||||||
|
tag_idx = tag_to_idx[tag_name]
|
||||||
|
|
||||||
|
# Store release index using full SHA as key
|
||||||
|
if sha not in commit_map:
|
||||||
|
commit_map[sha] = {}
|
||||||
|
commit_map[sha][tag_type] = tag_idx
|
||||||
|
|
||||||
|
pr = extract_pr_num(msg)
|
||||||
|
if pr:
|
||||||
|
if pr not in pr_map:
|
||||||
|
pr_map[pr] = {}
|
||||||
|
if tag_type not in pr_map[pr]:
|
||||||
|
pr_map[pr][tag_type] = tag_idx
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Generate lookup index for sglang releases"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--output", default="release_index.json", help="Output JSON file"
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
tags = get_tags()
|
||||||
|
print(f"Found {len(tags)} tags.")
|
||||||
|
|
||||||
|
main_tags = [t for t in tags if t["type"] == "main"]
|
||||||
|
gateway_tags = [t for t in tags if t["type"] == "gateway"]
|
||||||
|
|
||||||
|
print(f" - {len(main_tags)} main tags")
|
||||||
|
print(f" - {len(gateway_tags)} gateway tags")
|
||||||
|
|
||||||
|
# Build tag list and index mapping
|
||||||
|
# Tags array: [name, date, type] for each tag
|
||||||
|
tag_list = []
|
||||||
|
tag_to_idx = {}
|
||||||
|
|
||||||
|
for tag in tags:
|
||||||
|
tag_to_idx[tag["name"]] = len(tag_list)
|
||||||
|
# Compact format: [name, date, type (0=main, 1=gateway)]
|
||||||
|
tag_list.append(
|
||||||
|
[tag["name"], tag["date"], 1 if tag["type"] == "gateway" else 0]
|
||||||
|
)
|
||||||
|
|
||||||
|
pr_map = {}
|
||||||
|
commit_map_full = {}
|
||||||
|
|
||||||
|
process_tag_line(main_tags, commit_map_full, pr_map, "m", tag_to_idx)
|
||||||
|
process_tag_line(gateway_tags, commit_map_full, pr_map, "g", tag_to_idx)
|
||||||
|
|
||||||
|
# Convert full SHAs to short SHAs, checking for collisions
|
||||||
|
commit_map = {}
|
||||||
|
short_to_full_map = {}
|
||||||
|
for full_sha, data in commit_map_full.items():
|
||||||
|
short_sha = full_sha[:SHORT_HASH_LEN]
|
||||||
|
if short_sha in short_to_full_map and short_to_full_map[short_sha] != full_sha:
|
||||||
|
print(
|
||||||
|
f"CRITICAL: Short SHA collision detected for '{short_sha}'\n"
|
||||||
|
f" Commit 1: {short_to_full_map[short_sha]}\n"
|
||||||
|
f" Commit 2: {full_sha}\n"
|
||||||
|
"Please increase SHORT_HASH_LEN and re-run.",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
commit_map[short_sha] = data
|
||||||
|
short_to_full_map[short_sha] = full_sha
|
||||||
|
|
||||||
|
# Compact output format:
|
||||||
|
# - tags: array of [name, date, type]
|
||||||
|
# - prs: {pr_num: tag_idx} or {pr_num: {m: idx, g: idx}}
|
||||||
|
# - commits: {short_hash: tag_idx} or {short_hash: {m: idx, g: idx}}
|
||||||
|
|
||||||
|
# Simplify single-entry dicts to just the value
|
||||||
|
def simplify_map(m):
|
||||||
|
result = {}
|
||||||
|
for k, v in m.items():
|
||||||
|
if len(v) == 1:
|
||||||
|
# Single entry: just store the index directly with type prefix
|
||||||
|
key_type, idx = list(v.items())[0]
|
||||||
|
result[k] = f"{key_type}{idx}"
|
||||||
|
else:
|
||||||
|
# Multiple entries: keep as dict
|
||||||
|
result[k] = v
|
||||||
|
return result
|
||||||
|
|
||||||
|
output_data = {
|
||||||
|
"t": tag_list, # tags
|
||||||
|
"p": simplify_map(pr_map), # prs
|
||||||
|
"c": simplify_map(commit_map), # commits
|
||||||
|
"g": datetime.now().isoformat(), # generated_at
|
||||||
|
}
|
||||||
|
|
||||||
|
# Write minified JSON with a trailing newline for formatter compatibility.
|
||||||
|
json_str = json.dumps(output_data, separators=(",", ":"))
|
||||||
|
|
||||||
|
with open(args.output, "w", encoding="utf-8") as f:
|
||||||
|
f.write(json_str)
|
||||||
|
f.write("\n")
|
||||||
|
|
||||||
|
json_size = os.path.getsize(args.output)
|
||||||
|
|
||||||
|
print(f"Index generated at {args.output}")
|
||||||
|
print(f"Stats: {len(tag_list)} tags, {len(pr_map)} PRs, {len(commit_map)} commits.")
|
||||||
|
print(f"Size: {json_size/1024:.1f} KB")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
515
docs/release_lookup/index.html
Normal file
515
docs/release_lookup/index.html
Normal file
@@ -0,0 +1,515 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>SGLang Release Lookup</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary: #3b82f6;
|
||||||
|
--primary-hover: #2563eb;
|
||||||
|
--bg: #f8fafc;
|
||||||
|
--card-bg: #ffffff;
|
||||||
|
--text-main: #1e293b;
|
||||||
|
--text-secondary: #64748b;
|
||||||
|
--border: #e2e8f0;
|
||||||
|
--success-bg: #f0fdf4;
|
||||||
|
--success-border: #bbf7d0;
|
||||||
|
--success-text: #166534;
|
||||||
|
--error-bg: #fef2f2;
|
||||||
|
--error-border: #fecaca;
|
||||||
|
--error-text: #991b1b;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
background-color: var(--bg);
|
||||||
|
color: var(--text-main);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background-color: var(--card-bg);
|
||||||
|
padding: 2.5rem;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 550px;
|
||||||
|
text-align: center;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: var(--text-main);
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.subtitle {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 2px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
color: var(--text-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus {
|
||||||
|
border-color: var(--primary);
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
input::placeholder {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 12px 24px;
|
||||||
|
background-color: var(--primary);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s, transform 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: var(--primary-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
background-color: #cbd5e1;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#result {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
text-align: left;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: none;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#result.visible {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-content {
|
||||||
|
padding: 1.25rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-success {
|
||||||
|
background-color: var(--success-bg);
|
||||||
|
border: 1px solid var(--success-border);
|
||||||
|
color: var(--success-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-error {
|
||||||
|
background-color: var(--error-bg);
|
||||||
|
border: 1px solid var(--error-border);
|
||||||
|
color: var(--error-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-row:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-label {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-right: 1rem;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-link {
|
||||||
|
color: var(--primary);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
display: inline-block;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border: 3px solid rgba(59, 130, 246, 0.2);
|
||||||
|
border-radius: 50%;
|
||||||
|
border-top-color: var(--primary);
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin-right: 8px;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-msg {
|
||||||
|
margin-top: 1rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
min-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-main {
|
||||||
|
background-color: #dbeafe;
|
||||||
|
color: #1e40af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-gateway {
|
||||||
|
background-color: #f3e8ff;
|
||||||
|
color: #6b21a8;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<h1>Release Lookup</h1>
|
||||||
|
<p class="subtitle">Find which SGLang release first included your PR or commit.</p>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" id="queryInput" placeholder="PR # (e.g. 1425), URL, or Commit Hash" autocomplete="off" />
|
||||||
|
<button id="searchBtn" disabled>Search</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="loading" style="display: none; margin-bottom: 1rem; color: var(--text-secondary);">
|
||||||
|
<span class="loader"></span> Loading index...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="result"></div>
|
||||||
|
|
||||||
|
<div id="indexStatus" class="status-msg">Initializing...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let tagIndex = null;
|
||||||
|
let tagsArray = null; // Compact format: array of [name, date, type]
|
||||||
|
let sortedCommitKeys = null; // Sorted keys for binary prefix search
|
||||||
|
const INDEX_FILE = 'release_index.json';
|
||||||
|
const SHORT_HASH_LEN = 8;
|
||||||
|
|
||||||
|
const input = document.getElementById('queryInput');
|
||||||
|
const btn = document.getElementById('searchBtn');
|
||||||
|
const resultDiv = document.getElementById('result');
|
||||||
|
const loadingDiv = document.getElementById('loading');
|
||||||
|
const statusDiv = document.getElementById('indexStatus');
|
||||||
|
|
||||||
|
// Format date nicely (always in English)
|
||||||
|
function formatDate(isoString) {
|
||||||
|
if (!isoString) return 'Unknown';
|
||||||
|
try {
|
||||||
|
return new Date(isoString).toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric', month: 'long', day: 'numeric'
|
||||||
|
});
|
||||||
|
} catch(e) { return isoString; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if index is in compact format
|
||||||
|
function isCompactFormat(data) {
|
||||||
|
return Array.isArray(data.t);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get tag info by index (compact) or name (legacy)
|
||||||
|
function getTagInfo(tagRef) {
|
||||||
|
if (tagsArray) {
|
||||||
|
// Compact format: tagRef is index
|
||||||
|
const tag = tagsArray[tagRef];
|
||||||
|
return {
|
||||||
|
name: tag[0],
|
||||||
|
date: tag[1],
|
||||||
|
type: tag[2] === 1 ? 'gateway' : 'main'
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Legacy format: tagRef is name
|
||||||
|
const info = tagIndex.tags[tagRef];
|
||||||
|
return { name: tagRef, ...info };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse compact tag reference: "m5" -> {type: 'm', idx: 5}
|
||||||
|
function parseTagRef(ref) {
|
||||||
|
if (typeof ref === 'string' && /^[mg]\d+$/.test(ref)) {
|
||||||
|
return {
|
||||||
|
type: ref[0],
|
||||||
|
idx: parseInt(ref.slice(1))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadIndex() {
|
||||||
|
loadingDiv.style.display = 'block';
|
||||||
|
statusDiv.innerText = 'Downloading index...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(INDEX_FILE);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("No index file found. Please run generate_index.py.");
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Handle both compact and legacy formats
|
||||||
|
if (isCompactFormat(data)) {
|
||||||
|
tagsArray = data.t;
|
||||||
|
tagIndex = {
|
||||||
|
prs: data.p,
|
||||||
|
commits: data.c
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
tagIndex = data;
|
||||||
|
tagsArray = null;
|
||||||
|
}
|
||||||
|
// Pre-sort commit keys for binary prefix search
|
||||||
|
sortedCommitKeys = Object.keys(tagIndex.commits).sort();
|
||||||
|
|
||||||
|
const tagCount = tagsArray ? tagsArray.length : Object.keys(tagIndex.tags).length;
|
||||||
|
const prCount = Object.keys(tagIndex.prs).length;
|
||||||
|
|
||||||
|
statusDiv.innerText = `Ready. Indexed ${tagCount} releases and ${prCount} PRs.`;
|
||||||
|
btn.disabled = false;
|
||||||
|
} catch (e) {
|
||||||
|
statusDiv.textContent = '';
|
||||||
|
const errorSpan = document.createElement('span');
|
||||||
|
errorSpan.style.color = 'var(--error-text)';
|
||||||
|
errorSpan.textContent = `Error: ${e.message}`;
|
||||||
|
statusDiv.appendChild(errorSpan);
|
||||||
|
btn.disabled = true;
|
||||||
|
} finally {
|
||||||
|
loadingDiv.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Binary search for first commit key matching the given prefix (O(log n))
|
||||||
|
function prefixSearchCommit(prefix) {
|
||||||
|
if (!sortedCommitKeys) return null;
|
||||||
|
let lo = 0, hi = sortedCommitKeys.length;
|
||||||
|
while (lo < hi) {
|
||||||
|
const mid = (lo + hi) >>> 1;
|
||||||
|
if (sortedCommitKeys[mid] < prefix) lo = mid + 1;
|
||||||
|
else hi = mid;
|
||||||
|
}
|
||||||
|
if (lo < sortedCommitKeys.length && sortedCommitKeys[lo].startsWith(prefix)) {
|
||||||
|
return sortedCommitKeys[lo];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start loading
|
||||||
|
loadIndex();
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
btn.addEventListener('click', performSearch);
|
||||||
|
input.addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') performSearch();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-focus input
|
||||||
|
input.focus();
|
||||||
|
|
||||||
|
function performSearch() {
|
||||||
|
if (!tagIndex) return;
|
||||||
|
|
||||||
|
const rawQuery = input.value.trim();
|
||||||
|
if (!rawQuery) return;
|
||||||
|
|
||||||
|
// Hide previous result
|
||||||
|
resultDiv.style.display = 'none';
|
||||||
|
resultDiv.classList.remove('visible');
|
||||||
|
|
||||||
|
let queryType = 'unknown';
|
||||||
|
let key = rawQuery;
|
||||||
|
|
||||||
|
// Parse query
|
||||||
|
// 1. PR URL: https://github.com/.../pull/1234
|
||||||
|
const urlMatch = rawQuery.match(/\/pull\/(\d+)/);
|
||||||
|
if (urlMatch) {
|
||||||
|
key = urlMatch[1];
|
||||||
|
queryType = 'pr';
|
||||||
|
}
|
||||||
|
// 2. PR Number: #1234 or 1234
|
||||||
|
else if (rawQuery.match(/^#?\d+$/)) {
|
||||||
|
key = rawQuery.replace('#', '');
|
||||||
|
queryType = 'pr';
|
||||||
|
}
|
||||||
|
// 3. Commit Hash: usually hex string (min 7 chars)
|
||||||
|
else if (rawQuery.match(/^[0-9a-fA-F]{7,40}$/)) {
|
||||||
|
key = rawQuery.toLowerCase();
|
||||||
|
queryType = 'commit';
|
||||||
|
}
|
||||||
|
|
||||||
|
let tagData = null;
|
||||||
|
|
||||||
|
if (queryType === 'pr') {
|
||||||
|
tagData = tagIndex.prs[key];
|
||||||
|
} else if (queryType === 'commit') {
|
||||||
|
// Use short hash for lookup
|
||||||
|
const shortKey = key.slice(0, SHORT_HASH_LEN);
|
||||||
|
tagData = tagIndex.commits[shortKey];
|
||||||
|
|
||||||
|
// If not found with short hash, try prefix match (binary search)
|
||||||
|
if (!tagData) {
|
||||||
|
const matchKey = prefixSearchCommit(shortKey);
|
||||||
|
if (matchKey) {
|
||||||
|
tagData = tagIndex.commits[matchKey];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderResult(tagData, queryType, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderResult(tagData, queryType, key) {
|
||||||
|
resultDiv.innerHTML = '';
|
||||||
|
resultDiv.style.display = 'block';
|
||||||
|
|
||||||
|
// Trigger reflow for animation
|
||||||
|
void resultDiv.offsetWidth;
|
||||||
|
resultDiv.classList.add('visible');
|
||||||
|
|
||||||
|
// Collect tag references
|
||||||
|
let tagRefs = [];
|
||||||
|
|
||||||
|
if (!tagData) {
|
||||||
|
// Not found
|
||||||
|
} else if (typeof tagData === 'string') {
|
||||||
|
// Compact format: "m5" or "g3"
|
||||||
|
const parsed = parseTagRef(tagData);
|
||||||
|
if (parsed) {
|
||||||
|
tagRefs.push(parsed.idx);
|
||||||
|
} else {
|
||||||
|
// Legacy format: tag name directly
|
||||||
|
tagRefs.push(tagData);
|
||||||
|
}
|
||||||
|
} else if (typeof tagData === 'object') {
|
||||||
|
// Object format: {m: 5, g: 3} or {main: "v0.5.8", gateway: "..."}
|
||||||
|
if ('m' in tagData) tagRefs.push(tagData.m);
|
||||||
|
if ('g' in tagData) tagRefs.push(tagData.g);
|
||||||
|
if ('main' in tagData) tagRefs.push(tagData.main);
|
||||||
|
if ('gateway' in tagData) tagRefs.push(tagData.gateway);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tagRefs.length === 0) {
|
||||||
|
const label = queryType === 'pr' ? `PR #${key}` : `Commit ${key.substring(0, 7)}`;
|
||||||
|
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.className = 'result-content result-error';
|
||||||
|
|
||||||
|
const statusRow = document.createElement('div');
|
||||||
|
statusRow.className = 'result-row';
|
||||||
|
const statusLabel = document.createElement('span');
|
||||||
|
statusLabel.className = 'result-label';
|
||||||
|
statusLabel.textContent = 'Status';
|
||||||
|
const statusValue = document.createElement('span');
|
||||||
|
statusValue.textContent = 'Not Found';
|
||||||
|
statusRow.appendChild(statusLabel);
|
||||||
|
statusRow.appendChild(statusValue);
|
||||||
|
|
||||||
|
const msgDiv = document.createElement('div');
|
||||||
|
msgDiv.style.marginTop = '8px';
|
||||||
|
const strongEl = document.createElement('strong');
|
||||||
|
strongEl.textContent = label;
|
||||||
|
msgDiv.append(
|
||||||
|
`The ${queryType} `,
|
||||||
|
strongEl,
|
||||||
|
' has not been included in any release yet, or is not in the index.'
|
||||||
|
);
|
||||||
|
|
||||||
|
container.appendChild(statusRow);
|
||||||
|
container.appendChild(msgDiv);
|
||||||
|
resultDiv.appendChild(container);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const repoUrl = "https://github.com/sgl-project/sglang";
|
||||||
|
resultDiv.innerHTML = ''; // Clear previous results
|
||||||
|
|
||||||
|
for (const tagRef of tagRefs) {
|
||||||
|
const tagInfo = getTagInfo(tagRef);
|
||||||
|
const dateStr = formatDate(tagInfo.date);
|
||||||
|
const tagUrl = `${repoUrl}/releases/tag/${encodeURIComponent(tagInfo.name)}`;
|
||||||
|
const badgeClass = tagInfo.type === 'gateway' ? 'badge-gateway' : 'badge-main';
|
||||||
|
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.className = 'result-content result-success';
|
||||||
|
container.style.marginBottom = '0.75rem';
|
||||||
|
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="result-row">
|
||||||
|
<span class="result-label">Release</span>
|
||||||
|
<a target="_blank" class="tag-link"></a>
|
||||||
|
</div>
|
||||||
|
<div class="result-row">
|
||||||
|
<span class="result-label">Date</span>
|
||||||
|
<span class="date-value"></span>
|
||||||
|
</div>
|
||||||
|
<div class="result-row">
|
||||||
|
<span class="result-label">Module</span>
|
||||||
|
<span class="badge ${badgeClass} module-value"></span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Set dynamic content safely via textContent
|
||||||
|
const link = container.querySelector('.tag-link');
|
||||||
|
link.href = tagUrl;
|
||||||
|
link.textContent = tagInfo.name;
|
||||||
|
container.querySelector('.date-value').textContent = dateStr;
|
||||||
|
container.querySelector('.module-value').textContent = tagInfo.type;
|
||||||
|
|
||||||
|
resultDiv.appendChild(container);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1
docs/release_lookup/release_index.json
Normal file
1
docs/release_lookup/release_index.json
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user