Skip to content

Commit 72e8240

Browse files
committed
Ports merge-upstream fixes and sync skill
Ports the merge-upstream tooling and the syncing-primer-fork skill from the openproject-octicons fork, adapted for view_components. script/merge-upstream gains GNU/BSD sed auto-detection, a merge and a script/setup that both stay conflict-tolerant (|| true) so neither aborts under set -e before the staging and commit, and a quote-agnostic changeset rescope that matches both '@primer/view-components' and "@primer/view-components", rescopes the token at end of line, and still guards any -suffix sibling. It runs script/setup (not a non-existent top-level ./setup). The skill documents the workflow: merge the parent of the oldest unmerged "Release tracking" commit to preserve changesets, resolve conflicts per package manager (bundler for the manifests, npm for the package-lock.jsons; Gemfile.lock is merge=ours, not bundler-resolved), the merge.ours.driver prerequisite, and that script/setup aborts on conflicted manifests so conflicts must be resolved first. It also adds an audit step for the upstream Gemfile.lock-only dependency bumps that merge=ours silently drops (replay the urgent ones with bundle update --conservative; leave the rest to the fork's Dependabot), and warns that rerere is per-clone and enabled globally, so a sync resolves differently per person and every replayed hunk needs checking. Also ignores local Claude instruction files.
1 parent f53f7e4 commit 72e8240

3 files changed

Lines changed: 251 additions & 12 deletions

File tree

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
---
2+
name: syncing-primer-fork
3+
description: Use when syncing this OpenProject fork with its upstream Primer repo (primer/view_components or primer/octicons) — pulling new components, merging upstream changes, resolving dependency/lockfile conflicts, or running script/merge-upstream.
4+
---
5+
6+
# Syncing a Primer Fork from Upstream
7+
8+
## Overview
9+
10+
The `@openproject/*` repos (view_components, octicons) are forks of `primer/*`.
11+
Syncing means merging upstream changes onto a `bump/primer-upstream` branch via
12+
`script/merge-upstream`, **one upstream release batch at a time**, stopping just
13+
before each version-bump commit, then resolving dependency conflicts per package
14+
manager.
15+
16+
**Core principle:** Sync the *oldest* unmerged release batch first. Merge the
17+
*parent* of the oldest unmerged version-bump commit whose parent isn't already
18+
in `main` — never a version-bump commit itself, and never skip ahead to the
19+
latest one.
20+
21+
**Why the oldest, not the latest:** a version-bump commit (changesets bot)
22+
*deletes* the `.changeset/*.md` files it consumes. If you jump to the parent of
23+
the *latest* version-bump, every intervening version-bump in that range has
24+
already deleted its changesets — so the fork loses them and can't regenerate
25+
those changelog entries with its own `changeset:version`. Stopping before the
26+
*oldest* unmerged version-bump keeps that batch's changesets intact. Repeat per
27+
batch.
28+
29+
## One-time prerequisite: register the `ours` merge driver
30+
31+
`.gitattributes` marks `CHANGELOG.md`, `Gemfile.lock`, `demo/Gemfile.lock`, and
32+
`lib/primer/view_components/version.rb` with `merge=ours`, but that driver is
33+
**inert unless registered in git config**. Without it those files conflict like
34+
any other. Run once per clone (or set `--global`):
35+
36+
```bash
37+
git config merge.ours.driver true
38+
```
39+
40+
Verify: `git config merge.ours.driver` prints `true`.
41+
42+
## Procedure
43+
44+
### 1. Pick the SHA — parent of the *oldest* unmerged version-bump commit
45+
46+
Walk the unmerged version-bump commits oldest-first and merge the parent of the
47+
first one whose parent isn't already in `main`. (`main` usually sits exactly at
48+
the previous batch's boundary, i.e. the parent of the *oldest* unmerged
49+
version-bump — that's a no-op, so skip it and take the next.)
50+
51+
- view_components: version-bump commits are titled **`Release tracking`**
52+
- octicons: titled **`Version Packages`**
53+
54+
```bash
55+
git fetch upstream
56+
GREP="Release tracking" # octicons: "Version Packages"
57+
for vp in $(git log upstream/main --grep="$GREP" --not main --reverse --format=%H); do
58+
parent=$(git rev-parse "$vp^")
59+
git merge-base --is-ancestor "$parent" main && continue # parent already merged → skip
60+
echo "TARGET: $parent ($(git log -1 --format='%h %s' "$vp")'s parent)"
61+
break
62+
done
63+
```
64+
65+
That `TARGET` SHA is what you pass to the script. Each run advances one batch;
66+
re-run after the fork has versioned/released the previous batch.
67+
68+
### 2. Run the merge script
69+
70+
```bash
71+
script/merge-upstream <TARGET> gsed # the SHA from step 1
72+
```
73+
74+
On macOS pass `gsed` as the 2nd arg (the script auto-detects, but be explicit).
75+
The script: fetches upstream, builds `bump/primer-upstream-ref` (reset to the
76+
SHA) and `bump/primer-upstream` (from `origin/main`), merges with `--no-commit`,
77+
rescopes `@primer/view-components``@openproject/primer-view-components` in
78+
`.changeset/*.md`, then runs `script/setup`, stages, and stops at an interactive
79+
`git commit`.
80+
81+
**The merge step is conflict-tolerant (`|| true`), but the later `script/setup` is
82+
not.** `script/setup` runs `bundle install` and `npm install`; when a `Gemfile`,
83+
`*.gemspec`, or `package.json` conflicts (almost always — fork scope/version vs
84+
upstream), the conflict markers make those installs fail → `set -e` aborts the
85+
script *before* stage/commit. That's expected: the changeset rescope has already
86+
run, and you're left mid-merge. **Resolve conflicts (step 3) first, then run
87+
`script/setup` yourself**, then stage and commit.
88+
89+
### 3. Resolve conflicts by package manager
90+
91+
With the `ours` driver registered (see prerequisite), `.gitattributes` resolves
92+
`CHANGELOG.md`, `Gemfile.lock`, `demo/Gemfile.lock`, and the gem `version.rb`
93+
automatically (fork wins). For the rest:
94+
95+
| Conflict in | Manager | Fix |
96+
|---|---|---|
97+
| `Gemfile` / `*.gemspec` | **bundler** | Resolve the manifest, then `bundle install` (root and in `demo/`) to bring the `Gemfile.lock`s in sync. `bundle install` does **not** resolve conflict markers in a `Gemfile.lock` itself — those are handled by `merge=ours`; if any ever appear (driver not registered), `git checkout --ours <Gemfile.lock>` first, then `bundle install`. |
98+
| `package.json` (root or `demo/`) | **npm** | Resolve `package.json`, then `npm install` in that dir to regenerate `package-lock.json` |
99+
| component source, `.changeset/*.md`, `previews/`, docs || Resolve normally; keep upstream's new components |
100+
101+
`script/setup` does both (`bundle install` + `npm install`, root and `demo/`, then
102+
`rake docs:build`), so once conflicts are resolved a single `script/setup` refreshes
103+
every lockfile and the generated docs. The mirror repo (octicons) instead uses
104+
yarn at the root plus npm for `octicons_angular`.
105+
106+
After resolving, re-stage, run setup, and commit:
107+
108+
```bash
109+
git add -A
110+
script/setup # regenerates lockfiles + docs from the merged sources
111+
git add -A
112+
git commit # finishes the deferred merge commit
113+
```
114+
115+
### 4. Audit dependency bumps dropped by `merge=ours`
116+
117+
Because `Gemfile.lock` is `merge=ours`, any upstream bump made by `bundle update
118+
<gem>` **alone** — no `Gemfile` change, the common shape for Dependabot patch and
119+
transitive bumps like `nokogiri` or `rubocop` — is discarded on merge. The fork
120+
silently stays on the old locked version. `package-lock.json` is *not* `merge=ours`
121+
(it 3-way merges), but `npm install` re-resolving to the fork's manifest ranges can
122+
still lock a **lower** version than upstream had — the same "missing bump" failure.
123+
124+
The fork's own weekly Dependabot (`.github/dependabot.yml`) re-applies most of these
125+
eventually, but it's cooldown-gated and its grouping/ignore rules don't mirror
126+
upstream's — so security bumps shouldn't wait for it. After the merge, before you
127+
commit, list what upstream locked that the fork didn't pick up:
128+
129+
```bash
130+
# gems: '+' side is upstream's TARGET, '-' is the fork's merged result
131+
git diff bump/primer-upstream..<TARGET> -- Gemfile.lock demo/Gemfile.lock
132+
# npm (noisier — scan the "version" lines for shared packages)
133+
git diff bump/primer-upstream..<TARGET> -- package-lock.json demo/package-lock.json
134+
```
135+
136+
For each bump worth replaying now — **security first** — re-apply it within the
137+
fork's constraints, then re-stage:
138+
139+
```bash
140+
bundle update <gem> --conservative # root; add BUNDLE_GEMFILE=demo/Gemfile for demo
141+
npm install <pkg>@<version> # run in the dir that owns the lockfile
142+
```
143+
144+
`--conservative` limits the change to that one gem instead of dragging in unrelated
145+
transitive bumps. If the fork's `Gemfile` constraint *forbids* upstream's version,
146+
that's a manifest bump (resolve in step 3), not a lockfile-only replay — leave it to
147+
the fork's Dependabot. Leave everything non-urgent to the weekly Dependabot cycle.
148+
These are dependency bumps, so no changeset (matches the `skip changeset` label the
149+
fork's Dependabot uses).
150+
151+
## Quick Reference
152+
153+
```bash
154+
git fetch upstream
155+
# TARGET = parent of the OLDEST unmerged version-bump whose parent isn't in main:
156+
for vp in $(git log upstream/main --grep="Release tracking" --not main --reverse --format=%H); do
157+
p=$(git rev-parse "$vp^"); git merge-base --is-ancestor "$p" main && continue; echo "$p"; break
158+
done
159+
script/merge-upstream <TARGET> gsed
160+
# resolve conflicts: Gemfile→bundle install, package.json→npm install, others normal
161+
# audit dropped bumps (merge=ours discards upstream Gemfile.lock-only bumps):
162+
git diff bump/primer-upstream..<TARGET> -- Gemfile.lock demo/Gemfile.lock
163+
script/setup && git add -A && git commit
164+
```
165+
166+
## Common Mistakes
167+
168+
- **Jumping to the parent of the *latest* version-bump.** That drags in every
169+
intervening version-bump, which already deleted its changesets — the fork loses
170+
them. Sync the *oldest* unmerged batch first (see Core principle).
171+
- **Merging a version-bump commit itself** (e.g. `Release tracking`) instead of
172+
its parent — pulls upstream's bump and conflicts with the fork's `changeset:version`.
173+
- **Assuming the changeset rename worked.** Upstream changesets use *single*
174+
quotes (`'@primer/view-components'`); a double-quote-only sed silently no-ops.
175+
The script matches either quote (and a trailing `[^-]` guards any
176+
`@primer/view-components-*` sibling). Verify after: no `@primer/view-components'`
177+
or `@primer/view-components"` remains in `.changeset/`.
178+
- **Forgetting `gsed` on macOS.** BSD `sed -i` needs a backup-suffix arg; the
179+
changeset rename silently misbehaves without a GNU-compatible sed.
180+
- **Trusting hand-merged / rerere-resolved YAML by eye.** Merges in `.github/*.yml`
181+
(workflows, `dependabot.yml`) easily produce subtly broken YAML — wrong list-item
182+
indentation, a key dropped to the wrong level, duplicate keys, a mapping/sequence
183+
mismatch. `rerere` can also replay a resolution from a *different* merge context
184+
and reintroduce upstream's quirks (e.g. list items flush with their key). Always
185+
re-validate after resolving: `ruby -ryaml -e 'YAML.load_file(ARGV[0])' <file>`
186+
(or `yq` / `python3 -c`), and confirm the *structure* parsed as intended — that a
187+
list is a list and sibling keys sit at the right depth, not just that it loads.
188+
- **Assuming rerere makes the sync reproducible.** `rerere.enabled` is usually set
189+
*globally* (`~/.gitconfig`), and `.git/rr-cache` is **per-clone and never
190+
committed** — so the same upstream batch resolves *differently* for different
191+
people, and every replay lands silently in the working tree (all files, not just
192+
YAML; with `rerere.autoupdate` it's even auto-staged). Diff every rerere-touched
193+
hunk before staging; if a replayed resolution looks stale, `git rerere forget
194+
<path>` and resolve it by hand.
195+
- **Inventing a PR/release flow.** The job ends at the local merge commit on
196+
`bump/primer-upstream`; release is a separate changeset-driven process.
197+
198+
## Keeping the two forks aligned
199+
200+
This skill and `script/merge-upstream` are mirrored between view_components and
201+
octicons. When you change one, port the change to the other. The repo
202+
differences to account for:
203+
204+
- **Commit-title grep term:** `Release tracking` (view_components) vs
205+
`Version Packages` (octicons).
206+
- **Package managers / lockfiles:** view_components uses Ruby Bundler
207+
(`Gemfile.lock`, `demo/Gemfile.lock`) plus npm (`package-lock.json`,
208+
`demo/package-lock.json`), driven by `script/setup`; octicons uses yarn at the root
209+
plus npm for `octicons_angular`.
210+
- **npm scope:** `@primer/view-components``@openproject/primer-view-components`
211+
(octicons: `@primer/octicons``@openproject/octicons`).

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,7 @@ File::NULL
6161

6262
# OpenProject custom rules
6363
!app/assets/images
64+
65+
# Local Claude instructions
66+
CLAUDE.local.md
67+
claude.local.md

script/merge-upstream

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,51 @@
22

33
set -e
44

5-
6-
COMMIT=${1?"Usage: $0 <Upstream commit> <optional sed command>"}
7-
SED=${2:-sed}
8-
9-
git config remote.upstream.url || git remote add upstream https://github.com/primer/view_components.git
5+
COMMIT=${1?"Usage: $0 <Upstream commit> <optional sed binary, e.g. gsed>"}
6+
7+
# In-place sed. macOS/BSD sed needs an (empty) backup suffix for -i, GNU sed
8+
# does not. Pick a GNU-compatible invocation, or honour an explicit override.
9+
if [ -n "$2" ]; then
10+
SED_INPLACE=("$2" -i)
11+
elif command -v gsed >/dev/null 2>&1; then
12+
SED_INPLACE=(gsed -i)
13+
elif sed --version >/dev/null 2>&1; then
14+
SED_INPLACE=(sed -i) # GNU sed
15+
else
16+
SED_INPLACE=(sed -i '') # BSD/macOS sed
17+
fi
18+
19+
git config remote.upstream.url \
20+
|| git remote add upstream https://github.com/primer/view_components.git
1021
git fetch upstream
1122

1223
git checkout main
1324
git branch -D bump/primer-upstream &>/dev/null || true
1425
git branch -D bump/primer-upstream-ref &>/dev/null || true
1526

1627
git checkout -b bump/primer-upstream-ref
17-
git reset --hard $COMMIT
28+
git reset --hard "$COMMIT"
1829

1930
git checkout -b bump/primer-upstream
2031
git reset --hard origin/main
21-
git merge bump/primer-upstream-ref --no-commit
22-
23-
find .changeset/ -name "*.md" -exec $SED -i "s/\@primer\/view-components/\@openproject\/primer-view-components/g" {} +
24-
git add .changeset/
32+
# Merging upstream into this fork is expected to conflict (renamed package,
33+
# generated files). Don't let set -e abort; leave the merge state to resolve.
34+
git merge bump/primer-upstream-ref --no-commit || true
35+
36+
# Rename the @primer/view-components package token in changesets, whatever quote
37+
# style the changeset uses ('@primer/view-components' or "@primer/view-components").
38+
# The trailing ([^-]|$) guards against any sibling @primer/view-components-*
39+
# package while still matching the token at end of line; the captured char (empty
40+
# at EOL) is preserved via the \1 backreference.
41+
find .changeset/ -name "*.md" -exec "${SED_INPLACE[@]}" -E \
42+
's/@primer\/view-components([^-]|$)/@openproject\/primer-view-components\1/g' {} +
43+
44+
# Reinstall deps and rebuild generated docs from the merged sources. This is
45+
# expected to fail while manifests (Gemfile, *.gemspec, package.json) still hold
46+
# conflict markers, so stay tolerant like the merge above and let the staging and
47+
# commit below run for the clean case; resolve conflicts then re-run script/setup.
48+
script/setup || true
49+
50+
git add Gemfile.lock demo/Gemfile.lock package-lock.json demo/package-lock.json .changeset/
2551

26-
./setup
27-
git add */Gemfile.lock */package-lock.json
2852
git commit

0 commit comments

Comments
 (0)