chore: release v0.10.4 #242
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }} |