Skip to content

Commit 787c4d4

Browse files
committed
feat(FR-2887): serve released bundle via Portless with version-named URL
1 parent adaa5a0 commit 787c4d4

2 files changed

Lines changed: 218 additions & 2 deletions

File tree

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
---
2+
name: serve-release
3+
description: >
4+
Download a released backend.ai-webui bundle from GitHub Releases and serve it
5+
locally via Portless so the URL carries the version name
6+
(e.g. `https://v26-4-8-rc-3.localhost:1355`). Uses `scripts/serve-release.sh`
7+
under the hood, which fetches the bundle, copies the local `config.toml` /
8+
plugins into the extracted folder, and runs `serve` behind Portless.
9+
Trigger on: "릴리즈 띄워", "릴리즈 버전 실행", "serve release", "run release",
10+
"release 띄워줘", "특정 버전 띄워줘", "released bundle 띄워줘", "stage release",
11+
or any request that names a specific webui release version (e.g. "v26.4.8-rc.3
12+
실행해줘", "26.4.7 띄워줘").
13+
---
14+
15+
# Serve Release
16+
17+
Serves an already-published `backend.ai-webui` release bundle locally so it can
18+
be tested under a stable, version-named Portless URL. Wraps
19+
`scripts/serve-release.sh` — do not reinvent the download/extract logic.
20+
21+
## 1. Resolve the version
22+
23+
Pick a version with this priority:
24+
25+
1. **User-provided version in the prompt** — accept `26.4.8-rc.3`, `v26.4.8-rc.3`,
26+
or a release URL (`.../releases/tag/v26.4.8-rc.3`). Strip the leading `v` if
27+
present; everything downstream uses the bare semver.
28+
2. **Most recent release** — if the user said "릴리즈 띄워줘" without a version,
29+
fetch the latest tag with:
30+
```bash
31+
gh release list --repo lablup/backend.ai-webui --limit 10
32+
```
33+
Show the top 5–10 entries with `AskUserQuestion` so the user can pick (include
34+
stable and prerelease releases; mark prereleases). Do **not** silently default
35+
to the absolute latest — the user usually wants a specific one.
36+
3. **Tag exists check** — before running the script, confirm the tag exists:
37+
```bash
38+
gh release view v<VERSION> --repo lablup/backend.ai-webui --json tagName -q .tagName
39+
```
40+
If the tag does not exist, surface the error and offer the `gh release list`
41+
output so the user can correct the version. Do not try fuzzy matching.
42+
43+
The bundle asset must exist on the release. The script downloads
44+
`backend.ai-webui-bundle-<VERSION>.zip` from
45+
`https://github.com/lablup/backend.ai-webui/releases/download/v<VERSION>/...`.
46+
If the user picked a release where this asset is missing (rare, mostly for
47+
hotfix-only tags), the `curl` step will fail with 404 — surface the failure and
48+
suggest a different version rather than retrying.
49+
50+
## 2. Pre-flight checks
51+
52+
Before invoking the script, verify:
53+
54+
- **`config.toml` exists** in the project root. The script copies it into the
55+
extracted folder so the served bundle points at your local backend. If missing,
56+
copy `config.toml.sample` to `config.toml` first and remind the user to edit
57+
endpoints if needed.
58+
- **`serve` is on PATH** (`command -v serve`). If absent, tell the user to run
59+
`npm install -g serve` — the script also errors with the same hint.
60+
- **`portless` is on PATH** (`command -v portless`). If absent, the script
61+
silently falls back to plain `serve` on `http://localhost:<SERVE_PORT>`; tell
62+
the user the URL will be HTTP-only without Portless and suggest
63+
`npm install -g portless` (or `pnpm add -g portless`) if they want the
64+
named-subdomain experience.
65+
66+
## 3. Decide ports
67+
68+
The script reads two optional env vars:
69+
70+
- `SERVE_PORT` — the HTTP port `serve` listens on (default `9091`). Pin only
71+
if the user asked for a specific port, or `9091` is occupied.
72+
- `PORTLESS_PORT` — the Portless daemon port (default `1355`, same as
73+
`scripts/dev.mjs`). Do not change unless the user explicitly asks.
74+
75+
Quick check for port conflict on the default:
76+
77+
```bash
78+
lsof -nP -iTCP:9091 -sTCP:LISTEN 2>/dev/null | head -1
79+
```
80+
81+
If something's already on `9091`, pick the next free port in the 9091–9099 range
82+
and pass `SERVE_PORT=<port>`.
83+
84+
## 4. Compute the Portless URL
85+
86+
The script derives the Portless app slug from the version. Replicate the same
87+
sanitization so you can announce the URL before the server is fully up:
88+
89+
```
90+
v<VERSION> -> lowercased
91+
-> [^a-z0-9-] replaced with -
92+
-> repeated - collapsed
93+
-> leading/trailing - trimmed
94+
-> capped at 40 chars
95+
```
96+
97+
Examples:
98+
- `26.4.8-rc.3` -> app `v26-4-8-rc-3` -> `https://v26-4-8-rc-3.localhost:1355`
99+
- `26.4.7` -> app `v26-4-7` -> `https://v26-4-7.localhost:1355`
100+
- `25.7.1` -> app `v25-7-1` -> `https://v25-7-1.localhost:1355`
101+
102+
Use the standard daemon port (`1355`) unless the user pinned `PORTLESS_PORT`.
103+
104+
## 5. Run the script
105+
106+
Always run **in the background**`serve-release.sh` blocks on `serve`/`portless`
107+
and the user will need their shell back to interact with the served instance.
108+
Run from the project root so the `config.toml` and `dist/plugins` lookup paths
109+
resolve correctly.
110+
111+
Default invocation:
112+
113+
```bash
114+
./scripts/serve-release.sh <VERSION>
115+
```
116+
117+
With a custom serve port:
118+
119+
```bash
120+
SERVE_PORT=9092 ./scripts/serve-release.sh <VERSION>
121+
```
122+
123+
Use `run_in_background: true` and announce in one short sentence what's
124+
happening (e.g. *"Downloading v26.4.8-rc.3 bundle and serving via Portless at
125+
https://v26-4-8-rc-3.localhost:1355."*).
126+
127+
## 6. Wait for "Server starting"
128+
129+
The script prints a few stages — download, extract, copy `config.toml`, then
130+
finally Portless + `serve` startup. Poll the background task's output with a
131+
short until-loop (cap ~30s for first download, ~5s if the bundle is cached) for
132+
the line containing `Press Ctrl+C to stop the server`. Once seen, the URL is
133+
ready.
134+
135+
If `curl` fails (404 / network error), the script exits non-zero — surface the
136+
last ~20 lines of output to the user and don't pretend it's running.
137+
138+
## 7. Announce both URLs
139+
140+
After the server is up, present **two** URLs on separate lines so the user can
141+
pick whichever they prefer — Portless (HTTPS, named) and direct (HTTP, port).
142+
Format exactly like this, no preamble:
143+
144+
```
145+
Portless: https://v26-4-8-rc-3.localhost:1355
146+
Direct: http://localhost:9091
147+
```
148+
149+
If Portless wasn't available and the script fell back to plain `serve`, show
150+
only the Direct line and call out the fallback once.
151+
152+
## 8. Common follow-ups
153+
154+
- **"Stop the server"** — kill the background task. Don't `portless proxy stop`
155+
unless asked; that kills *all* portless apps including any running dev server.
156+
- **"Serve a different version"** — stop the current background task first
157+
(port collision otherwise), then invoke the script with the new version.
158+
- **"Switch the backend endpoint"** — the served bundle uses the project-root
159+
`config.toml` that was copied at script-start. To change endpoints, edit
160+
`config.toml`, then **restart** the script (the in-place copy is one-shot).
161+
- **Cached extraction** — the script reuses `scripts/temp-releases/webui-<VERSION>/`
162+
if it exists, skipping download. If the user reports stale content, delete that
163+
directory and rerun.
164+
165+
## 9. Out of scope
166+
167+
- Do not modify `scripts/serve-release.sh` for one-off behaviour changes —
168+
prefer driving via env vars (`SERVE_PORT`, `PORTLESS_PORT`).
169+
- Do not try to install `serve` or `portless` for the user; surface the missing
170+
binary and let them choose.
171+
- Do not run alongside `pnpm dev` on the same Portless daemon if you suspect a
172+
port conflict between `serve` (9091) and the React dev server (Portless picks
173+
free port, but pinned `PORT=9081`/etc. from the user could collide). When in
174+
doubt, check `lsof` first.

scripts/serve-release.sh

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,48 @@ if ! command -v serve &> /dev/null; then
8383
exit 1
8484
fi
8585

86-
echo "Server starting"
86+
# Pick the HTTP port `serve` will listen on. Honour SERVE_PORT if the caller
87+
# pinned one, otherwise default to the legacy 9091.
88+
SERVE_PORT="${SERVE_PORT:-9091}"
89+
90+
# Derive a Portless-safe app slug from the version so the URL reflects the
91+
# released version (e.g. `26.4.8-rc.3` -> `v26-4-8-rc-3`). Portless cert
92+
# generation does not like dots / very long subdomains, so collapse anything
93+
# outside `[a-z0-9-]`, dedupe dashes, trim, and cap at 40 chars. We prefix `v`
94+
# so the subdomain doesn't start with a digit (some DNS-style validators
95+
# reject leading digits in labels even though browsers accept them).
96+
APP_NAME=$(echo "v${VERSION}" \
97+
| tr '[:upper:]' '[:lower:]' \
98+
| sed -E 's/[^a-z0-9-]+/-/g; s/-+/-/g; s/^-+//; s/-+$//' \
99+
| cut -c1-40)
100+
101+
# If portless isn't installed, fall back to plain `serve` on the chosen port.
102+
if ! command -v portless &> /dev/null; then
103+
echo "Portless not found — serving directly on http://localhost:${SERVE_PORT}"
104+
echo "Press Ctrl+C to stop the server"
105+
serve -s -l "${SERVE_PORT}" .
106+
exit $?
107+
fi
108+
109+
# Ensure the Portless daemon is running on the project's standard port (1355).
110+
# `portless proxy start` is idempotent; honour PORTLESS_PORT if already set by
111+
# the caller, matching `scripts/dev.mjs` conventions.
112+
PORTLESS_DAEMON_PORT="${PORTLESS_PORT:-1355}"
113+
if [ -z "${PORTLESS_PORT:-}" ]; then
114+
portless proxy start -p "${PORTLESS_DAEMON_PORT}" || true
115+
else
116+
portless proxy start || true
117+
fi
118+
119+
echo ""
120+
echo "Serving v${VERSION} via Portless:"
121+
echo " https://${APP_NAME}.localhost:${PORTLESS_DAEMON_PORT}"
122+
echo " (direct: http://localhost:${SERVE_PORT})"
87123
echo "Press Ctrl+C to stop the server"
88-
serve -s -l 9091 .
124+
echo ""
125+
126+
# `portless <name> <cmd>` routes traffic from the named subdomain to <cmd>.
127+
# Pin --app-port so portless knows exactly where `serve` listens; this also
128+
# avoids portless picking a random port and confusing the "direct" fallback URL.
129+
exec portless "${APP_NAME}" --force --app-port "${SERVE_PORT}" -- \
130+
serve -s -l "${SERVE_PORT}" .

0 commit comments

Comments
 (0)