Add migration plan for Forgejo brew-to-source transition #140

Merged
eblume merged 2 commits from doc/forgejo-brew-migration-plan into main 2026-02-10 10:18:54 -08:00
5 changed files with 327 additions and 0 deletions

View file

@ -0,0 +1 @@
Add migration plan for Forgejo brew-to-source transition

View file

@ -43,3 +43,12 @@ Task-oriented instructions for common BlumeOps operations. These guides assume y
| [[restart-indri]] | Safely shut down and restart indri |
| [[manage-flyio-proxy]] | Deploy, shutoff, and troubleshoot the public proxy |
| [[troubleshooting]] | Diagnose and fix common issues |
## Plans
Migration and transition plans for upcoming infrastructure changes.
| Plan | Description |
|------|-------------|
| [[plans]] | Index of all plans |
| [[migrate-forgejo-from-brew]] | Transition Forgejo from Homebrew to source-built binary |

View file

@ -0,0 +1,298 @@
---
title: "Plan: Migrate Forgejo from Brew to Source Build"
tags:
- how-to
- plans
- forgejo
---
# Plan: Migrate Forgejo from Brew to Source Build
> **Status:** Planned (not yet executed)
eblume marked this conversation as resolved

we dont need audience labels on non-tutorials

we dont need audience labels on non-tutorials
## 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
eblume marked this conversation as resolved

I think caddy and alloy have service reference cards to link to here as well. if not, just skip this comment

I think caddy and alloy have service reference cards to link to here as well. if not, just skip this comment
## 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.ops.eblu.me/eblume/forgejo.git
```
## One-Time Migration Steps
eblume marked this conversation as resolved

is that true? i thought caddy fixed that actually, can we test this?

is that true? i thought caddy fixed that actually, can we test this?
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.ops.eblu.me/eblume/forgejo.git'
```
### 3. Check Out the Desired Version Tag
eblume marked this conversation as resolved

see above question on whether hairpinning is a valid concern

see above question on whether hairpinning is a valid concern
```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):
#
eblume marked this conversation as resolved

see above notes about hairpinning

see above notes about hairpinning
# 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.ops.eblu.me/eblume/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.ops.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
eblume marked this conversation as resolved

Also add indri user management to this list. We really aught to run all of these services as their own users. I tried doing that once and I think a forgejo user still exists but it went really wonky. macos user session management is hard with launch agents, even when using the root launch agent thing.

Also add indri user management to this list. We really aught to run all of these services as their own users. I tried doing that once and I think a forgejo user still exists but it went really wonky. macos user session management is hard with launch agents, even when using the root launch agent thing.
## 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) |

View file

@ -0,0 +1,16 @@
---
title: Plans
tags:
- how-to
- plans
---
# Plans
Migration and transition plans for upcoming infrastructure changes. Each plan is a how-to document that captures the full context, steps, and verification criteria for a future execution session.
Plans differ from regular how-to guides in that they describe work that has been designed but not yet executed. Once a plan is completed, it may be archived or converted into a standard how-to guide.
| Plan | Status | Description |
|------|--------|-------------|
| [[migrate-forgejo-from-brew]] | Planned | Transition Forgejo from Homebrew to source-built binary with LaunchAgent |

View file

@ -20,6 +20,7 @@ The docs follow the [Diataxis](https://diataxis.fr/) framework:
| **[[tutorials|Tutorials]]** | Learning-oriented | "I'm new and want to understand" |
| **[[reference|Reference]]** | Information-oriented | "I need specific technical details" |
| **[[how-to|How-to]]** | Task-oriented | "I need to do X" |
| **[[plans|Plans]]** | Future work | "What's planned next?" |
| **[[explanation|Explanation]]** | Understanding-oriented | "I want to understand why" |
## Quick Paths by Audience
@ -28,6 +29,7 @@ The docs follow the [Diataxis](https://diataxis.fr/) framework:
You probably want quick access to operational details:
- [[how-to]] guides for common operations (deploy, troubleshoot, update ACLs)
- [[plans]] captures migration and transition plans for future execution
- [[reference]] has service URLs, commands, and config locations
- [[ai-assistance-guide]] explains how to work effectively with Claude
- Run `mise run zk-docs` to prime AI context with key documentation
@ -36,6 +38,7 @@ You probably want quick access to operational details:
Context for effective assistance:
- Read [[ai-assistance-guide]] for operational conventions
- [[plans]] has migration plans designed for AI-executed sessions
- [[reference]] has the technical specifics you'll need
- The repo's `CLAUDE.md` has critical rules (especially the kubectl context requirement)