| name | syncing-primer-fork |
|---|---|
| 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. |
The @openproject/* repos (view_components, octicons) are forks of primer/*.
Syncing means merging upstream changes onto a bump/primer-upstream branch via
script/merge-upstream, one upstream release batch at a time, stopping just
before each version-bump commit, then resolving dependency conflicts per package
manager.
Core principle: Sync the oldest unmerged release batch first. Merge the
parent of the oldest unmerged version-bump commit whose parent isn't already
in main — never a version-bump commit itself, and never skip ahead to the
latest one.
Why the oldest, not the latest: a version-bump commit (changesets bot)
deletes the .changeset/*.md files it consumes. If you jump to the parent of
the latest version-bump, every intervening version-bump in that range has
already deleted its changesets — so the fork loses them and can't regenerate
those changelog entries with its own changeset:version. Stopping before the
oldest unmerged version-bump keeps that batch's changesets intact. Repeat per
batch.
.gitattributes marks CHANGELOG.md, Gemfile.lock, demo/Gemfile.lock, and
lib/primer/view_components/version.rb with merge=ours, but that driver is
inert unless registered in git config. Without it those files conflict like
any other. Run once per clone (or set --global):
git config merge.ours.driver trueVerify: git config merge.ours.driver prints true.
Walk the unmerged version-bump commits oldest-first and merge the parent of the
first one whose parent isn't already in main. (main usually sits exactly at
the previous batch's boundary, i.e. the parent of the oldest unmerged
version-bump — that's a no-op, so skip it and take the next.)
- view_components: version-bump commits are titled
Release Tracking - octicons: titled
Version Packages
The --grep below is case-insensitive (-i): upstream has used both
Release Tracking and Release tracking casings over time.
git fetch upstream
GREP="Release Tracking" # octicons: "Version Packages"
for vp in $(git log upstream/main -i --grep="$GREP" --not main --reverse --format=%H); do
parent=$(git rev-parse "$vp^")
git merge-base --is-ancestor "$parent" main && continue # parent already merged → skip
echo "TARGET: $parent ($(git log -1 --format='%h %s' "$vp")'s parent)"
break
doneThat TARGET SHA is what you pass to the script. Each run advances one batch;
re-run after the fork has versioned/released the previous batch.
script/merge-upstream <TARGET> gsed # the SHA from step 1On macOS pass gsed as the 2nd arg (the script auto-detects, but be explicit).
The script: fetches upstream, builds bump/primer-upstream-ref (reset to the
SHA) and bump/primer-upstream (from origin/main), merges with --no-commit,
rescopes @primer/view-components → @openproject/primer-view-components in
.changeset/*.md, then runs script/setup, stages, and stops at an interactive
git commit.
The merge step is conflict-tolerant (|| true), but the later script/setup is
not. script/setup runs bundle install and npm install; when a Gemfile,
*.gemspec, or package.json conflicts (almost always — fork scope/version vs
upstream), the conflict markers make those installs fail → set -e aborts the
script before stage/commit. That's expected: the changeset rescope has already
run, and you're left mid-merge. Resolve conflicts (step 3) first, then run
script/setup yourself, then stage and commit.
With the ours driver registered (see prerequisite), .gitattributes resolves
CHANGELOG.md, Gemfile.lock, demo/Gemfile.lock, and the gem version.rb
automatically (fork wins). For the rest:
| Conflict in | Manager | Fix |
|---|---|---|
Gemfile / *.gemspec |
bundler | Resolve the manifest, then bundle install (root and in demo/) to bring the Gemfile.locks 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. |
package.json (root or demo/) |
npm | Resolve package.json, then npm install in that dir to regenerate package-lock.json |
component source, .changeset/*.md, previews/, docs |
— | Resolve normally; keep upstream's new components |
script/setup does both (bundle install + npm install, root and demo/, then
rake docs:build), so once conflicts are resolved a single script/setup refreshes
every lockfile and the generated docs. The mirror repo (octicons) instead uses
yarn at the root plus npm for octicons_angular.
After resolving, re-stage, run setup, and commit:
git add -A
script/setup # regenerates lockfiles + docs from the merged sources
git add -A
git commit # finishes the deferred merge commitBecause Gemfile.lock is merge=ours, any upstream bump made by bundle update <gem> alone — no Gemfile change, the common shape for Dependabot patch and
transitive bumps like nokogiri or rubocop — is discarded on merge. The fork
silently stays on the old locked version. package-lock.json is not merge=ours
(it 3-way merges), but npm install re-resolving to the fork's manifest ranges can
still lock a lower version than upstream had — the same "missing bump" failure.
The fork's own weekly Dependabot (.github/dependabot.yml) re-applies most of these
eventually, but it's cooldown-gated and its grouping/ignore rules don't mirror
upstream's — so security bumps shouldn't wait for it. After the merge, before you
commit, list what upstream locked that the fork didn't pick up:
# gems: '+' side is upstream's TARGET, '-' is the fork's merged result
git diff bump/primer-upstream..<TARGET> -- Gemfile.lock demo/Gemfile.lock
# npm (noisier — scan the "version" lines for shared packages)
git diff bump/primer-upstream..<TARGET> -- package-lock.json demo/package-lock.jsonFor each bump worth replaying now — security first — re-apply it within the fork's constraints, then re-stage:
bundle update <gem> --conservative # root; add BUNDLE_GEMFILE=demo/Gemfile for demo
npm install <pkg>@<version> # run in the dir that owns the lockfile--conservative limits the change to that one gem instead of dragging in unrelated
transitive bumps. If the fork's Gemfile constraint forbids upstream's version,
that's a manifest bump (resolve in step 3), not a lockfile-only replay — leave it to
the fork's Dependabot. Leave everything non-urgent to the weekly Dependabot cycle.
These are dependency bumps, so no changeset (matches the skip changeset label the
fork's Dependabot uses).
The SHA is meaningless to a reviewer — title the PR with the upstream package
version the batch reaches instead, read from package.json at TARGET
(TARGET sits right before the next version-bump, so its version field is
the last upstream release this batch actually includes):
git show <TARGET>:package.json | grep '"version"'e.g. TARGET's version 0.51.6 → title Sync Primer view_components upstream through v0.51.6,
not ...through 6733f3c0. The SHA can still go in the PR body for traceability.
git fetch upstream
# TARGET = parent of the OLDEST unmerged version-bump whose parent isn't in main:
for vp in $(git log upstream/main -i --grep="Release Tracking" --not main --reverse --format=%H); do
p=$(git rev-parse "$vp^"); git merge-base --is-ancestor "$p" main && continue; echo "$p"; break
done
script/merge-upstream <TARGET> gsed
# resolve conflicts: Gemfile→bundle install, package.json→npm install, others normal
# audit dropped bumps (merge=ours discards upstream Gemfile.lock-only bumps):
git diff bump/primer-upstream..<TARGET> -- Gemfile.lock demo/Gemfile.lock
script/setup && git add -A && git commit- Jumping to the parent of the latest version-bump. That drags in every intervening version-bump, which already deleted its changesets — the fork loses them. Sync the oldest unmerged batch first (see Core principle).
- Merging a version-bump commit itself (e.g.
Release Tracking) instead of its parent — pulls upstream's bump and conflicts with the fork'schangeset:version. - Assuming the changeset rename worked. Upstream changesets use single
quotes (
'@primer/view-components'); a double-quote-only sed silently no-ops. The script matches either quote (and a trailing[^-]guards any@primer/view-components-*sibling). Verify after: no@primer/view-components'or@primer/view-components"remains in.changeset/. - Forgetting
gsedon macOS. BSDsed -ineeds a backup-suffix arg; the changeset rename silently misbehaves without a GNU-compatible sed. - Trusting hand-merged / rerere-resolved YAML by eye. Merges in
.github/*.yml(workflows,dependabot.yml) easily produce subtly broken YAML — wrong list-item indentation, a key dropped to the wrong level, duplicate keys, a mapping/sequence mismatch.rererecan also replay a resolution from a different merge context and reintroduce upstream's quirks (e.g. list items flush with their key). Always re-validate after resolving:ruby -ryaml -e 'YAML.load_file(ARGV[0])' <file>(oryq/python3 -c), and confirm the structure parsed as intended — that a list is a list and sibling keys sit at the right depth, not just that it loads. - Assuming rerere makes the sync reproducible.
rerere.enabledis usually set globally (~/.gitconfig), and.git/rr-cacheis per-clone and never committed — so the same upstream batch resolves differently for different people, and every replay lands silently in the working tree (all files, not just YAML; withrerere.autoupdateit's even auto-staged). Diff every rerere-touched hunk before staging; if a replayed resolution looks stale,git rerere forget <path>and resolve it by hand. - Inventing a PR/release flow. The job ends at the local merge commit on
bump/primer-upstream; release is a separate changeset-driven process. - Titling the PR with the merge SHA. A hex SHA tells a reviewer nothing. Use the upstream package version at TARGET instead (see step 5).
This skill and script/merge-upstream are mirrored between view_components and
octicons. When you change one, port the change to the other. The repo
differences to account for:
- Commit-title grep term:
Release Tracking(view_components) vsVersion Packages(octicons). - Package managers / lockfiles: view_components uses Ruby Bundler
(
Gemfile.lock,demo/Gemfile.lock) plus npm (package-lock.json,demo/package-lock.json), driven byscript/setup; octicons uses yarn at the root plus npm forocticons_angular. - npm scope:
@primer/view-components→@openproject/primer-view-components(octicons:@primer/octicons→@openproject/octicons).