blumeops/docs/how-to/plans/upstream-fork-strategy.md
Erich Blume b0bac91ca9 Fix frontmatter field name for Quartz date display (#158)
## Summary

- Rename `date-modified` -> `modified` in all 80 docs and the `docs-check-frontmatter` task

Quartz's `CreatedModifiedDate` plugin recognizes `modified`, `lastmod`, `updated`, and `last-modified` — but not `date-modified`. The wrong field name caused Quartz to ignore frontmatter dates entirely and fall through to filesystem timestamps (UTC inside Dagger), showing Feb 12 on pages built late on Feb 11 PST.

## Test plan

- [x] `mise run docs-check-frontmatter` passes
- [ ] Kick off docs release after merge — verify rendered dates match frontmatter values

Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/158
2026-02-11 16:45:12 -08:00

11 KiB

title modified tags
Plan: Upstream Fork Strategy 2026-02-11
how-to
plans
forgejo
git

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

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.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/eblume/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/eblume/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)