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
74 changes: 74 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ on:
push:
branches: [main, vue]
pull_request:
# Manual runs deploy whatever branch they are dispatched from — this is how a staging
# deploy of `vue` works before it merges to main.
workflow_dispatch:

# Replaces .circleci/config.yml (Node 8) and .travis.yml (Node 6), both of which
# had been dead for years. See docs/UPGRADE-PLAN.md.
Expand Down Expand Up @@ -35,3 +38,74 @@ jobs:
name: static-site
path: .output/public
retention-days: 7
# Since v4.4 hidden files are excluded by default, which would silently drop
# .htaccess — the file the whole Apache setup depends on. The verify step in
# the deploy job would catch it, but better it never breaks.
include-hidden-files: true

# ---------------------------------------------------------------------------
# Deploy to the Virtualmin VPS (docs/UPGRADE-PLAN.md §5a).
#
# Builds happen in CI, never on the VPS, so a broken build cannot take the site
# down — the deploy only ever ships an artifact that already passed typecheck,
# tests and generate.
#
# Runs on push to main and on manual dispatch (any branch, for staging). Until the
# repository has deploy secrets configured, the job short-circuits with a notice
# instead of failing, so CI stays green while hosting is being set up.
#
# Required repository secrets:
# DEPLOY_SSH_KEY private key for a dedicated deploy keypair; its public half
# goes in ~/.ssh/authorized_keys of the Virtualmin domain user
# DEPLOY_HOST the VPS hostname
# DEPLOY_USER the Virtualmin domain user (NOT root) — files land owned by
# the right user with no chown gymnastics
# DEPLOY_PATH document root, e.g. /home/smallrobot/public_html
# ---------------------------------------------------------------------------
deploy:
needs: build
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main')
runs-on: ubuntu-latest
# `secrets` is not usable in a job-level `if`, so expose presence via env instead.
env:
DEPLOY_CONFIGURED: ${{ secrets.DEPLOY_SSH_KEY != '' && secrets.DEPLOY_HOST != '' }}
steps:
- name: Check deploy secrets
if: env.DEPLOY_CONFIGURED != 'true'
run: echo "::notice::Deploy secrets not configured — skipping deploy. Set DEPLOY_SSH_KEY, DEPLOY_HOST, DEPLOY_USER, DEPLOY_PATH to enable."

- uses: actions/download-artifact@v4
if: env.DEPLOY_CONFIGURED == 'true'
with:
name: static-site
path: site

- name: Set up SSH
if: env.DEPLOY_CONFIGURED == 'true'
run: |
mkdir -p ~/.ssh
printf '%s\n' "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
# Fresh runner every time, so pin the host key on first contact. For a
# stricter setup, add a DEPLOY_KNOWN_HOSTS secret and write it here instead.
ssh-keyscan -H "${{ secrets.DEPLOY_HOST }}" >> ~/.ssh/known_hosts 2>/dev/null

- name: Rsync to VPS
if: env.DEPLOY_CONFIGURED == 'true'
run: |
# --delete keeps the docroot an exact mirror of the build: files removed from
# the site (old hashed bundles, deleted pages) disappear rather than rot.
# Not atomic — a visitor mid-deploy could see a torn state for a second or
# two. Acceptable here; the upgrade path is rsync to a timestamped dir and a
# symlink swap, which needs the vhost docroot pointed at the symlink first.
rsync -az --delete \
-e "ssh -i ~/.ssh/deploy_key" \
site/ "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:${{ secrets.DEPLOY_PATH }}/"

- name: Verify deployment
if: env.DEPLOY_CONFIGURED == 'true'
run: |
# The htaccess ships with the site; if it made it, everything did.
ssh -i ~/.ssh/deploy_key "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" \
"test -f '${{ secrets.DEPLOY_PATH }}/.htaccess' && test -f '${{ secrets.DEPLOY_PATH }}/index.html'"
echo "::notice::Deployed to ${{ secrets.DEPLOY_HOST }}:${{ secrets.DEPLOY_PATH }}"
30 changes: 28 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,34 @@ production (`npm run diff:prod`).
cutover; see [docs/UPGRADE-PLAN.md](docs/UPGRADE-PLAN.md) §5a.
2. **A visual pass** — text parity is verified, layout is not.
3. **Delete `legacy/`** once 1 and 2 are settled.
4. **Hosting** — Virtualmin vhost, `.htaccess` SPA fallback, TLS, rsync deploy, and the
three `smallrobot.org` → `smallrobot.co` 301s.
4. **Hosting** — Virtualmin vhost + TLS + DNS. The Apache config (`public/.htaccess`,
tested against real httpd), the service-worker kill switch (`public/sw.js`), the
`smallrobot.org` 301s, and the CI deploy job are all in place; what remains is
server-side setup and repository secrets (see below).

## Repository

Canonical repo is `bmx269/smallrobot`. The old `smallrobotco/smallrobot` org repo is
retired — Netlify watches it, which is exactly why nothing gets pushed there during the
migration.

## Deployment

CI deploys `.output/public` to the VPS by rsync over SSH — on every push to `main`, and
manually via *Actions → CI → Run workflow* from any branch (that is how `vue` gets a
staging deploy before merging). Until the secrets below exist, the deploy job skips
itself with a notice rather than failing.

| Secret | Value |
|---|---|
| `DEPLOY_SSH_KEY` | private half of a dedicated deploy keypair; public half goes in the domain user's `~/.ssh/authorized_keys` |
| `DEPLOY_HOST` | VPS hostname |
| `DEPLOY_USER` | the Virtualmin domain user (not root) |
| `DEPLOY_PATH` | document root, e.g. `/home/smallrobot/public_html` |

Server prerequisites, one-time (see docs/UPGRADE-PLAN.md §5a): Virtualmin virtual
server for `smallrobot.co` with `smallrobot.org` as an alias, Let's Encrypt for both,
and `AllowOverride All` on the docroot so `.htaccess` is honoured.

## Requirements

Expand Down
Loading