ci: add isolated domestic frontend deployment
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
|
||||
const workflowUrl = new URL(
|
||||
"../../../.gitea/workflows/domestic-test.yml",
|
||||
import.meta.url,
|
||||
);
|
||||
const deployScriptUrl = new URL(
|
||||
"../../server/deploy-domestic-test-archive.sh",
|
||||
import.meta.url,
|
||||
);
|
||||
|
||||
test("domestic frontend workflow is manual and registry-free", async () => {
|
||||
const workflow = await readFile(workflowUrl, "utf8");
|
||||
|
||||
assert.match(workflow, /workflow_dispatch:/);
|
||||
assert.doesNotMatch(workflow, /\bpush:/);
|
||||
assert.doesNotMatch(workflow, /REGISTRY_IMAGE/);
|
||||
assert.match(workflow, /docker save/);
|
||||
assert.match(workflow, /DOMESTIC_TEST_SSH_PRIVATE_KEY/);
|
||||
});
|
||||
|
||||
test("domestic frontend workflow pins the China test API and isolated port", async () => {
|
||||
const workflow = await readFile(workflowUrl, "utf8");
|
||||
const deployScript = await readFile(deployScriptUrl, "utf8");
|
||||
|
||||
assert.match(workflow, /https:\/\/testapi\.banlv-ai\.com/);
|
||||
assert.match(workflow, /wss:\/\/testapi\.banlv-ai\.com\/ws/);
|
||||
assert.match(workflow, /9135/);
|
||||
assert.match(deployScript, /127\.0\.0\.1:\$HOST_PORT/);
|
||||
assert.match(deployScript, /cozsweet-frontend-domestic-test/);
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ARCHIVE="${1:-}"
|
||||
IMAGE="${2:-}"
|
||||
ENV_FILE="${3:-}"
|
||||
HOST_PORT="${4:-9135}"
|
||||
CONTAINER_NAME="cozsweet-frontend-domestic-test"
|
||||
INTERNAL_PORT="3000"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SERVICE_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
SHARED_ROOT="$SERVICE_ROOT/shared"
|
||||
RELEASE_ROOT="$SERVICE_ROOT/releases/docker"
|
||||
|
||||
if [ -z "$ARCHIVE" ] || [ -z "$IMAGE" ] || [ -z "$ENV_FILE" ]; then
|
||||
echo "Usage: $0 <image-archive> <image> <env-file> [host-port]" >&2
|
||||
exit 64
|
||||
fi
|
||||
if [ "$HOST_PORT" != "9135" ]; then
|
||||
echo "Domestic frontend test must use isolated host port 9135" >&2
|
||||
exit 64
|
||||
fi
|
||||
case "$ENV_FILE" in
|
||||
"$SHARED_ROOT"/*) ;;
|
||||
*)
|
||||
echo "Environment file must be stored under $SHARED_ROOT" >&2
|
||||
exit 64
|
||||
;;
|
||||
esac
|
||||
if [ ! -s "$ARCHIVE" ] || [ ! -s "$ENV_FILE" ]; then
|
||||
echo "Image archive or environment file is missing" >&2
|
||||
exit 66
|
||||
fi
|
||||
|
||||
mkdir -p "$RELEASE_ROOT"
|
||||
chmod 700 "$SHARED_ROOT"
|
||||
gzip -dc "$ARCHIVE" | docker load >/dev/null
|
||||
docker image inspect "$IMAGE" >/dev/null
|
||||
|
||||
old_image_id=""
|
||||
old_image_ref=""
|
||||
if docker container inspect "$CONTAINER_NAME" >/dev/null 2>&1; then
|
||||
old_image_id="$(docker inspect -f '{{.Image}}' "$CONTAINER_NAME")"
|
||||
old_image_ref="$(docker inspect -f '{{.Config.Image}}' "$CONTAINER_NAME")"
|
||||
elif ss -ltnH "sport = :$HOST_PORT" 2>/dev/null | grep -q .; then
|
||||
echo "Host port $HOST_PORT is already used by another service" >&2
|
||||
exit 69
|
||||
fi
|
||||
|
||||
run_container() {
|
||||
local image_ref="$1"
|
||||
docker run -d \
|
||||
--name "$CONTAINER_NAME" \
|
||||
--restart unless-stopped \
|
||||
-p "127.0.0.1:$HOST_PORT:$INTERNAL_PORT" \
|
||||
--env-file "$ENV_FILE" \
|
||||
-e "PORT=$INTERNAL_PORT" \
|
||||
"$image_ref" >/dev/null
|
||||
}
|
||||
|
||||
wait_healthy() {
|
||||
local deadline=$((SECONDS + 180))
|
||||
while [ "$SECONDS" -le "$deadline" ]; do
|
||||
state="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$CONTAINER_NAME" 2>/dev/null || true)"
|
||||
status="$(curl -sS -o /dev/null -w '%{http_code}' "http://127.0.0.1:$HOST_PORT/" 2>/dev/null || true)"
|
||||
if [[ "$status" =~ ^(200|204|301|302|303|307|308)$ ]] && { [ "$state" = "healthy" ] || [ "$state" = "running" ]; }; then
|
||||
return 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
deployment_ok="1"
|
||||
if ! run_container "$IMAGE"; then
|
||||
deployment_ok="0"
|
||||
elif ! wait_healthy; then
|
||||
deployment_ok="0"
|
||||
fi
|
||||
|
||||
if [ "$deployment_ok" != "1" ]; then
|
||||
docker logs --tail 120 "$CONTAINER_NAME" >&2 || true
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
if [ -n "$old_image_id" ] && docker image inspect "$old_image_id" >/dev/null 2>&1; then
|
||||
echo "New container failed health checks; restoring previous image" >&2
|
||||
run_container "$old_image_id"
|
||||
wait_healthy || true
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
created_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
image_id="$(docker image inspect "$IMAGE" -f '{{.Id}}')"
|
||||
record="{\"createdAt\":\"$created_at\",\"environment\":\"domestic-test\",\"image\":\"$IMAGE\",\"imageId\":\"$image_id\",\"container\":\"$CONTAINER_NAME\",\"hostPort\":$HOST_PORT}"
|
||||
if [ -f "$RELEASE_ROOT/current.json" ]; then
|
||||
cp "$RELEASE_ROOT/current.json" "$RELEASE_ROOT/previous.json"
|
||||
fi
|
||||
printf '%s\n' "$record" > "$RELEASE_ROOT/current.json"
|
||||
printf '%s\n' "$record" >> "$RELEASE_ROOT/releases.jsonl"
|
||||
|
||||
echo "Domestic frontend test is healthy"
|
||||
echo "container=$CONTAINER_NAME"
|
||||
echo "host_port=$HOST_PORT"
|
||||
echo "image=$IMAGE"
|
||||
if [ -n "$old_image_ref" ]; then
|
||||
echo "previous_image=$old_image_ref"
|
||||
fi
|
||||
Reference in New Issue
Block a user