|
| 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`). |
0 commit comments