CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history
SELF_HOST.md
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| f2c00b4 | 1 | # Gluecron self-host — eating our own dog food |
| 2 | ||
| 5472bea | 3 | > **Status (2026-07-09): the mirror is being CUT.** Gluecron is now |
| 4 | > self-canonical — no GitHub mirror, no fallback. This doc is the background | |
| 5 | > design; the live cutover steps (offsite backup, freeze-then-delete the | |
| 6 | > GitHub repo, retire `hetzner-deploy.yml`) are in **`docs/CUTOVER_RUNBOOK.md`**. | |
| 7 | > The old "keep GitHub as a 7-day/30-day fallback" guidance below is superseded | |
| 8 | > by the runbook, which sequences durability (backups) before removal. | |
| 9 | ||
| f2c00b4 | 10 | > BLOCK W. The moment Gluecron's own source code stops living on GitHub |
| 11 | > and starts living on Gluecron itself. | |
| 12 | ||
| 13 | ## Why | |
| 14 | ||
| 15 | Today every push to `ccantynz-alt/Gluecron.com` on GitHub fires the | |
| 16 | `hetzner-deploy.yml` workflow, which SSHes into the box and runs a deploy | |
| 17 | script. Every red Action means a manual SSH deploy. Every AI safeguard | |
| 18 | (auto-merge, AI review, gate enforcement) only applies to **Gluecron- | |
| 19 | hosted repos** — so the platform's own source has been the one repo where | |
| 20 | none of the safety net runs. | |
| 21 | ||
| 22 | After this block lands and the operator runs the bootstrap, `git push` | |
| 23 | fires the deploy directly via Gluecron's own post-receive hook in ~25 | |
| 24 | seconds. No GitHub Actions in the middle, no SSH retries, no missing AI | |
| 25 | safeguards. | |
| 26 | ||
| 27 | The pitch to customers writes itself: *"Gluecron deploys Gluecron with | |
| 28 | Gluecron."* | |
| 29 | ||
| 30 | ## Pre-flight (do this BEFORE the bootstrap) | |
| 31 | ||
| 32 | 1. **You have site-admin access.** Confirm `/admin/ops` renders for your | |
| 33 | account. | |
| 34 | 2. **You have root SSH to the production box.** The bootstrap must run on | |
| 35 | the box where `/opt/gluecron` lives, because it writes to | |
| 36 | `$GIT_REPOS_PATH` and to `/opt/gluecron/repos/<owner>/<repo>.git/hooks/`. | |
| 37 | 3. **`DATABASE_URL` points at the live Neon DB.** Source | |
| 38 | `/etc/gluecron.env` before running the script: `set -a && source | |
| 39 | /etc/gluecron.env && set +a`. | |
| 40 | 4. **`git` is on PATH and the box can reach `github.com`.** The bootstrap | |
| 41 | clones the GitHub source over HTTPS before push-mirroring it into the | |
| 42 | local bare repo. | |
| 43 | 5. **Disk: ~200 MB free** for the temp mirror clone + the bare repo | |
| 44 | itself. | |
| 45 | 6. **No SSH key required from your laptop yet.** Gluecron's git is HTTPS- | |
| 46 | only for now; cutover is `https://gluecron.com/<owner>/<repo>.git`. | |
| 47 | ||
| 48 | ## 1. Run the bootstrap | |
| 49 | ||
| 50 | On the box, as root: | |
| 51 | ||
| 52 | ```bash | |
| 53 | cd /opt/gluecron | |
| 54 | set -a; source /etc/gluecron.env; set +a | |
| 55 | bun run scripts/self-host-bootstrap.ts | |
| 56 | ``` | |
| 57 | ||
| 58 | Optional flags: | |
| 59 | ||
| 60 | ```bash | |
| 61 | bun run scripts/self-host-bootstrap.ts \ | |
| 62 | --owner=ccantynz \ | |
| 63 | --name=Gluecron.com \ | |
| 64 | --source=https://github.com/ccantynz-alt/Gluecron.com.git \ | |
| 65 | --dry-run # print what would happen, change nothing | |
| 66 | ``` | |
| 67 | ||
| 68 | What happens (every step prints v/x/!): | |
| 69 | ||
| 70 | 1. Looks up the operator — first row in `site_admins`, falling back to | |
| 71 | the oldest user (the bootstrap admin). | |
| 72 | 2. INSERTs `repositories(name='Gluecron.com', ownerId=<operator>, | |
| 73 | isPrivate=false, defaultBranch='main', diskPath=<computed>)`. Skips | |
| 74 | if a row already exists. | |
| 75 | 3. `git init --bare /opt/gluecron/repos/ccantynz/Gluecron.com.git` — | |
| 76 | skips if the bare repo already exists. | |
| 77 | 4. `git clone --mirror` the GitHub source into a temp dir, then `git | |
| 78 | push --mirror` into the bare repo. Every branch, tag, and commit is | |
| 79 | transferred. | |
| 80 | 5. Writes `hooks/post-receive` on the bare repo: a one-line bash script | |
| 81 | that invokes `scripts/self-deploy.sh` when ref is `refs/heads/main`. | |
| 82 | 6. Prints cutover instructions. | |
| 83 | ||
| 84 | The script is idempotent — re-run anytime. It only fails if a step | |
| 85 | truly cannot proceed (no users in DB, mirror push rejected, etc.). | |
| 86 | ||
| 87 | ## 2. Cutover | |
| 88 | ||
| 89 | ### On your laptop | |
| 90 | ||
| 91 | ```bash | |
| 92 | cd ~/code/Gluecron.com | |
| 93 | git remote set-url origin https://gluecron.com/ccantynz/Gluecron.com.git | |
| 94 | ``` | |
| 95 | ||
| 96 | ### On the production box | |
| 97 | ||
| 98 | ```bash | |
| 99 | cd /opt/gluecron | |
| 100 | git remote set-url origin https://gluecron.com/ccantynz/Gluecron.com.git | |
| 101 | ``` | |
| 102 | ||
| 103 | ### Flip the self-host env var | |
| 104 | ||
| 105 | Add to `/etc/gluecron.env`: | |
| 106 | ||
| 107 | ``` | |
| 108 | SELF_HOST_REPO=ccantynz/Gluecron.com | |
| 109 | ``` | |
| 110 | ||
| 111 | Then reload systemd so the live process picks it up: | |
| 112 | ||
| 113 | ```bash | |
| 114 | systemctl restart gluecron | |
| 115 | ``` | |
| 116 | ||
| 117 | ## 3. Verify | |
| 118 | ||
| 119 | 1. Open `/admin/self-host` in the browser. All three status pills should | |
| 120 | be green: **Mirrored**, **Installed**, **Set**. | |
| 121 | 2. From your laptop, push an empty commit: | |
| 122 | ||
| 123 | ```bash | |
| 124 | git commit --allow-empty -m "self-host smoke" | |
| 125 | git push | |
| 126 | ``` | |
| 127 | ||
| 128 | 3. Watch `/admin/deploys` — a new row appears with `source='self-deploy'` | |
| 129 | and streams through `git-pull → bun-install → db-migrate → build → | |
| 130 | restart-service → healthz → full-smoke`. | |
| 131 | 4. Tail the log on the box: `tail -f /var/log/gluecron-self-deploy.log`. | |
| 132 | ||
| 133 | Total wall-clock: push to live ≈ 20–30 seconds. | |
| 134 | ||
| 135 | ## 4. Rollback plan | |
| 136 | ||
| 137 | If a self-deploy breaks the site: | |
| 138 | ||
| 139 | - **Automatic:** `scripts/self-deploy.sh` runs `post-deploy-smoke.ts` | |
| 140 | after every restart. On failure it `git reset --hard <PREV_SHA>` and | |
| 141 | restarts. The previous-good SHA is captured *before* `git pull`, so | |
| 142 | rollback is reflog-safe. | |
| 143 | - **Manual fallback to GitHub:** change the remote back and re-fire the | |
| 144 | old workflow. | |
| 145 | ||
| 146 | ```bash | |
| 147 | # on laptop AND on the box | |
| 148 | git remote set-url origin https://github.com/ccantynz-alt/Gluecron.com.git | |
| 149 | ``` | |
| 150 | ||
| 151 | The GitHub mirror remains the canonical history for at least 30 days | |
| 152 | after cutover — we don't deprecate `hetzner-deploy.yml` until the | |
| 153 | self-host path has run a full week without intervention. | |
| 154 | ||
| 155 | ## 5. Deprecating GitHub Actions | |
| 156 | ||
| 157 | After 7 days of green self-deploys: | |
| 158 | ||
| 159 | 1. Set the workflow `on:` trigger to `workflow_dispatch:` only (no push | |
| 160 | trigger) so it stays available for emergencies but doesn't fire on | |
| 161 | every push. | |
| 162 | 2. After 30 days, remove the `.github/workflows/hetzner-deploy.yml` file | |
| 163 | entirely. | |
| 164 | ||
| 165 | Until then it's a free hot-spare deploy path. | |
| 166 | ||
| 167 | ## Operator footnotes | |
| 168 | ||
| 169 | - **Log rotation:** `/var/log/gluecron-self-deploy.log` is appended to | |
| 170 | forever. Add a `/etc/logrotate.d/gluecron-self-deploy` entry — TODO. | |
| 171 | - **Permissions:** `scripts/self-deploy.sh` runs as root (it has to, | |
| 172 | because of `systemctl restart`). The bare-repo post-receive hook | |
| 173 | inherits whatever user the HTTP receive-pack runs under (usually root | |
| 174 | in our setup); SSH receive-pack will eventually need a `git` user with | |
| 175 | passwordless `sudo systemctl restart gluecron`. | |
| 176 | - **Self-deploy is gated by `SELF_HOST_REPO`.** Customer repos named | |
| 177 | `Gluecron.com` are safe — only the exact `<owner>/<repo>` slug in the | |
| 178 | env var fires the local deploy. | |
| 179 | - **The optional `.gluecron/workflows/deploy.yml`** runs `self-deploy.sh | |
| 180 | --inline` on the in-process workflow runner if you want belt-and- | |
| 181 | braces. The post-receive hook is the primary path. |