Skip to content

Commit 195a52d

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 conflict- tolerant merge (|| true) so it no longer aborts under set -e, and a quote-agnostic changeset rescope that matches both '@primer/view- components' and "@primer/view-components" while guarding 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. Also ignores local Claude instruction files.
1 parent 132bcdf commit 195a52d

4 files changed

Lines changed: 206 additions & 11 deletions

File tree

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
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+
## Quick Reference
116+
117+
```bash
118+
git fetch upstream
119+
# TARGET = parent of the OLDEST unmerged version-bump whose parent isn't in main:
120+
for vp in $(git log upstream/main --grep="Release tracking" --not main --reverse --format=%H); do
121+
p=$(git rev-parse "$vp^"); git merge-base --is-ancestor "$p" main && continue; echo "$p"; break
122+
done
123+
script/merge-upstream <TARGET> gsed
124+
# resolve conflicts: Gemfile→bundle install, package.json→npm install, others normal
125+
script/setup && git add -A && git commit
126+
```
127+
128+
## Common Mistakes
129+
130+
- **Jumping to the parent of the *latest* version-bump.** That drags in every
131+
intervening version-bump, which already deleted its changesets — the fork loses
132+
them. Sync the *oldest* unmerged batch first (see Core principle).
133+
- **Merging a version-bump commit itself** (e.g. `Release tracking`) instead of
134+
its parent — pulls upstream's bump and conflicts with the fork's `changeset:version`.
135+
- **Assuming the changeset rename worked.** Upstream changesets use *single*
136+
quotes (`'@primer/view-components'`); a double-quote-only sed silently no-ops.
137+
The script matches either quote (and a trailing `[^-]` guards any
138+
`@primer/view-components-*` sibling). Verify after: no `@primer/view-components'`
139+
or `@primer/view-components"` remains in `.changeset/`.
140+
- **Forgetting `gsed` on macOS.** BSD `sed -i` needs a backup-suffix arg; the
141+
changeset rename silently misbehaves without a GNU-compatible sed.
142+
- **Trusting hand-merged / rerere-resolved YAML by eye.** Merges in `.github/*.yml`
143+
(workflows, `dependabot.yml`) easily produce subtly broken YAML — wrong list-item
144+
indentation, a key dropped to the wrong level, duplicate keys, a mapping/sequence
145+
mismatch. `rerere` can also replay a resolution from a *different* merge context
146+
and reintroduce upstream's quirks (e.g. list items flush with their key). Always
147+
re-validate after resolving: `ruby -ryaml -e 'YAML.load_file(ARGV[0])' <file>`
148+
(or `yq` / `python3 -c`), and confirm the *structure* parsed as intended — that a
149+
list is a list and sibling keys sit at the right depth, not just that it loads.
150+
- **Inventing a PR/release flow.** The job ends at the local merge commit on
151+
`bump/primer-upstream`; release is a separate changeset-driven process.
152+
153+
## Keeping the two forks aligned
154+
155+
This skill and `script/merge-upstream` are mirrored between view_components and
156+
octicons. When you change one, port the change to the other. The repo
157+
differences to account for:
158+
159+
- **Commit-title grep term:** `Release tracking` (view_components) vs
160+
`Version Packages` (octicons).
161+
- **Package managers / lockfiles:** view_components uses Ruby Bundler
162+
(`Gemfile.lock`, `demo/Gemfile.lock`) plus npm (`package-lock.json`,
163+
`demo/package-lock.json`), driven by `script/setup`; octicons uses yarn at the root
164+
plus npm for `octicons_angular`.
165+
- **npm scope:** `@primer/view-components``@openproject/primer-view-components`
166+
(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

demo/.byebug_history

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
continue
2+
Array(nil)
3+
params[:checked_ids]
4+
continue
5+
params[:checked_ids]

script/merge-upstream

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,47 @@
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
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-* package
39+
# and is preserved via the \1 backreference.
40+
find .changeset/ -name "*.md" -exec "${SED_INPLACE[@]}" \
41+
's/@primer\/view-components\([^-]\)/@openproject\/primer-view-components\1/g' {} +
42+
43+
# Reinstall deps and rebuild generated docs from the merged sources.
44+
script/setup
2245

23-
find .changeset/ -name "*.md" -exec $SED -i "s/\@primer\/view-components/\@openproject\/primer-view-components/g" {} +
24-
git add .changeset/
46+
git add Gemfile.lock demo/Gemfile.lock package-lock.json demo/package-lock.json .changeset/
2547

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

0 commit comments

Comments
 (0)