# Gluecron self-host — eating our own dog food
> **Status (2026-07-09): the mirror is being CUT.** Gluecron is now
> self-canonical — no GitHub mirror, no fallback. This doc is the background
> design; the live cutover steps (offsite backup, freeze-then-delete the
> GitHub repo, retire `hetzner-deploy.yml`) are in **`docs/CUTOVER_RUNBOOK.md`**.
> The old "keep GitHub as a 7-day/30-day fallback" guidance below is superseded
> by the runbook, which sequences durability (backups) before removal.
> BLOCK W. The moment Gluecron's own source code stops living on GitHub
> and starts living on Gluecron itself.
## Why
Today every push to `ccantynz-alt/Gluecron.com` on GitHub fires the
`hetzner-deploy.yml` workflow, which SSHes into the box and runs a deploy
script. Every red Action means a manual SSH deploy. Every AI safeguard
(auto-merge, AI review, gate enforcement) only applies to **Gluecron-
hosted repos** — so the platform's own source has been the one repo where
none of the safety net runs.
After this block lands and the operator runs the bootstrap, `git push`
fires the deploy directly via Gluecron's own post-receive hook in ~25
seconds. No GitHub Actions in the middle, no SSH retries, no missing AI
safeguards.
The pitch to customers writes itself: *"Gluecron deploys Gluecron with
Gluecron."*
## Pre-flight (do this BEFORE the bootstrap)
1. **You have site-admin access.** Confirm `/admin/ops` renders for your
account.
2. **You have root SSH to the production box.** The bootstrap must run on
the box where `/opt/gluecron` lives, because it writes to
`$GIT_REPOS_PATH` and to `/opt/gluecron/repos/<owner>/<repo>.git/hooks/`.
3. **`DATABASE_URL` points at the live Neon DB.** Source
`/etc/gluecron.env` before running the script: `set -a && source
/etc/gluecron.env && set +a`.
4. **`git` is on PATH and the box can reach `github.com`.** The bootstrap
clones the GitHub source over HTTPS before push-mirroring it into the
local bare repo.
5. **Disk: ~200 MB free** for the temp mirror clone + the bare repo
itself.
6. **No SSH key required from your laptop yet.** Gluecron's git is HTTPS-
only for now; cutover is `https://gluecron.com/<owner>/<repo>.git`.
## 1. Run the bootstrap
On the box, as root:
```bash
cd /opt/gluecron
set -a; source /etc/gluecron.env; set +a
bun run scripts/self-host-bootstrap.ts
```
Optional flags:
```bash
bun run scripts/self-host-bootstrap.ts \
--owner=ccantynz \
--name=Gluecron.com \
--source=https://github.com/ccantynz-alt/Gluecron.com.git \
--dry-run # print what would happen, change nothing
```
What happens (every step prints v/x/!):
1. Looks up the operator — first row in `site_admins`, falling back to
the oldest user (the bootstrap admin).
2. INSERTs `repositories(name='Gluecron.com', ownerId=<operator>,
isPrivate=false, defaultBranch='main', diskPath=<computed>)`. Skips
if a row already exists.
3. `git init --bare /opt/gluecron/repos/ccantynz/Gluecron.com.git` —
skips if the bare repo already exists.
4. `git clone --mirror` the GitHub source into a temp dir, then `git
push --mirror` into the bare repo. Every branch, tag, and commit is
transferred.
5. Writes `hooks/post-receive` on the bare repo: a one-line bash script
that invokes `scripts/self-deploy.sh` when ref is `refs/heads/main`.
6. Prints cutover instructions.
The script is idempotent — re-run anytime. It only fails if a step
truly cannot proceed (no users in DB, mirror push rejected, etc.).
## 2. Cutover
### On your laptop
```bash
cd ~/code/Gluecron.com
git remote set-url origin https://gluecron.com/ccantynz/Gluecron.com.git
```
### On the production box
```bash
cd /opt/gluecron
git remote set-url origin https://gluecron.com/ccantynz/Gluecron.com.git
```
### Flip the self-host env var
Add to `/etc/gluecron.env`:
```
SELF_HOST_REPO=ccantynz/Gluecron.com
```
Then reload systemd so the live process picks it up:
```bash
systemctl restart gluecron
```
## 3. Verify
1. Open `/admin/self-host` in the browser. All three status pills should
be green: **Mirrored**, **Installed**, **Set**.
2. From your laptop, push an empty commit:
```bash
git commit --allow-empty -m "self-host smoke"
git push
```
3. Watch `/admin/deploys` — a new row appears with `source='self-deploy'`
and streams through `git-pull → bun-install → db-migrate → build →
restart-service → healthz → full-smoke`.
4. Tail the log on the box: `tail -f /var/log/gluecron-self-deploy.log`.
Total wall-clock: push to live ≈ 20–30 seconds.
## 4. Rollback plan
If a self-deploy breaks the site:
- **Automatic:** `scripts/self-deploy.sh` runs `post-deploy-smoke.ts`
after every restart. On failure it `git reset --hard <PREV_SHA>` and
restarts. The previous-good SHA is captured *before* `git pull`, so
rollback is reflog-safe.
- **Manual fallback to GitHub:** change the remote back and re-fire the
old workflow.
```bash
# on laptop AND on the box
git remote set-url origin https://github.com/ccantynz-alt/Gluecron.com.git
```
The GitHub mirror remains the canonical history for at least 30 days
after cutover — we don't deprecate `hetzner-deploy.yml` until the
self-host path has run a full week without intervention.
## 5. Deprecating GitHub Actions
After 7 days of green self-deploys:
1. Set the workflow `on:` trigger to `workflow_dispatch:` only (no push
trigger) so it stays available for emergencies but doesn't fire on
every push.
2. After 30 days, remove the `.github/workflows/hetzner-deploy.yml` file
entirely.
Until then it's a free hot-spare deploy path.
## Operator footnotes
- **Log rotation:** `/var/log/gluecron-self-deploy.log` is appended to
forever. Add a `/etc/logrotate.d/gluecron-self-deploy` entry — TODO.
- **Permissions:** `scripts/self-deploy.sh` runs as root (it has to,
because of `systemctl restart`). The bare-repo post-receive hook
inherits whatever user the HTTP receive-pack runs under (usually root
in our setup); SSH receive-pack will eventually need a `git` user with
passwordless `sudo systemctl restart gluecron`.
- **Self-deploy is gated by `SELF_HOST_REPO`.** Customer repos named
`Gluecron.com` are safe — only the exact `<owner>/<repo>` slug in the
env var fires the local deploy.
- **The optional `.gluecron/workflows/deploy.yml`** runs `self-deploy.sh
--inline` on the in-process workflow runner if you want belt-and-
braces. The post-receive hook is the primary path.
|