blumeops/docs/how-to/plans/migrate-forgejo-from-brew.md
Erich Blume 538a8cf6c1 Rename HTTPS forge.ops.eblu.me → forge.eblu.me across codebase
Update all HTTPS references to use the new public domain. This
touches workflows, ArgoCD manifests, Ansible, mise-tasks, NixOS
config, and documentation (~29 files).

Deliberately kept as forge.ops.eblu.me:
- SSH repoURLs in argocd/apps/ (SSH stays tailnet-only)
- containers/*/Dockerfile and *.nix (internal CI efficiency)
- Caddy services table in routing.md
- Internal URL references in forgejo.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 07:57:48 -08:00

299 lines
9 KiB
Markdown

---
title: "Plan: Migrate Forgejo from Brew to Source Build"
modified: 2026-02-10
tags:
- how-to
- plans
- forgejo
---
# Plan: Migrate Forgejo from Brew to Source Build
> **Status:** Planned (not yet executed)
## Background
Forgejo was force-upgraded from v13 to v14 by `brew upgrade`, breaking version control. To prevent uncontrolled upgrades and align with the established pattern for other native services (zot, caddy, alloy), we are transitioning Forgejo from Homebrew to a source-built binary managed by a LaunchAgent.
### Why Source Build?
- **Version pinning** — upgrade on our schedule by checking out specific tags
- **Consistency** — matches [[zot]], [[caddy]], and [[alloy]] deployment patterns
- **Control** — build flags, patches, and dependencies are explicit
## Source Remote
Use **Codeberg upstream** as the primary clone source to avoid a circular dependency (Forgejo hosting its own source):
```
https://codeberg.org/forgejo/forgejo.git
```
Add the forge mirror as a secondary remote for convenience and backup:
```
https://forge.eblu.me/mirrors/forgejo.git
```
## One-Time Migration Steps
These steps are performed manually on indri **before** running Ansible.
### 1. Clone Forgejo from Codeberg
```fish
ssh indri 'git clone https://codeberg.org/forgejo/forgejo.git ~/code/3rd/forgejo'
```
### 2. Add Forge Mirror as Secondary Remote
```fish
ssh indri 'cd ~/code/3rd/forgejo && git remote add forge https://forge.eblu.me/mirrors/forgejo.git'
```
### 3. Check Out the Desired Version Tag
```fish
ssh indri 'cd ~/code/3rd/forgejo && git checkout v14.0.1'
```
### 4. Create a Local Deployment Branch
Create a local-only `indri-deployment` branch to track the deployed version. Rebase this branch when upgrading to new tags:
```fish
ssh indri 'cd ~/code/3rd/forgejo && git checkout -b indri-deployment'
```
### 5. Set Up Build Dependencies via Mise
Forgejo requires Go 1.24+ and Node 20+:
```fish
ssh indri 'cd ~/code/3rd/forgejo && mise use go@1.24 node@20'
```
### 6. Build the Binary
```fish
ssh indri 'cd ~/code/3rd/forgejo && TAGS="bindata timedzdata sqlite sqlite_unlock_notify" mise x -- make build'
```
This produces `./forgejo` in the repo root.
### 7. Stop Brew Forgejo
```fish
ssh indri 'brew services stop forgejo'
```
### 8. Copy Data to New Location
```fish
ssh indri 'sudo cp -a /opt/homebrew/var/forgejo ~/forgejo'
```
### 9. Fix Ownership
```fish
ssh indri 'sudo chown -R erichblume:staff ~/forgejo'
```
### 10. Run Ansible to Deploy New Config + LaunchAgent
```fish
mise run provision-indri -- --tags forgejo
```
### 11. Verify Service Health
See the verification checklist below.
### 12. Uninstall Brew Forgejo
Only after verifying everything works:
```fish
ssh indri 'brew uninstall forgejo'
```
## Ansible Role Changes
The following changes to `ansible/roles/forgejo/` should be made in the execution session.
### `defaults/main.yml`
Update paths and add new variables to match the zot pattern (`ansible/roles/zot/defaults/main.yml`):
```yaml
# Source build paths
forgejo_repo_dir: /Users/erichblume/code/3rd/forgejo
forgejo_binary: "{{ forgejo_repo_dir }}/forgejo"
# Data paths (migrated from brew)
forgejo_work_path: /Users/erichblume/forgejo
forgejo_config_path: "{{ forgejo_work_path }}/custom/conf/app.ini"
forgejo_data_path: "{{ forgejo_work_path }}/data"
forgejo_log_path: "{{ forgejo_work_path }}/log"
forgejo_log_dir: /Users/erichblume/Library/Logs
# RUN_USER changes from 'forgejo' to 'erichblume' (LaunchAgent user)
forgejo_run_user: erichblume
```
### `tasks/main.yml`
Replace brew install/start with binary-check + LaunchAgent pattern (matching `ansible/roles/zot/tasks/main.yml`):
```yaml
---
# Forgejo role — source-built binary with LaunchAgent
#
# ONE-TIME SETUP (before running ansible):
#
# 1. Clone forgejo from codeberg (avoid circular dependency):
# ssh indri 'git clone https://codeberg.org/forgejo/forgejo.git ~/code/3rd/forgejo'
#
# 2. Add forge mirror as secondary remote:
# ssh indri 'cd ~/code/3rd/forgejo && git remote add forge https://forge.eblu.me/mirrors/forgejo.git'
#
# 3. Set up Go and Node via mise:
# ssh indri 'cd ~/code/3rd/forgejo && mise use go@1.24 node@20'
#
# 4. Build:
# ssh indri 'cd ~/code/3rd/forgejo && TAGS="bindata timedzdata sqlite sqlite_unlock_notify" mise x -- make build'
#
# 5. Run ansible to deploy config and LaunchAgent
- name: Verify forgejo binary exists
ansible.builtin.stat:
path: "{{ forgejo_binary }}"
register: forgejo_binary_stat
- name: Fail if forgejo binary not found
ansible.builtin.fail:
msg: |
Forgejo binary not found at {{ forgejo_binary }}.
Please build from source first:
ssh indri 'cd ~/code/3rd/forgejo && TAGS="bindata timedzdata sqlite sqlite_unlock_notify" mise x -- make build'
when: not forgejo_binary_stat.stat.exists
- name: Ensure forgejo config directory exists
ansible.builtin.file:
path: "{{ forgejo_work_path }}/custom/conf"
state: directory
mode: '0755'
- name: Deploy forgejo config
ansible.builtin.template:
src: app.ini.j2
dest: "{{ forgejo_config_path }}"
mode: '0600'
notify: Restart forgejo
- name: Deploy forgejo LaunchAgent plist
ansible.builtin.template:
src: forgejo.plist.j2
dest: ~/Library/LaunchAgents/mcquack.eblume.forgejo.plist
mode: '0644'
notify: Restart forgejo
- name: Check if forgejo LaunchAgent is loaded
ansible.builtin.command: launchctl list mcquack.eblume.forgejo
register: forgejo_launchctl_check
changed_when: false
failed_when: false
- name: Load forgejo LaunchAgent if not loaded
ansible.builtin.command: launchctl load ~/Library/LaunchAgents/mcquack.eblume.forgejo.plist
when: forgejo_launchctl_check.rc != 0
changed_when: true
failed_when: false
```
### `handlers/main.yml`
Replace `brew services restart` with `launchctl unload/load` (matching `ansible/roles/zot/handlers/main.yml`):
```yaml
---
- name: Restart forgejo
ansible.builtin.shell: |
launchctl unload ~/Library/LaunchAgents/mcquack.eblume.forgejo.plist 2>/dev/null || true
launchctl load ~/Library/LaunchAgents/mcquack.eblume.forgejo.plist
changed_when: true
```
### New Template: `forgejo.plist.j2`
LaunchAgent plist (matching `ansible/roles/zot/templates/zot.plist.j2`):
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- {{ ansible_managed }} -->
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>mcquack.eblume.forgejo</string>
<key>ProgramArguments</key>
<array>
<string>{{ forgejo_binary }}</string>
<string>-w</string>
<string>{{ forgejo_work_path }}</string>
<string>-c</string>
<string>{{ forgejo_config_path }}</string>
<string>web</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>{{ forgejo_log_dir }}/mcquack.forgejo.out.log</string>
<key>StandardErrorPath</key>
<string>{{ forgejo_log_dir }}/mcquack.forgejo.err.log</string>
</dict>
</plist>
```
### `app.ini.j2`
No changes needed — paths already flow through variables in `defaults/main.yml`. The only change is that `RUN_USER` will pick up `erichblume` from the updated default.
## What Stays the Same
- **1Password secret fetching** — playbook `pre_tasks` are unchanged
- **`forgejo_actions_secrets` role** — API-based secret sync is unaffected
- **SSH clone URLs** — `BUILTIN_SSH_SERVER_USER` stays `forgejo` (this is the git SSH user, not the OS user)
- **Caddy routing** — still proxies to `localhost:3001`
- **SQLite database** — copied as-is to new location
- **All `app.ini` settings** — template is unchanged, just re-rendered with new paths
## Verification Checklist
After running the migration and Ansible:
- [ ] `ssh indri 'launchctl list mcquack.eblume.forgejo'` — shows running
- [ ] `curl https://forge.eblu.me/api/v1/version` — returns JSON with version
- [ ] Git clone over SSH: `git clone ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git /tmp/test-clone`
- [ ] Git push works on an existing clone
- [ ] Ansible dry-run is clean: `mise run provision-indri -- --tags forgejo --check --diff`
- [ ] `mise run services-check` — all green
- [ ] Forgejo Actions runners reconnect and jobs succeed
## Future Considerations
- **CI-built binaries** — build on gilbert or in Forgejo Actions, deploy as artifact
- **Artifact release system** — tag-triggered binary builds, similar to container releases (`mise run container-release`)
- **Automated upgrades** — Renovate or similar watching Codeberg tags, opening PRs with version bumps
- **Indri user management** — run each service as its own macOS user for isolation (a `forgejo` user exists but LaunchAgent session management under non-login users is tricky on macOS)
## Reference Pattern Files
| File | Purpose |
|------|---------|
| `ansible/roles/zot/tasks/main.yml` | Primary pattern for source-built binary tasks |
| `ansible/roles/zot/defaults/main.yml` | Variable naming conventions |
| `ansible/roles/zot/templates/zot.plist.j2` | LaunchAgent plist template |
| `ansible/roles/zot/handlers/main.yml` | Handler pattern (launchctl unload/load) |