Add cached Web-only releases through existing Docker runtime
PROMPT Docker CI and deployment / verify-and-deploy (push) Successful in 5m36s
PROMPT Docker CI and deployment / verify-and-deploy (push) Successful in 5m36s
This commit is contained in:
@@ -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"
|
||||
Reference in New Issue
Block a user