93 lines
4.2 KiB
Bash
93 lines
4.2 KiB
Bash
#!/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"
|