Skip to content

chore: release v0.10.4 #242

chore: release v0.10.4

chore: release v0.10.4 #242

name: Deploy
on:
push:
branches: [main]
paths:
- 'packages/registry/src/**'
- 'packages/registry/dashboard/**'
- 'packages/registry/site/**'
- 'public/install.sh'
- 'docs/**'
- '.config/bunpress.ts'
# Re-deploy when the deploy workflow itself changes.
- '.github/workflows/deploy-registry.yml'
# Root deps: stx + crosswind transitively render the site, so any
# dependency bump can change what gets served. Re-deploy on lockfile
# changes too — see CLAUDE.md "Site CSS pinning" for the v0.2.0 incident.
- 'package.json'
- 'bun.lock'
workflow_dispatch:
concurrency:
group: deploy-registry
cancel-in-progress: false
permissions:
contents: read
jobs:
deploy:
name: hetzner
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- name: Deploy to registry server
env:
SSH_PRIVATE_KEY: ${{ secrets.REGISTRY_SSH_KEY }}
# Target the live box by its DNS name. The old REGISTRY_HOST secret
# still points at the decommissioned EC2 IP, so we deliberately do NOT
# use it — registry.pantry.dev always resolves to the current Hetzner
# host. (Public DNS, not a secret.)
REGISTRY_HOST: registry.pantry.dev
run: |
# Create the key file with a 0600 umask before writing so it is
# never readable by other users for any window of time, and use
# stdin + heredoc rather than passing the secret as an argument.
mkdir -p ~/.ssh
(umask 077 && cat > ~/.ssh/id_rsa <<< "$SSH_PRIVATE_KEY")
ssh-keyscan -H "$REGISTRY_HOST" >> ~/.ssh/known_hosts 2>/dev/null
# Hetzner Cloud images log in as root (the old EC2 host used ec2-user).
# Running as root means no sudo is needed (and minimal images lack it).
ssh -o StrictHostKeyChecking=no "root@${REGISTRY_HOST}" << 'EOF'
set -e
cd /opt/pantry-registry/repo
git config --global --add safe.directory /opt/pantry-registry/repo
git checkout -- bun.lock 2>/dev/null || true
git fetch origin main
git reset --hard origin/main
/root/.bun/bin/bun install
# Build bunpress docs (served at /docs). Project-local bunpress finds
# crosswind from the root node_modules.
/root/.bun/bin/bun ./node_modules/@stacksjs/bunpress/dist/bin/cli.js build docs || echo "::warning::Docs build failed — check bunpress/crosswind"
# Stop the old standalone site service if it lingers (merged into registry).
systemctl stop pantry-site 2>/dev/null || true
systemctl disable pantry-site 2>/dev/null || true
systemctl restart pantry-registry
sleep 2
# The registry listens on :3001 (the :3000 buddy-serve is the stacksjs
# site on this shared box).
curl -sf http://localhost:3001/health || echo "Health check failed"
# Re-assert our rpx gateway routes. The gateway (/etc/rpx/gateway.ts)
# is regenerated by ts-cloud on buddy deploy and does NOT include our
# domains, so without this a buddy deploy silently drops them (-> the
# default vhost's wrong cert + "No proxy configured" 404). All three
# proxy to the registry on :3001 — it serves the API, the install
# script at / (curl https://pantry.dev | bash), /packages and /docs.
# Idempotent. TLS certs live in /etc/rpx/certs (acme.sh DNS-01 via
# Porkbun, auto-renewed daily with an rpx reload hook).
RPX_CHANGED=0
for D in registry.pantry.dev pantry.dev www.pantry.dev; do
if [ -f /etc/rpx/gateway.ts ] && ! grep -qF "\"to\": \"$D\"" /etc/rpx/gateway.ts; then
perl -0pi -e "s/(\"proxies\": \[\n)/\$1 {\n \"to\": \"$D\",\n \"from\": \"localhost:3001\",\n \"id\": \"$D\"\n },\n/" /etc/rpx/gateway.ts \
&& { RPX_CHANGED=1; echo "re-added rpx route: $D"; }
fi
done
[ "$RPX_CHANGED" = 1 ] && systemctl restart rpx-gateway || true
EOF
- name: Verify deployment
run: |
sleep 5
HTTP_CODE=$(curl -sS -o /dev/null -w '%{http_code}' \
'https://registry.pantry.dev/health' \
--max-time 15 2>/dev/null) || true
echo "Health check: HTTP $HTTP_CODE"
if [ "$HTTP_CODE" != "200" ]; then
echo "::error::Health check returned $HTTP_CODE — deployment may have failed"
exit 1
fi
ACTIONS_STATUS=$(curl -fsSL \
'https://registry.pantry.dev/api/github-actions-status' \
--max-time 15)
ACTIONS_ERROR=$(echo "$ACTIONS_STATUS" | jq -r '.error // ""')
ACTIONS_RECENT=$(echo "$ACTIONS_STATUS" | jq -r '.recent | length')
ACTIONS_RUNNING=$(echo "$ACTIONS_STATUS" | jq -r '.running')
ACTIONS_QUEUED=$(echo "$ACTIONS_STATUS" | jq -r '.queued')
echo "GitHub Actions status: running=$ACTIONS_RUNNING queued=$ACTIONS_QUEUED recent=$ACTIONS_RECENT"
if [ -n "$ACTIONS_ERROR" ]; then
echo "::error::GitHub Actions status endpoint returned error: $ACTIONS_ERROR"
exit 1
fi
if [ "$ACTIONS_RECENT" -eq 0 ]; then
echo "::error::GitHub Actions status endpoint returned no recent runs"
exit 1
fi
notify:
name: discord-notification
needs: deploy
if: always()
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: ./.github/actions/discord-notify
with:
title: "Registry Deploy — ${{ needs.deploy.result == 'success' && 'Deployed' || needs.deploy.result == 'cancelled' && 'Cancelled' || 'Failed' }}"
description: "Registry server updated"
status: ${{ needs.deploy.result == 'success' && 'success' || needs.deploy.result == 'cancelled' && 'warning' || 'failure' }}
fields: |
[
{"name": "Branch", "value": "${{ github.ref_name }}", "inline": true},
{"name": "Actor", "value": "${{ github.actor }}", "inline": true}
]
webhook_url: ${{ secrets.DISCORD_WEBHOOK_URL }}