#!/usr/bin/env bash # Package the two release artifacts (run from the repo root by build-artifacts.yml): # # spa.tar.gz = CONTENTS of the built SPA dir (ui/.output/public/*), so it extracts # straight into the nginx web root with _shell.html at the root. # app.tar.gz = the Python source the box runs `uv sync` against. `git archive` # gives a clean tree (no node_modules, no .venv, no local cruft); # ui/ is intentionally excluded (it ships as spa.tar.gz). set -euo pipefail SPA_DIR="ui/.output/public" [ -f "${SPA_DIR}/_shell.html" ] || { echo "ERROR: SPA build output missing ${SPA_DIR}/_shell.html (did 'bun run build' run?)" >&2 exit 1 } echo "==> spa.tar.gz from ${SPA_DIR}" tar -C "${SPA_DIR}" -czf spa.tar.gz . echo "==> app.tar.gz from source (git archive HEAD)" git archive --format=tar.gz -o app.tar.gz HEAD \ agent deploy langgraph.json pyproject.toml uv.lock README.md # Sanity: the box's `uv sync --frozen` needs pyproject.toml + uv.lock at the root, # and the package itself (agent/). Fail loudly here rather than on the box. for required in pyproject.toml uv.lock agent/server.py langgraph.json; do tar -tzf app.tar.gz | grep -qx "${required}" || { echo "ERROR: app.tar.gz is missing ${required}" >&2 exit 1 } done # Fail-closed secret guard: the source is git-archived wholesale, so reject the # release if a secret-shaped FILE slipped into the tracked tree (defense in depth # on top of .gitignore — the artifact lands on the box + in S3). Scoped to data # extensions so credential-handling *source* (e.g. team_credentials.py) is not a # false positive. SECRET_RE='(^|/)(\.env(\..+)?|id_rsa|.*\.(pem|key|p12|pfx)|.*(secret|credential|password|token)s?\.(json|ya?ml|txt|env|ini|cfg))$' if tar -tzf app.tar.gz | grep -qiE "${SECRET_RE}"; then echo "ERROR: app.tar.gz contains a secret-shaped file — refusing to publish:" >&2 tar -tzf app.tar.gz | grep -iE "${SECRET_RE}" >&2 exit 1 fi echo "==> artifacts:" ls -la spa.tar.gz app.tar.gz