docs: add upstream-PR workflow + lnbits branch-model reference

Two new docs under docs/:

- upstream-prs.md — how to send a PR upstream using the
  ~/dev/upstream-prs/ worktree flow. Opens with a "Primer" section
  for anyone new to forks / pull requests (the three remote roles —
  upstream / github-fork / origin — explained as a table), then the
  per-project prerequisites, the prb/prc helper-driven workflow, and
  a "Common pitfalls" section.

- lnbits-upstream-flow.md — reference for how lnbits/lnbits actually
  moves: the dev/main branch split, the squash-merge PR convention,
  the non-FF release merge from dev into main, and how to read the
  history with --first-parent. Adapted from internal aiolabs notes
  with the fork-versioning specifics stripped; closing section is
  generic guidance for anyone maintaining their own long-lived fork.

README links both from a new "Further reading" section. The prior
"Contributing" section is retitled "Contributing to this scaffold"
to avoid colliding with the new upstream-PR doc on the term.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-05-25 20:07:03 +02:00
commit 30fbd0cef9
3 changed files with 278 additions and 1 deletions

View file

@ -192,7 +192,18 @@ lb dev # cd ~/dev/lnbits/dev
lb fix-issue-123 # cd ~/dev/lnbits/fix-issue-123
```
## Contributing
## Further reading
- [`docs/remotes.md`](docs/remotes.md) — the three remote-topology
patterns (upstream-only / github-fork / multi-remote).
- [`docs/upstream-prs.md`](docs/upstream-prs.md) — how to send a PR
upstream using the `~/dev/upstream-prs/` worktree flow. Includes
a primer for anyone new to forks / pull requests.
- [`docs/lnbits-upstream-flow.md`](docs/lnbits-upstream-flow.md) —
reference for how `lnbits/lnbits` itself moves: `dev` / `main`
branch model, squash-merge convention, release tagging.
## Contributing to this scaffold
Iteration happens under the sandboxed-claude policy from omnixy's
`scripts/sandbox-claude.sh` — a per-launch `.claude/settings.json`

View file

@ -0,0 +1,128 @@
# `lnbits/lnbits` — Upstream Development Flow
Reference for [`github.com/lnbits/lnbits`](https://github.com/lnbits/lnbits).
Verified against git history on 2026-05-18; bump the date when you
re-verify, and patch this doc if the model has drifted.
## Branch model
Two long-lived branches:
| Branch | Role | How it moves |
|---|---|---|
| `dev` | Integration / staging | One squash-merge commit per PR. Linear. |
| `main` | Release | A non-fast-forward merge of `dev` at each release. |
`main` never receives feature PRs directly. **Every change reaches
`main` through `dev`.**
## PR flow → `dev`
1. Contributor opens a PR targeting `dev` (not `main`).
2. Maintainer squash-merges via the GitHub UI.
3. The resulting commit on `dev` has:
- exactly one parent (linear history),
- subject ending in `(#NNNN)` — the squash-merge signature,
- lowercase conventional-commit prefix: `feat:`, `fix:`, `chore:`,
`chore(deps):`, `docs:`, `ci:`, `test:`, `refactor:`.
4. CI runs on the PR; nothing else is required between merge and the
commit appearing on `dev`.
Example chain on `dev`:
```
c9c68bd8 Fix: Use default reaction on bootstrap (#3965)
810a1372 fix: tighten agents file (#3966)
36d696b2 Fix: wrong use of `in` operator (#3960)
8b426efa test: add pyinstrument profiler (#3955)
```
Each is a single squashed commit. No merge commits inside `dev`.
## Release flow → `main`
When `dev` is ready to ship:
1. A release-candidate version bump lands on `dev` as a normal PR:
`chore: update to version vX.Y.Z-rcN (#NNNN)`.
2. Validation happens against the RC.
3. A final version-bump PR lands on `dev`:
`chore: update to version vX.Y.Z (#NNNN)`.
4. A maintainer runs the release merge locally:
```bash
git checkout main
git pull --ff-only origin main
git merge dev # true (non-FF) merge, default message
git push origin main
git tag vX.Y.Z # tag the merge commit or the bump commit
git push origin vX.Y.Z
```
5. The resulting merge commit on `main` has:
- **two parents** (prior `main` tip + `dev` tip),
- subject exactly `Merge branch 'dev'` (git default when on `main`),
- **not** authored via the GitHub PR-merge UI (that would produce
`Merge pull request #N from …`).
Because `main` typically carries a stray release-bump commit that isn't
on `dev`, the histories have diverged and git is forced into a true
merge. `--no-ff` is not needed for that reason.
## Reading the history
```bash
# Release log (one entry per release):
git log main --first-parent --oneline
# Full changelog leading into the next release:
git log dev --oneline
# Verify a merge is a true non-FF merge:
git log <sha> -1 --format='%P' # two parent hashes = true merge
```
## Diagram
```
(PRs squash-merged one at a time)
dev: ── A ── B ── C ── D ── E ── F (rc1) ── G ── H (v1.5.4)
╲ (true merge)
main: ────────── prev release ─────────────── X ─────────── M
Merge branch 'dev'
tag: v1.5.4
```
## Implications for contributors
- **Base PRs on `dev`.** PRs against `main` will not be accepted.
- **Use lowercase conventional-commit titles.** Verified stable across
the last 25+ merged PRs.
- **Don't expect `main` to move between releases.** It only advances
when a maintainer cuts a release merge.
- **Tagging is on `main`.** Consumers pinning to a tag get the
released state, never a `dev` snapshot.
## Adapting this in your own fork
If you maintain a long-lived fork of `lnbits/lnbits` for production use,
you'll likely want to:
1. Mirror upstream's `dev` / `main` split — easier to track upstream
merges back into your fork.
2. Adopt a version-suffix convention that surfaces fork identity in
tags and the packaged `pyproject.toml` version, e.g.
`v<upstream>-<your-tag>.<N>`. This makes it unambiguous in logs and
deployed-package metadata that you're running fork-modified code.
3. Decide whether you also need pre-release channels (`-rcN`, `-devN`)
on top of upstream's; useful if your fork ships to staging hosts
before promotion.
Specifics depend entirely on your team's needs — this scaffold
deliberately doesn't prescribe a fork-versioning scheme.

138
docs/upstream-prs.md Normal file
View file

@ -0,0 +1,138 @@
# Contributing back to upstream
How to send fixes or features upstream (to projects like `lnbits/lnbits`)
using the worktree-based PR flow built into this scaffold's dev-env
layer.
> **New to forks and pull requests?** Skim the [Primer](#primer) first.
> Otherwise jump to [Workflow](#workflow).
## Primer
GitHub, Forgejo, Gitea, and similar services host *git repositories*.
To contribute to a project you don't own:
1. **Fork** the upstream repo via the web UI ("Fork" button). This
creates `<host>/<your-user>/<repo>` — your personal copy.
2. **Branch** off your fork's `main` (or whichever branch upstream
accepts PRs against). Make your change.
3. **Push** the branch to your fork.
4. **Open a Pull Request** from your fork's branch into the upstream.
5. **Iterate** — maintainers review, you push follow-up commits to the
same branch and the PR updates automatically.
6. **Maintainer merges** — usually as a single squashed commit on
upstream's target branch.
7. **Delete the branch** on both sides once merged.
The three roles a remote can play in your local clone:
| Remote name | What it points at | Read or write? |
|---|---|---|
| `upstream` | The canonical project you're contributing to | Read (you fetch from it) |
| `github-fork` (or `fork`) | Your personal copy you push to | Write (you push to it) |
| `origin` | Optional: a private mirror you control (forgejo / gitea / codeberg / sourcehut) | Read + write |
This scaffold's `settings.nix` declares all three via `git.remotes`;
the bootstrap script wires them into the local bare repo so every
worktree sees the same set automatically. See
[remotes.md](remotes.md) for topology patterns.
## Prerequisites (per project)
1. **Fork** the upstream repo on the relevant host.
2. **Set your fork URL** in `settings.nix`:
```nix
git.remotes.fork = "https://github.com/<your-username>/lnbits";
```
Use the SSH form (`git@github.com:<your-username>/lnbits.git`) if
you've set up SSH push.
3. **Bootstrap** (or re-bootstrap). The (planned) bootstrap script
adds the `github-fork` remote to the bare repo at
`~/dev/repos/lnbits.git`.
Verify the remotes are wired:
```sh
git -C ~/dev/repos/lnbits.git remote -v
```
Expected at minimum:
```
upstream https://github.com/lnbits/lnbits (fetch / push)
github-fork https://github.com/<your-user>/lnbits (fetch / push)
```
If `origin` is set, it points at your private remote (forgejo etc.).
## Workflow
### 1. Create the PR branch
```sh
prb lnbits fix-invoice-validation
```
The (planned) `prb` helper:
- `git fetch upstream`
- creates branch `fix-invoice-validation` off `upstream/main`
- adds a worktree at `~/dev/upstream-prs/lnbits-fix-invoice-validation`
- sets the worktree's push target to your fork (`github-fork`)
### 2. Make changes
```sh
cd ~/dev/upstream-prs/lnbits-fix-invoice-validation
# … edit, test, commit …
git commit -am "Fix invoice validation for zero-amount invoices"
```
### 3. Push to your fork + open the PR
```sh
git push -u github-fork fix-invoice-validation
```
git prints a URL to open the PR. Alternatively, with the GitHub CLI:
```sh
gh pr create --base main --head <your-username>:fix-invoice-validation
```
(Use the project's preferred target branch — for `lnbits/lnbits` it's
`dev`, not `main`. See [lnbits-upstream-flow.md](lnbits-upstream-flow.md).)
### 4. Iterate on review
Push more commits to the same branch — the PR updates automatically.
Don't force-push unless you've squashed/rebased deliberately.
### 5. After merge — clean up
```sh
prc lnbits fix-invoice-validation
```
The (planned) `prc` helper removes the worktree and prunes the local
branch. On the web UI, click "Delete branch" on the merged PR to
remove your fork's copy too.
## Common pitfalls
- **PR target branch.** Many projects (including `lnbits/lnbits`)
accept PRs against `dev`, not `main`. Read the project's
CONTRIBUTING file or its branch-model reference before opening.
- **Stale branches on your fork.** They accumulate forever if you
don't clean up. `prc` handles the local side; the web UI handles
the fork side.
- **Commits drifting onto your fork's `main`.** Keep your fork's
`main` strictly in lockstep with `upstream/main`. All feature
work lives in branches.
- **Wrong remote when pushing.** If `git push` defaults to `upstream`
(because that's `origin`), you'll get an access-denied error. The
worktree's tracking remote should be `github-fork`; if not, set it:
`git branch --set-upstream-to=github-fork/<branch>`.