@@ -67,41 +67,12 @@ jobs:
6767 - name : Set up uv
6868 uses : astral-sh/setup-uv@v6
6969
70- - name : Test Studio release backend
71- run : |
72- uv run --group dev pytest -q \
73- tests/test_studio_release_server.py \
74- tests/cli/test_studio_release.py
75-
76- - name : Stage and test release source
77- id : source
78- run : |
79- set -euo pipefail
80- archive="$RUNNER_TEMP/studio-release-source.tar.gz"
81- source_parent="$RUNNER_TEMP/studio-release-source"
82- source_root="$source_parent/veadk-python-${GITHUB_SHA}"
83- prepared_root="$source_root/.studio-release"
84- mkdir -p "$source_root" "$prepared_root/frontend"
85- git archive "$GITHUB_SHA" -- \
86- pyproject.toml README.md LICENSE frontend veadk \
87- ':(exclude)veadk/webui' | tar -x -C "$source_root"
88- npm ci --prefix frontend
89- npm test --prefix frontend
90- npm run build --prefix frontend -- \
91- --outDir "$prepared_root/frontend"
92- python -m veadk.cli.studio_dependencies \
93- --output-dir "$prepared_root/wheels" \
94- --manifest "$prepared_root/dependencies.json"
95- tar -C "$source_parent" \
96- --exclude="veadk-python-${GITHUB_SHA}/.studio-release/wheels" \
97- -czf "$archive" \
98- "veadk-python-${GITHUB_SHA}"
99- echo "archive=$archive" >> "$GITHUB_OUTPUT"
100- echo "source_root=$source_root" >> "$GITHUB_OUTPUT"
101-
10270 - name : Build and validate Studio bundle
10371 env :
104- SOURCE_ROOT : ${{ steps.source.outputs.source_root }}
72+ PIP_INDEX_URL : https://mirrors.aliyun.com/pypi/simple/
73+ UV_DEFAULT_INDEX : https://mirrors.aliyun.com/pypi/simple/
74+ npm_config_registry : https://registry.npmmirror.com
75+ npm_config_replace_registry_host : always
10576 run : |
10677 set -euo pipefail
10778 output_dir="$RUNNER_TEMP/studio-release-output"
@@ -114,16 +85,14 @@ jobs:
11485
11586 from veadk.cli.studio_release import build_studio_release
11687
117- source_root = Path(os.environ["SOURCE_ROOT "])
88+ source_root = Path(os.environ["GITHUB_WORKSPACE "])
11889 output_dir = Path(os.environ["output_dir"])
11990 bundle, manifest = build_studio_release(
12091 source_root=source_root,
12192 output_dir=output_dir,
12293 version=os.environ["version"],
12394 git_sha=os.environ["GITHUB_SHA"],
12495 changelog=("Pull request release validation",),
125- frontend_assets=source_root / ".studio-release" / "frontend",
126- dependency_wheels=source_root / ".studio-release" / "wheels",
12796 )
12897 print(
12998 json.dumps(
@@ -143,15 +112,6 @@ jobs:
143112 test "${#manifests[@]}" -eq 1
144113 unzip -t "${bundles[0]}"
145114
146- - name : Preserve staged source for publishing
147- if : github.event_name == 'workflow_dispatch'
148- uses : actions/upload-artifact@v4
149- with :
150- name : studio-release-source-${{ github.sha }}
151- path : ${{ steps.source.outputs.archive }}
152- compression-level : 0
153- retention-days : 1
154-
155115 publish :
156116 if : >-
157117 github.event_name == 'workflow_dispatch' &&
@@ -166,12 +126,6 @@ jobs:
166126 RELEASE_SERVER_API_KEY : ${{ secrets.STUDIO_RELEASE_SERVER_API_KEY }}
167127
168128 steps :
169- - name : Download staged release source
170- uses : actions/download-artifact@v4
171- with :
172- name : studio-release-source-${{ github.sha }}
173- path : ${{ runner.temp }}/studio-release-source
174-
175129 - name : Validate release server configuration
176130 run : |
177131 set -euo pipefail
@@ -190,119 +144,124 @@ jobs:
190144 fi
191145 echo "RELEASE_SERVER_URL=$release_server_url" >> "$GITHUB_ENV"
192146
193- - name : Resolve release changelog
194- id : changelog
147+ - name : Request Studio release
195148 env :
196- MANUAL_CHANGELOG : ${{ inputs.changelog }}
149+ STUDIO_CHANGELOG : ${{ inputs.changelog }}
197150 run : |
198151 set -euo pipefail
199- changelog="$MANUAL_CHANGELOG"
200- test -n "$changelog" || {
152+ test -n "$STUDIO_CHANGELOG" || {
201153 echo "Studio release changelog is empty." >&2
202154 exit 1
203155 }
204- {
205- echo "value<<STUDIO_CHANGELOG"
206- echo "$changelog"
207- echo "STUDIO_CHANGELOG"
208- } >> "$GITHUB_OUTPUT"
156+ python - <<'PY'
157+ import http.client
158+ import json
159+ import os
160+ import sys
161+ import time
162+ import urllib.error
163+ import urllib.request
209164
210- - name : Upload staged release source
211- id : source
212- run : |
213- set -euo pipefail
214- job_id="${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
215- archive="$RUNNER_TEMP/studio-release-source/studio-release-source.tar.gz"
216- test -s "$archive"
217- for attempt in 1 2 3; do
218- echo "Uploading staged source to TOS (attempt $attempt/3)."
219- upload_response="$(curl --fail-with-body --silent --show-error \
220- --proto '=https' \
221- --connect-timeout 30 \
222- --max-time 60 \
223- --request POST \
224- --header "Content-Type: application/json" \
225- --header "X-API-Key: $RELEASE_SERVER_API_KEY" \
226- --data "$(jq -n --arg requestId "$job_id" \
227- '{requestId:$requestId}')" \
228- --url "$RELEASE_SERVER_URL/source-upload")"
229- source_key="$(jq -er '.sourceKey' <<<"$upload_response")"
230- upload_url="$(jq -er '.uploadUrl' <<<"$upload_response")"
231- if curl --fail-with-body --silent --show-error \
232- --proto '=https' \
233- --connect-timeout 30 \
234- --max-time 600 \
235- --speed-limit 1024 \
236- --speed-time 60 \
237- --request PUT \
238- --header "Content-Type: application/gzip" \
239- --upload-file "$archive" \
240- --write-out 'TOS upload status=%{http_code} uploaded=%{size_upload} speed=%{speed_upload} time=%{time_total}\n' \
241- --url "$upload_url"; then
242- break
243- else
244- upload_exit=$?
245- fi
246- if (( attempt == 3 )); then
247- echo "TOS source upload failed after 3 attempts." >&2
248- exit "$upload_exit"
249- fi
250- echo "TOS source upload failed; requesting a fresh signed URL before retrying." >&2
251- sleep $((attempt * 10))
252- done
253- echo "job_id=$job_id" >> "$GITHUB_OUTPUT"
254- echo "source_key=$source_key" >> "$GITHUB_OUTPUT"
165+ job_id = (
166+ f"{os.environ['GITHUB_RUN_ID']}-"
167+ f"{os.environ['GITHUB_RUN_ATTEMPT']}"
168+ )
169+ payload = json.dumps(
170+ {
171+ "repository": os.environ["GITHUB_REPOSITORY"],
172+ "gitSha": os.environ["GITHUB_SHA"],
173+ "requestId": job_id,
174+ "changelog": [os.environ["STUDIO_CHANGELOG"]],
175+ }
176+ ).encode()
177+ request = urllib.request.Request(
178+ f"{os.environ['RELEASE_SERVER_URL']}/release",
179+ data=payload,
180+ headers={
181+ "Accept": "text/event-stream",
182+ "Content-Type": "application/json",
183+ "X-API-Key": os.environ["RELEASE_SERVER_API_KEY"],
184+ },
185+ method="POST",
186+ )
187+ def report(current):
188+ print(
189+ "Studio release "
190+ f"state={current['state']} stage={current['stage']} "
191+ f"message={current['message']}",
192+ flush=True,
193+ )
255194
256- - name : Request Studio release
257- id : release
258- env :
259- STUDIO_CHANGELOG : ${{ steps.changelog.outputs.value }}
260- JOB_ID : ${{ steps.source.outputs.job_id }}
261- SOURCE_KEY : ${{ steps.source.outputs.source_key }}
262- run : |
263- set -euo pipefail
264- payload="$(jq -n \
265- --arg repository "$GITHUB_REPOSITORY" \
266- --arg gitSha "$GITHUB_SHA" \
267- --arg requestId "$JOB_ID" \
268- --arg changelog "$STUDIO_CHANGELOG" \
269- --arg sourceKey "$SOURCE_KEY" \
270- '{repository:$repository,gitSha:$gitSha,requestId:$requestId,changelog:[$changelog],sourceKey:$sourceKey}')"
271- curl --fail-with-body --silent --show-error \
272- --proto '=https' \
273- --connect-timeout 30 \
274- --max-time 1800 \
275- --request POST \
276- --header "Content-Type: application/json" \
277- --header "X-API-Key: $RELEASE_SERVER_API_KEY" \
278- --data "$payload" \
279- --url "$RELEASE_SERVER_URL/release" >/dev/null
280- echo "job_id=$JOB_ID" >> "$GITHUB_OUTPUT"
195+ terminal = None
196+ stream_error = None
197+ for attempt in range(1, 4):
198+ try:
199+ with urllib.request.urlopen(request, timeout=1800) as response:
200+ for raw_line in response:
201+ line = raw_line.decode().strip()
202+ if not line.startswith("data: "):
203+ continue
204+ terminal = json.loads(line.removeprefix("data: "))
205+ report(terminal)
206+ stream_error = None
207+ break
208+ except (
209+ OSError,
210+ urllib.error.URLError,
211+ http.client.HTTPException,
212+ ) as error:
213+ stream_error = error
214+ print(
215+ f"Studio release stream interrupted (attempt {attempt}/3): "
216+ f"{error}",
217+ file=sys.stderr,
218+ flush=True,
219+ )
220+ if attempt < 3:
221+ time.sleep(2 * attempt)
281222
282- - name : Wait for Studio release
283- env :
284- JOB_ID : ${{ steps.release.outputs.job_id }}
285- run : |
286- set -euo pipefail
287- for _ in $(seq 1 180); do
288- response="$(curl --fail-with-body --silent --show-error \
289- --proto '=https' \
290- --connect-timeout 30 \
291- --max-time 60 \
292- --header "X-API-Key: $RELEASE_SERVER_API_KEY" \
293- --url "$RELEASE_SERVER_URL/status/$JOB_ID")"
294- state="$(jq -r '.state' <<<"$response")"
295- stage="$(jq -r '.stage' <<<"$response")"
296- echo "Studio release state=$state stage=$stage"
297- if [[ "$state" == "succeeded" ]]; then
298- jq '{state,stage,result}' <<<"$response"
299- exit 0
300- fi
301- if [[ "$state" == "failed" ]]; then
302- jq '{state,stage,message,error}' <<<"$response" >&2
303- exit 1
304- fi
305- sleep 10
306- done
307- echo "Studio release timed out after 30 minutes." >&2
308- exit 1
223+ deadline = time.monotonic() + 1800
224+ missing_status = 0
225+ while terminal is None or terminal["state"] not in {
226+ "succeeded",
227+ "failed",
228+ }:
229+ if time.monotonic() >= deadline:
230+ raise RuntimeError(
231+ "Studio release did not reach a terminal state."
232+ ) from stream_error
233+ status_request = urllib.request.Request(
234+ f"{os.environ['RELEASE_SERVER_URL']}/status/{job_id}",
235+ headers={
236+ "X-API-Key": os.environ["RELEASE_SERVER_API_KEY"],
237+ },
238+ )
239+ try:
240+ with urllib.request.urlopen(
241+ status_request,
242+ timeout=60,
243+ ) as response:
244+ terminal = json.load(response)
245+ missing_status = 0
246+ report(terminal)
247+ except urllib.error.HTTPError as error:
248+ if error.code != 404:
249+ print(f"Status check failed: {error}", file=sys.stderr)
250+ else:
251+ missing_status += 1
252+ if missing_status >= 3 and terminal is None:
253+ raise RuntimeError(
254+ "Studio release request was not accepted."
255+ ) from stream_error
256+ except (OSError, urllib.error.URLError) as error:
257+ print(f"Status check interrupted: {error}", file=sys.stderr)
258+ if terminal is None or terminal["state"] not in {
259+ "succeeded",
260+ "failed",
261+ }:
262+ time.sleep(2)
263+ if terminal["state"] != "succeeded":
264+ print(json.dumps(terminal, ensure_ascii=False), file=sys.stderr)
265+ raise SystemExit(1)
266+ print(json.dumps(terminal["result"], ensure_ascii=False, indent=2))
267+ PY
0 commit comments