Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ jobs:
- uses: actions/checkout@v7
- uses: jdx/mise-action@v4
- run: pnpm install --frozen-lockfile
- run: mise //:check
- run: mise //:verify
- run: mise :biome
- run: mise :verify
- run: mise //...:build

changeset:
Expand Down
8 changes: 4 additions & 4 deletions hk.pkl
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@ hooks {
fix = true
stash = true
steps {
["check"] {
["biome"] {
glob = List("**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx", "**/*.css", "**/*.json")
check = "mise //:check {{ files }}"
fix = "mise //:check --write {{ files }}"
check = "mise :biome {{ files }}"
fix = "mise :biome --write {{ files }}"
}
}
}
["pre-push"] {
steps {
["verify"] {
check = "mise //:verify"
check = "mise :verify"
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ pnpm = "11"
[hooks]
postinstall = "hk install --mise"

[tasks.check]
[tasks.biome]
description = "Lint and format with Biome"
usage = '''
flag "--write" help="Apply safe fixes and formatting in place"
Expand Down
80 changes: 80 additions & 0 deletions scripts/deploy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/usr/bin/env bash
#
# Plexus deploy verb — PLEXUS.md §5. A stateless procedure, not a system:
#
# ssh → docker compose pull → mise migrate → docker compose up -d
# → poll /healthz → on failure: re-up the previous image, exit non-zero
#
# State lives in git (compose.yml + the app's mise.toml, placed on the host by the
# tenant's Ansible) and in the registry (the image). "Which version is live" is the
# running container's image, queried from `docker` (reality) — never a file we own.
# No persistent state, no daemon, no UI, no reconciliation loop.
#
# Degradation test — hand-runnable with no extra machinery:
# ./deploy.sh deploy@1.2.3.4 plexus-website ghcr.io/org/plexus-website:<sha>
#
set -euo pipefail

HOST="${1:?usage: deploy.sh <ssh-host> <app> <image-ref>}"
APP="${2:?usage: deploy.sh <ssh-host> <app> <image-ref>}"
IMAGE="${3:?usage: deploy.sh <ssh-host> <app> <image-ref>}"

# Overridable knobs (sane defaults for the stateless-app profile).
APP_DIR="${PLEXUS_APP_DIR:-/opt/plexus/$APP}"
HEALTH_URL="${PLEXUS_HEALTH_URL:-http://127.0.0.1:3000/healthz}"
RETRIES="${PLEXUS_HEALTH_RETRIES:-30}"

echo "→ deploying $IMAGE"
echo " host: $HOST"
echo " dir: $APP_DIR"
echo " health: $HEALTH_URL"

# Everything below runs on the host. Args are passed positionally (no fragile
# remote-env forwarding); the heredoc is quoted so it is not expanded locally.
ssh -o StrictHostKeyChecking=accept-new "$HOST" bash -seuo pipefail -- \
"$APP_DIR" "$IMAGE" "$HEALTH_URL" "$RETRIES" <<'REMOTE'
APP_DIR="$1"; IMAGE="$2"; HEALTH_URL="$3"; RETRIES="$4"
cd "$APP_DIR"

# Reality is the source of truth: read the currently-live image for rollback.
PREV_IMAGE=""
CID="$(docker compose ps -q web 2>/dev/null || true)"
[ -n "$CID" ] && PREV_IMAGE="$(docker inspect --format '{{.Config.Image}}' "$CID" 2>/dev/null || true)"
echo " previous: ${PREV_IMAGE:-<none>}"

up() { # $1 = image ref
echo "IMAGE=$1" > .env # reproducibility cache for a manual `docker compose up`
IMAGE="$1" docker compose pull web
IMAGE="$1" docker compose up -d web
}

healthy() {
for _ in $(seq 1 "$RETRIES"); do
curl -fsS -o /dev/null "$HEALTH_URL" && return 0
sleep 2
done
return 1
}

# Pull → migrate (idempotent; a no-op for stateless apps) → up.
echo "IMAGE=$IMAGE" > .env
IMAGE="$IMAGE" docker compose pull web
# Bare `mise migrate` (not `:migrate`): the app dir on the host holds a
# standalone mise.toml, and monorepo path syntax requires a monorepo root.
mise trust . >/dev/null 2>&1 || true
mise migrate
IMAGE="$IMAGE" docker compose up -d web

if healthy; then
echo "✓ $IMAGE is live and healthy"
exit 0
fi

echo "✗ healthcheck failed after $((RETRIES * 2))s"
if [ -n "$PREV_IMAGE" ] && [ "$PREV_IMAGE" != "$IMAGE" ]; then
echo "↩ rolling back to $PREV_IMAGE"
up "$PREV_IMAGE"
if healthy; then echo "✓ rolled back to $PREV_IMAGE"; else echo "✗ rollback also unhealthy — manual intervention needed"; fi
fi
exit 1
REMOTE