Files
autofirmer-expanded/deploy/bootstrap-tls.sh
T
Brandon LiandClaude Opus 5 d1e8668440 Let the extension update itself, and host the dashboard
Chrome has no update channel for an unpacked extension, so every pull left one
manual step: click reload on its card. The dashboard now publishes the version
its own checkout ships at /api/autobuyer/status, and the extension reloads
itself when it is behind — chrome.runtime.reload() re-reads the folder from
disk, which the update check has already pulled into. It only ever runs code
that is on disk, so a reload that changes nothing is reported and waited out
rather than repeated; a dashboard that is behind is ignored.

Also adds the deploy stack for running the dashboard on the droplet, next to
the autotrader one: it joins that nginx's network and renders a server block
into its conf.d rather than standing up a second ingress. AUTOFIRMER_DB moves
the SQLite file onto a volume, since the working directory in a container is
replaced on every deploy.

And waitFor gained `hidden`: an absent gate matched on DOM presence only, so a
modal that closes by going display:none could never satisfy it. The extension
grew a visibility probe for this — locate cannot answer it, because absent and
hidden are both errors there and a wait gate has to tell them apart.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 12:30:59 -05:00

52 lines
2.4 KiB
Bash
Executable File

#!/usr/bin/env bash
# First run only: get a certificate for FIRMER_HOST, then hand off to deploy.sh.
#
# This uses the autotrader stack's certbot container and its Let's Encrypt
# account, so it agrees to the Subscriber Agreement the same way that stack's
# own bootstrap did (certbot --agree-tos):
# https://letsencrypt.org/repository/
set -euo pipefail
cd "$(dirname "$0")"
log() { printf '\n\033[1;34m==> %s\033[0m\n' "$1"; }
fail() { printf '\n\033[1;31mERROR: %s\033[0m\n' "$1" >&2; exit 1; }
[ -f .env ] || fail "no .env — copy .env.example to .env and fill it in"
set -a; . ./.env; set +a
: "${FIRMER_HOST:?set FIRMER_HOST in .env}"
SHARED_STACK="${SHARED_STACK:-/opt/autotrader/deploy}"
# ACME_EMAIL is not duplicated here: it belongs to the account that already
# holds the other certificates on this box.
[ -f "$SHARED_STACK/.env" ] || fail "no .env under $SHARED_STACK"
ACME_EMAIL=$(grep -E '^ACME_EMAIL=' "$SHARED_STACK/.env" | cut -d= -f2-)
[ -n "$ACME_EMAIL" ] || fail "ACME_EMAIL is not set in $SHARED_STACK/.env"
# Let's Encrypt rate-limits failures, so check DNS before spending an attempt.
log "Checking DNS"
ip=$(curl -fsS --max-time 5 http://169.254.169.254/metadata/v1/interfaces/public/0/ipv4/address 2>/dev/null \
|| curl -fsS --max-time 5 https://api.ipify.org 2>/dev/null) \
|| fail "could not determine this droplet's public IP"
got=$(getent ahostsv4 "$FIRMER_HOST" 2>/dev/null | awk 'NR==1{print $1}') || true
echo " droplet: $ip"
echo " $FIRMER_HOST -> ${got:-<unresolved>}"
[ "$got" = "$ip" ] || fail "$FIRMER_HOST does not resolve to $ip — fix DNS and wait for it to propagate"
# The challenge is served over plain HTTP by the shared nginx, which needs a
# server block for this name before it will answer for it.
log "Rendering the HTTP-only config so the challenge can be served"
envsubst '${FIRMER_HOST}' < nginx/firmer-acme.conf.template > "$SHARED_STACK/nginx/conf.d/firmer.conf"
docker exec nginx nginx -t
docker exec nginx nginx -s reload
log "Requesting a certificate for $FIRMER_HOST"
docker compose --project-directory "$SHARED_STACK" -f "$SHARED_STACK/docker-compose.yml" \
run --rm --entrypoint certbot certbot \
certonly --webroot -w /var/www/certbot \
--email "$ACME_EMAIL" --agree-tos --no-eff-email \
--non-interactive -d "$FIRMER_HOST" \
|| fail "certbot failed — check DNS and that port 80 reaches this droplet"
log "Switching to the TLS config"
exec ./deploy.sh