## Summary Expose Forgejo publicly at `forge.eblu.me` via the Fly.io reverse proxy — the first dynamic, authenticated public-facing service. - **Forgejo hardening:** Domain changed to forge.eblu.me, SSH stays on forge.ops.eblu.me, reverse proxy trust headers configured, local registration locked to external-only (Authentik SSO) - **Tailscale Ingress:** ExternalName Service + Ingress in tailscale-operator creates forge.tail8d86e.ts.net endpoint - **Fly.io proxy:** nginx server block with rate-limited auth endpoints (3r/s), fail2ban with custom nginx-deny action, security headers, /swagger blocked, WebSocket support, 512m body limit - **Authentik:** OAuth callback updated to forge.eblu.me - **DNS/TLS:** CNAME record in Pulumi, cert in fly-setup - **Rename:** ~29 files updated from forge.ops.eblu.me to forge.eblu.me (HTTPS refs only; SSH, container builds, and Caddy table kept as-is) ## Deployment Order 1. `mise run provision-indri -- --tags forgejo` (config changes) 2. Verify forge.ops.eblu.me still works 3. `argocd app set tailscale-operator --revision feature/forge-public && argocd app sync tailscale-operator` 4. Verify `curl https://forge.tail8d86e.ts.net` 5. `cd fly && fly deploy` 6. Verify pre-DNS: `curl -H "Host: forge.eblu.me" https://blumeops-proxy.fly.dev/` 7. `fly certs add forge.eblu.me -a blumeops-proxy` 8. `argocd app set authentik --revision feature/forge-public && argocd app sync authentik` 9. `mise run dns-preview && mise run dns-up` 10. Full verification (see below) 11. Rehearse `mise run fly-shutoff` 12. After merge: reset ArgoCD revisions to main, re-sync ## Verification Checklist - [ ] forge.eblu.me loads, shows public repos - [ ] forge.ops.eblu.me still works from tailnet - [ ] SSH clone via forge.ops.eblu.me:2222 works - [ ] HTTPS clone via forge.eblu.me works - [ ] UI shows forge.eblu.me for HTTPS clone, forge.ops.eblu.me for SSH - [ ] /swagger returns 403 - [ ] Rapid login attempts trigger 429 rate limit - [ ] fail2ban bans after 5 failed logins in 10 minutes - [ ] ArgoCD can still sync (SSH unaffected) - [ ] `mise run fly-shutoff` stops all public traffic - [ ] `mise run services-check` passes Reviewed-on: #278
11 KiB
| title | modified | tags | ||||
|---|---|---|---|---|---|---|
| Plan: Upstream Fork Strategy | 2026-02-11 |
|
Plan: Upstream Fork Strategy
Status: Planned (design sketch — not yet executed)
Background
Several BlumeOps projects need to track upstream repositories while maintaining local modifications. Examples include a personal Quartz fork (for docs site customization) and potentially other tools where upstream changes need to flow in continuously.
The current approach — Forgejo auto-tracking mirrors — works for read-only copies but breaks down when we need to:
- Add BlumeOps-specific changes (delete upstream workflows, add
mise.toml, custom config) - Develop features that might eventually be upstreamed
- Keep all of this synchronized as upstream evolves
Goals
- Upstream changes flow in automatically (daily rebase)
- Rebase conflicts are detected and reported, not silently ignored
- BlumeOps-specific patches are cleanly separated from upstream-candidate work
- Feature branches that could become upstream PRs are maintained independently
Branch Model
The strategy uses stacked branches — each layer builds on the one below:
upstream/main (read-only tracking branch)
│
▼
blumeops (primary branch — blumeops-specific patches)
│ e.g., delete .github/, add mise.toml,
│ configure for tailnet, etc.
│
├──▶ feature/foo (feature branch — developed on top of blumeops)
│ │ intended for local use, may never go upstream
│ │
│ └──▶ feature/foo-upstream (optional — same changes rebased onto main)
│ for submitting as an upstream PR
│
└──▶ feature/bar (another feature branch)
Branch Purposes
| Branch | Base | Purpose | Rebased onto |
|---|---|---|---|
upstream/main |
— | Tracks upstream's main (or master) via git fetch |
Never rebased |
blumeops |
upstream/main |
Primary branch; BlumeOps-specific patches only | upstream/main (daily) |
feature/* |
blumeops |
Feature development | blumeops (after successful rebase) |
feature/*-upstream |
upstream/main |
Cherry-picked/rebased feature for upstream PR | upstream/main (on demand) |
What Goes in blumeops vs feature/*
blumeops branch — infrastructure-level changes that are permanent and BlumeOps-specific:
- Delete upstream CI workflows (
.github/workflows/) - Add
mise.tomlfor local tooling - Add or modify configuration for the BlumeOps environment
- Patch version pins or dependency overrides
feature/* branches — functional changes to the project itself:
- Bug fixes you want to contribute upstream
- New features or customizations
- Anything that could theoretically stand on its own as a PR to the upstream project
This separation ensures the blumeops branch stays small and conflict-resistant (infrastructure changes rarely conflict with upstream code changes), while feature branches carry the substantive modifications.
Daily Rebase Workflow
A Forgejo Actions workflow runs on a schedule to keep blumeops rebased onto the latest upstream:
Workflow Outline
trigger: cron (daily) or manual dispatch
1. Fetch upstream remote
2. Check if upstream/main has new commits since last rebase
3. If no new commits → exit early
4. Attempt: git rebase blumeops --onto upstream/main
5. If rebase succeeds:
a. Force-push blumeops
b. For each feature/* branch:
- Attempt rebase onto updated blumeops
- If success → force-push
- If conflict → skip, record failure
6. If rebase fails (conflict):
a. Abort rebase
b. Create or update a Forgejo issue with conflict details
c. Label the issue for visibility
Conflict Reporting
When a rebase fails, the workflow creates (or updates) a Forgejo issue via the API:
- Title:
Rebase conflict: blumeops onto upstream/main(orfeature/foo onto blumeops) - Body: Include the conflicting files, the upstream commit range, and the git output
- Labels:
rebase-conflict,automated - Assignee:
eblume
The issue serves as a task to manually resolve the conflict. Once resolved and force-pushed, the next daily run succeeds and the issue can be closed.
Safety Guards
- Never force-push
upstream/main— this is a read-only tracking branch, only updated viagit fetch - Abort on any rebase ambiguity — if the rebase produces unexpected state, abort and report rather than pushing garbage
- Dry-run mode — the workflow should support a manual dispatch input to run in dry-run mode (rebase but don't push, just report what would happen)
- Lock file — prevent concurrent rebase runs from colliding (Forgejo Actions concurrency groups)
One-Time Setup Per Fork
Step 1: Create the Mirror
Set up a Forgejo auto-tracking mirror of the upstream project:
Forgejo → New Migration → Git → URL: https://github.com/org/project.git
Step 2: Disable Mirroring
Once mirrored, disable the auto-sync in Forgejo repository settings. The repository is now a regular Forgejo repo with the upstream history.
Step 3: Set Up Remotes
cd ~/code/3rd/<project>
git remote rename origin forge
git remote add upstream https://github.com/org/project.git
git fetch upstream
Step 4: Create the blumeops Branch
git checkout upstream/main
git checkout -b blumeops
# Apply blumeops-specific patches
git commit -m "BlumeOps: remove upstream workflows, add mise.toml"
git push forge blumeops
Set blumeops as the default branch in Forgejo repository settings.
Step 5: Add the Rebase Workflow
Add .forgejo/workflows/rebase-upstream.yaml to the blumeops branch. This workflow is itself a blumeops-specific patch — upstream doesn't have it.
Step 6: Protect Branches
Configure Forgejo branch protection:
blumeops: only the rebase workflow (and manual push) can force-pushupstream/main: read-only (only updated by the rebase workflow'sgit fetch)
The Upstream PR Path
When a feature is ready to be proposed upstream:
- Create
feature/foo-upstreamfromupstream/main - Cherry-pick or rebase
feature/foocommits onto it (excluding anyblumeops-specific commits) - Push to the fork on the upstream platform (e.g., GitHub)
- Open PR from the fork to upstream
This branch is maintained independently — it does not participate in the daily rebase. It's a point-in-time snapshot for the PR. If the PR needs updates, rebase it manually.
First Instance: Quartz Fork
Quartz (the documentation site generator) is the planned first fork and the primary motivation for this strategy.
- Upstream:
https://github.com/jackyzha0/quartz.git - Forge repo:
forge.ops.eblu.me/mirrors/quartz - Primary branch:
blumeops
BlumeOps-Specific Patches (blumeops branch)
Changes that are permanently BlumeOps-specific and would never go upstream:
- Remove
.github/workflows - Add
mise.tomlwith pinned Node version - Configure Quartz defaults for BlumeOps site metadata
Feature Work (feature/* branches)
The key feature motivating this fork is last-reviewed frontmatter support. BlumeOps documentation uses a last-reviewed date in frontmatter to track documentation staleness (see mise run docs-review). Upstream Quartz has no awareness of this field. The fork enables:
- Rendering
last-reviewedin article headers — display when a doc was last reviewed, making staleness visible to readers without running CLI tools - Staleness indicators — visual styling (e.g., a warning banner) for docs where
last-reviewedexceeds a threshold - Sorting/filtering by review date — Quartz explorer or listing pages that surface docs needing attention
This is a strong upstream PR candidate — other Quartz users maintaining knowledge bases would benefit from custom frontmatter rendering. The feature/last-reviewed branch would be developed on the blumeops branch (for local use) with a parallel feature/last-reviewed-upstream branch rebased onto upstream/main for the PR.
Integration with Dagger Docs Build
This fork directly supports the adopt-dagger-ci plan. Once the fork exists, the Dagger build_docs function switches from cloning upstream Quartz to using the fork:
# Before (cloning upstream):
.with_exec(["git", "clone", "--depth=1",
"https://github.com/jackyzha0/quartz.git", "/tmp/quartz"])
# After (using the BlumeOps fork):
.with_exec(["git", "clone", "--depth=1", "--branch=blumeops",
"https://forge.eblu.me/mirrors/quartz.git", "/tmp/quartz"])
This means the build-blumeops.yaml workflow automatically picks up fork customizations (like last-reviewed rendering) when building docs — no separate integration step needed. Local iteration via dagger call build-docs also uses the fork, so you can test Quartz customizations against actual BlumeOps content before pushing.
Open Questions
- Rebase vs merge: This plan uses rebase for a clean linear history. Merge commits would avoid force-pushes but create a messier history. Rebase is preferred for small forks; revisit if the commit volume grows.
- Notification mechanism: Forgejo issues are proposed for conflict reporting. Alternatives: email, Slack webhook, Todoist task via API. Issues are preferred because they're visible in the forge and can carry discussion.
- Feature branch automation: The daily rebase of feature branches onto
blumeopsis aggressive — it means feature branches are force-pushed daily. An alternative is to only rebase feature branches on demand (manually or via workflow dispatch). Start with manual and automate later based on experience. - Multiple upstreams: Some projects track multiple remotes (e.g., a CNCF project with a GitHub mirror and a self-hosted primary). The workflow should support configurable upstream remote URLs.
Future Considerations
- Renovate integration — Renovate could watch upstream tags and open PRs to the
blumeopsbranch when new releases are available, complementing the daily rebase with release-aware updates - Dagger integration — forked projects that produce build artifacts can use the BlumeOps Dagger module for builds, sharing the same local iteration and CI patterns
- Template repository — once the pattern is proven with quartz, create a template repo or mise task that scaffolds the branch structure and rebase workflow for new forks
Related
- adopt-dagger-ci — CI/CD build engine (consumes fork artifacts)
- forgejo — Git forge hosting the forks
- docs — Documentation site (first fork consumer)