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:

  1. Add BlumeOps-specific changes (delete upstream workflows, add mise.toml, custom config)
  2. Develop features that might eventually be upstreamed
  3. 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

BranchBasePurposeRebased onto
upstream/mainTracks upstream’s main (or master) via git fetchNever rebased
blumeopsupstream/mainPrimary branch; BlumeOps-specific patches onlyupstream/main (daily)
feature/*blumeopsFeature developmentblumeops (after successful rebase)
feature/*-upstreamupstream/mainCherry-picked/rebased feature for upstream PRupstream/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.toml for 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 (or feature/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 via git 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-push
  • upstream/main: read-only (only updated by the rebase workflow’s git fetch)

The Upstream PR Path

When a feature is ready to be proposed upstream:

  1. Create feature/foo-upstream from upstream/main
  2. Cherry-pick or rebase feature/foo commits onto it (excluding any blumeops-specific commits)
  3. Push to the fork on the upstream platform (e.g., GitHub)
  4. 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.toml with 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-reviewed in 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-reviewed exceeds 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.ops.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 blumeops is 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 blumeops branch 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
  • adopt-dagger-ci — CI/CD build engine (consumes fork artifacts)
  • forgejo — Git forge hosting the forks
  • docs — Documentation site (first fork consumer)