Skip to content

Commit ce2617e

Browse files
noonghunnaclaude
andcommitted
fix(ci): simplify vllm image workflow, drop smoke-gate (noonghunna#135)
The smoke-gating mechanism required a self-hosted GPU runner that we don't maintain + administration:read permission on GITHUB_TOKEN that the default scope lacks. Result: :latest never got promoted on the v0.7.0 release run. Simplification: - Remove detect-smoke-runner job (no permission dependency) - Remove smoke job (no runner dependency) - Add promote-aliases job: unconditionally promote :latest + :nightly-stable to the just-built dated tag using docker buildx imagetools create - Build job continues to produce dated + :club-vX.Y.Z (on tag pushes) - retention job unchanged (still gated on schedule/dispatch) - Workflow permissions reduced: actions:read no longer needed The Docker Hub :latest convention is "most recent, no guarantees". Users who want verified images pin to :club-vX.Y.Z. If smoke-gating becomes useful when we get a registered runner, it can layer on top as a separate post-build job without re-introducing the failure modes. docs/CI_RUNNER_SETUP.md rewritten to reflect the simplified flow + manual :latest bootstrap recipe. docs/UPSTREAM.md updated to point at :club-vX.Y.Z as the recommended pin instead of :latest. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 02249ab commit ce2617e

3 files changed

Lines changed: 88 additions & 287 deletions

File tree

.github/workflows/build-vllm-image.yml

Lines changed: 9 additions & 198 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,6 @@ on:
77
description: "Upstream vLLM image to vendor overlays into"
88
required: false
99
default: "vllm/vllm-openai:nightly-1acd67a795ebccdf9b9db7697ae9082058301657"
10-
smoke:
11-
description: "GPU smoke behavior"
12-
required: false
13-
default: "auto"
14-
type: choice
15-
options:
16-
- auto
17-
- skip
18-
- required
1910
schedule:
2011
- cron: "0 0 * * 0"
2112
push:
@@ -36,14 +27,12 @@ concurrency:
3627
cancel-in-progress: false
3728

3829
permissions:
39-
actions: read
4030
contents: read
4131
packages: write
4232

4333
env:
4434
IMAGE_NAME: ghcr.io/noonghunna/vllm-club3090
4535
DEFAULT_VLLM_BASE_IMAGE: vllm/vllm-openai:nightly-1acd67a795ebccdf9b9db7697ae9082058301657
46-
CANONICAL_COMPOSE: models/qwen3.6-27b/vllm/compose/dual/docker-compose.yml
4736

4837
jobs:
4938
build:
@@ -111,206 +100,28 @@ jobs:
111100
org.opencontainers.image.version=${{ steps.meta.outputs.image_tag }}
112101
club3090.upstream_vllm_image=${{ steps.meta.outputs.upstream_image }}
113102
114-
detect-smoke-runner:
115-
name: Detect self-hosted GPU runner
103+
promote-aliases:
104+
name: Promote latest and nightly-stable
116105
needs: build
117106
runs-on: ubuntu-latest
118-
outputs:
119-
available: ${{ steps.detect.outputs.available }}
120-
smoke_mode: ${{ steps.detect.outputs.smoke_mode }}
121107
steps:
122-
- name: Detect online gpu-labeled runner
123-
id: detect
124-
uses: actions/github-script@v7
125-
env:
126-
SMOKE_MODE: ${{ inputs.smoke || 'auto' }}
127-
with:
128-
script: |
129-
const mode = process.env.SMOKE_MODE || "auto";
130-
core.setOutput("smoke_mode", mode);
131-
132-
if (mode === "skip") {
133-
core.notice("Smoke explicitly skipped. Dated image was pushed; aliases will not move.");
134-
core.setOutput("available", "false");
135-
return;
136-
}
137-
138-
const runners = await github.paginate(
139-
github.rest.actions.listSelfHostedRunnersForRepo,
140-
{
141-
owner: context.repo.owner,
142-
repo: context.repo.repo,
143-
per_page: 100,
144-
},
145-
);
146-
147-
const available = runners.some((runner) => {
148-
const labels = runner.labels.map((label) => label.name.toLowerCase());
149-
return runner.status === "online" &&
150-
labels.includes("self-hosted") &&
151-
labels.includes("gpu");
152-
});
153-
154-
core.setOutput("available", available ? "true" : "false");
155-
if (!available) {
156-
const message = "No online self-hosted runner with label 'gpu' was found. Dated image was pushed; latest/nightly-stable were not moved.";
157-
if (mode === "required") {
158-
core.setFailed(message);
159-
} else {
160-
core.notice(message);
161-
}
162-
}
163-
164-
smoke:
165-
name: GPU smoke and alias promotion
166-
needs:
167-
- build
168-
- detect-smoke-runner
169-
if: needs.detect-smoke-runner.outputs.available == 'true'
170-
runs-on:
171-
- self-hosted
172-
- linux
173-
- x64
174-
- gpu
175-
timeout-minutes: 120
176-
env:
177-
IMAGE_REF: ${{ needs.build.outputs.image_ref }}
178-
IMAGE_NAME: ghcr.io/noonghunna/vllm-club3090
179-
COMPOSE_PROJECT_NAME: club3090-ci-vllm-dual
180-
COMPOSE_OVERRIDE: /tmp/club3090-ci-vllm-image.override.yml
181-
URL: http://localhost:8010
182-
MODEL: qwen3.6-27b-autoround
183-
CONTAINER: vllm-qwen36-27b-dual
184-
steps:
185-
- name: Checkout
186-
uses: actions/checkout@v4
187-
188108
- name: Log in to GHCR
189109
uses: docker/login-action@v3
190110
with:
191111
registry: ghcr.io
192112
username: ${{ github.actor }}
193113
password: ${{ secrets.GITHUB_TOKEN }}
194114

195-
- name: Prepare canonical compose override
196-
shell: bash
197-
run: |
198-
set -euo pipefail
199-
docker pull "${IMAGE_REF}"
200-
cat > "${COMPOSE_OVERRIDE}" <<EOF
201-
services:
202-
vllm-qwen36-27b-dual:
203-
image: ${IMAGE_REF}
204-
EOF
205-
206-
- name: Stop prior club-3090 estate if present
207-
shell: bash
208-
run: |
209-
set -euo pipefail
210-
if [[ -f "${HOME}/.club3090/estate.yml" ]]; then
211-
bash scripts/launch.sh --down-estate "${HOME}/.club3090/estate.yml" || true
212-
fi
213-
docker compose \
214-
-f "${CANONICAL_COMPOSE}" \
215-
-f "${COMPOSE_OVERRIDE}" \
216-
-p "${COMPOSE_PROJECT_NAME}" \
217-
down --remove-orphans || true
218-
219-
- name: Boot canonical dual vLLM compose
220-
shell: bash
221-
run: |
222-
set -euo pipefail
223-
docker compose \
224-
-f "${CANONICAL_COMPOSE}" \
225-
-f "${COMPOSE_OVERRIDE}" \
226-
-p "${COMPOSE_PROJECT_NAME}" \
227-
up -d
228-
229-
- name: Wait for OpenAI endpoint
230-
shell: bash
231-
run: |
232-
set -euo pipefail
233-
for _ in {1..120}; do
234-
if curl -sf -m 5 "${URL}/v1/models" >/dev/null; then
235-
exit 0
236-
fi
237-
sleep 10
238-
done
239-
docker compose \
240-
-f "${CANONICAL_COMPOSE}" \
241-
-f "${COMPOSE_OVERRIDE}" \
242-
-p "${COMPOSE_PROJECT_NAME}" \
243-
logs --tail=200
244-
exit 1
245-
246-
- name: Run verify-full
247-
shell: bash
248-
run: |
249-
set -euo pipefail
250-
URL="${URL}" MODEL="${MODEL}" CONTAINER="${CONTAINER}" bash scripts/verify-full.sh
251-
252-
- name: Run 3-prompt smoke bench
253-
shell: bash
254-
run: |
255-
set -euo pipefail
256-
python3 - <<'PY'
257-
import json
258-
import time
259-
import urllib.request
260-
261-
url = "http://localhost:8010"
262-
model = "qwen3.6-27b-autoround"
263-
prompts = [
264-
("narrative", "Write a concise paragraph explaining transformer attention.", 96),
265-
("code", "Write a small Python function that returns the nth Fibonacci number.", 96),
266-
("reasoning", "A train leaves at 08:00 traveling 60 km/h. Another leaves at 09:00 traveling 90 km/h. When does the second catch up?", 96),
267-
]
268-
269-
for label, prompt, max_tokens in prompts:
270-
body = json.dumps({
271-
"model": model,
272-
"messages": [{"role": "user", "content": prompt}],
273-
"max_tokens": max_tokens,
274-
"temperature": 0.3,
275-
"stream": False,
276-
"chat_template_kwargs": {"enable_thinking": False},
277-
}).encode()
278-
req = urllib.request.Request(
279-
f"{url}/v1/chat/completions",
280-
data=body,
281-
headers={"Content-Type": "application/json"},
282-
)
283-
start = time.time()
284-
with urllib.request.urlopen(req, timeout=300) as response:
285-
data = json.load(response)
286-
wall = time.time() - start
287-
usage = data.get("usage") or {}
288-
tokens = usage.get("completion_tokens") or 0
289-
text = data["choices"][0]["message"].get("content") or ""
290-
if not text.strip():
291-
raise SystemExit(f"{label}: empty completion")
292-
tps = tokens / wall if wall > 0 else 0
293-
print(f"{label}: wall={wall:.2f}s completion_tokens={tokens} wall_TPS={tps:.2f}")
294-
PY
295-
296-
- name: Promote latest aliases
115+
- name: Promote :latest and :nightly-stable to dated tag
297116
shell: bash
117+
env:
118+
IMAGE_REF: ${{ needs.build.outputs.image_ref }}
298119
run: |
299120
set -euo pipefail
300-
docker tag "${IMAGE_REF}" "${IMAGE_NAME}:latest"
301-
docker tag "${IMAGE_REF}" "${IMAGE_NAME}:nightly-stable"
302-
docker push "${IMAGE_NAME}:latest"
303-
docker push "${IMAGE_NAME}:nightly-stable"
304-
305-
- name: Cleanup canonical compose
306-
if: always()
307-
shell: bash
308-
run: |
309-
docker compose \
310-
-f "${CANONICAL_COMPOSE}" \
311-
-f "${COMPOSE_OVERRIDE}" \
312-
-p "${COMPOSE_PROJECT_NAME}" \
313-
down --remove-orphans || true
121+
docker buildx imagetools create \
122+
-t "${IMAGE_NAME}:latest" \
123+
-t "${IMAGE_NAME}:nightly-stable" \
124+
"${IMAGE_REF}"
314125
315126
retention:
316127
name: Retain four weeks of dated nightlies

0 commit comments

Comments
 (0)