Add cached Web-only releases through existing Docker runtime
PROMPT Docker CI and deployment / verify-and-deploy (push) Successful in 5m36s

This commit is contained in:
2026-10-23 19:45:26 +08:00
parent 83e19d31ca
commit b67574f950
5 changed files with 192 additions and 1 deletions
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env bash
set -Eeuo pipefail
if [ "$(id -u)" -ne 0 ]; then exec sudo -n bash "$0" "$@"; fi
SHA=${1:?Release SHA is required}
[[ "$SHA" =~ ^[0-9a-f]{40}$ ]] || exit 1
ROOT=/opt/prompt
PREVIOUS=$(readlink -f "$ROOT/current")
test -f "$PREVIOUS/compose.yml"
BASE=$(sed -n 's/^RELEASE_SHA=//p' "$PREVIOUS/release.env")
test "$(git -c safe.directory="$PWD" rev-parse HEAD)" = "$SHA"
# Never let a manually invoked fast release bypass the change policy.
export GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=safe.directory GIT_CONFIG_VALUE_0="$PWD"
test "$(bash scripts/release-mode.sh "$BASE")" = web
IMAGE=$(sed -n 's/^PROMPT_IMAGE=//p' "$PREVIOUS/release.env")
[[ "$IMAGE" =~ ^prompt:[0-9a-f]{40}$ ]] || exit 1
BUILDER="prompt-tests:${IMAGE#prompt:}"
docker image inspect "$BUILDER" >/dev/null
RELEASE="$ROOT/releases/$SHA"
CACHE="$ROOT/web-cache/${IMAGE#prompt:}"
mkdir -p "$ROOT/web-artifacts" "$ROOT/web-cache" "$ROOT/releases"
exec 8>"$ROOT/.web-build.lock"
flock -w 300 8
# Build away from the running application. Only the compiler cache is shared.
ARTIFACT=$(mktemp -d "$ROOT/web-artifacts/$SHA.XXXXXX")
mkdir -p "$CACHE"
CONTAINER="prompt-web-build-$SHA"
cleanup(){ docker rm -f "$CONTAINER" >/dev/null 2>&1 || true; }
trap cleanup EXIT
# The existing validated test image contains the matching Node/toolchain/dependencies.
# Replacing source in a disposable container does not commit/export image layers.
docker run --name "$CONTAINER" --user 0:0 \
--mount "type=bind,src=$PWD,dst=/source,readonly" \
--mount "type=bind,src=$ARTIFACT,dst=/output" \
--mount "type=bind,src=$CACHE,dst=/app/.next/cache" \
--entrypoint bash "$BUILDER" -ceu '
cd /app
rm -rf src public scripts tests db seed
cp -a /source/src /source/public /source/scripts /source/tests /source/db /source/seed ./
cp /source/package.json /source/package-lock.json /source/tsconfig.json /source/next-env.d.ts /source/next.config.ts /source/postcss.config.mjs ./
npm run typecheck
npm test
node node_modules/next/dist/bin/next build
node scripts/prepare-standalone.mjs
npm run test:smoke
cp -a .next/standalone/. /output/
chmod -R a+rX /output
'
cleanup
# Serialize cutover with full deployments and recheck the running release.
exec 9>"$ROOT/.deploy.lock"
flock -w 300 9
test "$(readlink -f "$ROOT/current")" = "$PREVIOUS"
LATEST=$(git -c safe.directory="$PWD" ls-remote origin refs/heads/main | cut -f1)
if [ "$LATEST" != "$SHA" ]; then echo 'Skipping stale Web release'; exit 0; fi
mkdir -p "$RELEASE"
cp "$PREVIOUS/release.env" "$RELEASE/release.env"
sed -i "s/^RELEASE_SHA=.*/RELEASE_SHA=$SHA/" "$RELEASE/release.env"
cat > "$RELEASE/web.override.yml" <<YAML
services:
web:
command: ["node", "/web/server.js"]
volumes:
- type: bind
source: $ARTIFACT
target: /web
read_only: true
YAML
# Preserve variable expressions: never materialize production secrets into release files.
docker compose -p prompt -f "$PREVIOUS/compose.yml" -f "$RELEASE/web.override.yml" config --no-interpolate > "$RELEASE/compose.yml"
compose_at(){ local dir="$1"; shift; env -u PROMPT_IMAGE -u PROMPT_TEST_IMAGE -u RELEASE_SHA -u WEB_PORT -u WEB_BIND_ADDRESS docker compose -p prompt --env-file /etc/prompt.env --env-file "$dir/release.env" -f "$dir/compose.yml" "$@"; }
CUTOVER=0
rollback(){
code=$?
trap - ERR
if [ "$CUTOVER" -eq 1 ]; then compose_at "$PREVIOUS" up -d --no-deps --no-build --wait --wait-timeout 120 web || true; fi
exit "$code"
}
trap rollback ERR
CUTOVER=1
compose_at "$RELEASE" up -d --no-deps --no-build --wait --wait-timeout 120 web
compose_at "$RELEASE" exec -T web node /app/container-health.mjs
compose_at "$RELEASE" exec -T web node -e '
(async()=>{
for(const path of ["/", "/agent/index.md", "/tools/skills"]){
const r=await fetch("http://127.0.0.1:3000"+path);
if(!r.ok)throw Error(path+" returned "+r.status);
if(path.endsWith(".md")&&!r.headers.get("content-type")?.includes("text/markdown"))throw Error("Expected Markdown");
}
})().catch(e=>{console.error(e.message);process.exit(1)})'
ln -sfn "$RELEASE" "$ROOT/.current.next"
mv -Tf "$ROOT/.current.next" "$ROOT/current"
echo "Web-only release verified: $SHA; runtime image=$IMAGE; worker and infrastructure unchanged"