a6167629cc
CI / verify (push) Successful in 3m23s
The monorepo was on npm workspaces. pnpm gives it a content-addressed store shared between the eight packages, a lockfile that records the whole graph rather than a flattened view of it, and — the reason this mattered in practice — `workspace:*`, which makes an internal dependency unambiguous instead of a version range that npm may satisfy from the registry. Mechanics: - `packageManager: pnpm@11.21.0` pins the version; corepack installs it in CI and in the image, so all three environments resolve identically. - The npm `workspaces` array is replaced by `pnpm-workspace.yaml`. pnpm ignores the former, and keeping both would leave two sources of truth. - All six internal dependencies moved to `workspace:*`. - Root scripts use `pnpm -r --if-present` and `pnpm -F <pkg>`. Two findings worth recording, both from running it rather than reading it: `tsx` was a devDependency, but the server runs TypeScript directly in production — the container's command is `pnpm exec tsx apps/api/src/server.ts`. Under npm this was concealed by the runtime stage re-installing tsx by hand after pruning dev dependencies. Under `pnpm install --prod` that sleight of hand stops working and the image simply fails to start. tsx is now declared in `dependencies`, which is what it has always actually been. The first image build failed with ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY. That is not a pnpm bug: it had decided the modules directory was stale and wanted confirmation before deleting it, which a non-interactive build cannot give. The trigger was the host's `node_modules` reaching the build context — there was no `.dockerignore` at all. pnpm's tree is symlinks into a content-addressed store, so copying it into an image produces dangling links and a directory pnpm rightly considers corrupt. Fixed by adding `.dockerignore` and setting `CI=true`, which is required in any non-interactive pnpm build. `esbuild` is denied install scripts via `allowBuilds`. Its platform binary arrives through the optional dependency `@esbuild/linux-x64` and the postinstall only verifies it; confirmed by running the binary directly, which reports 0.25.12. Verified under pnpm: typecheck clean, 150 tests / 0 failures, e2e passes, web builds. The image was built and booted against a real Postgres — health ok, `/api/dashboard` 401 with an issuer configured, `/` and `/capacity` serve the SPA, `/og.png` serves as image/png, and the migrator runs from the pruned runtime stage. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
87 lines
2.7 KiB
Bash
Executable File
87 lines
2.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Deploy PIG. Run on the host that serves it.
|
|
#
|
|
# ./scripts/deploy.sh
|
|
#
|
|
# Deliberately a script rather than automated push-to-deploy. Automating it
|
|
# would mean putting an SSH key with write access to the production host onto
|
|
# the CI runner, which is a meaningful escalation for a project this size. CI
|
|
# proves the commit is sound; a human decides when it ships.
|
|
#
|
|
# Safe to re-run. Migrations are additive and tracked.
|
|
|
|
set -euo pipefail
|
|
|
|
cd "$(dirname "$0")/.."
|
|
|
|
echo "==> Fetching"
|
|
git fetch -q origin
|
|
BEFORE=$(git rev-parse --short HEAD)
|
|
git reset --hard -q origin/main
|
|
AFTER=$(git rev-parse --short HEAD)
|
|
|
|
if [ "$BEFORE" = "$AFTER" ]; then
|
|
echo " Already at $AFTER"
|
|
else
|
|
echo " $BEFORE -> $AFTER"
|
|
git --no-pager log --oneline "$BEFORE..$AFTER" | sed 's/^/ /'
|
|
fi
|
|
|
|
echo "==> Backing up the database first"
|
|
# Cheap insurance. A migration that goes wrong on a database holding real deal
|
|
# data is not something to discover without a dump in hand.
|
|
mkdir -p backups
|
|
BACKUP="backups/pig-$(date +%Y%m%d-%H%M%S).sql.gz"
|
|
sudo docker compose -p pig exec -T db pg_dump -U pig pig | gzip > "$BACKUP"
|
|
echo " $BACKUP ($(du -h "$BACKUP" | cut -f1))"
|
|
|
|
echo "==> Building"
|
|
sudo docker compose -p pig build app
|
|
|
|
echo "==> Starting the database"
|
|
sudo docker compose -p pig up -d db
|
|
for _ in $(seq 1 60); do
|
|
if sudo docker compose -p pig exec -T db pg_isready -U pig -d pig > /dev/null; then break; fi
|
|
sleep 1
|
|
done
|
|
if ! sudo docker compose -p pig exec -T db pg_isready -U pig -d pig > /dev/null; then
|
|
echo "ERROR: database did not become ready within 60 seconds" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "==> Migrating before the schema-dependent app starts"
|
|
# A release may query a newly introduced table during startup. Running the
|
|
# migration from a one-off container prevents that app from crash-looping
|
|
# before an `exec`-based migration can reach it.
|
|
sudo docker compose -p pig run --rm --no-deps app pnpm exec tsx packages/db/src/migrate.ts
|
|
|
|
echo "==> Starting the app"
|
|
sudo docker compose -p pig up -d app
|
|
|
|
echo "==> Waiting for health"
|
|
for _ in $(seq 1 60); do
|
|
if curl -sf http://127.0.0.1:8920/api/health > /dev/null; then break; fi
|
|
sleep 1
|
|
done
|
|
|
|
echo "==> Verifying"
|
|
if curl -sf http://127.0.0.1:8920/api/health | grep -q '"ok":true'; then
|
|
echo " health ok"
|
|
else
|
|
echo " HEALTH CHECK FAILED"
|
|
sudo docker compose -p pig logs app --tail 40
|
|
exit 1
|
|
fi
|
|
|
|
# Authentication must be enforced. A deploy that accidentally serves the CRM
|
|
# unauthenticated is the one failure worth blocking on.
|
|
CODE=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8920/api/dashboard)
|
|
if [ "$CODE" != "401" ]; then
|
|
echo " UNAUTHENTICATED REQUEST RETURNED $CODE, EXPECTED 401"
|
|
exit 1
|
|
fi
|
|
echo " auth enforced"
|
|
|
|
echo "==> Deployed $AFTER"
|