Skip to content

Commit d73f2f2

Browse files
Automate vendored patch proof with site-specific ledger (CL-5720) (#462)
1 parent d0892eb commit d73f2f2

12 files changed

Lines changed: 589 additions & 164 deletions

File tree

bin/vendor-patch-diff

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
#!/usr/bin/env bash
2+
# Diff vendored packages against a pristine upstream checkout at the SHA
3+
# recorded in docs/VENDORING.md. Prints only local divergences (the lines
4+
# that are ours). Markers in vendor/ are navigation; this diff is proof.
5+
#
6+
# Usage:
7+
# bin/vendor-patch-diff [--upstream PATH] [package...]
8+
#
9+
# package is a vendor directory basename (e.g. intx-inference) or an
10+
# @intx/* name. With no packages, diffs every row in VENDORING.md that
11+
# carries local patches.
12+
#
13+
# Upstream clone resolution (first hit wins):
14+
# 1. --upstream PATH
15+
# 2. $INTERCHANGE_UPSTREAM
16+
# 3. ../interchange relative to the repo root
17+
# 4. ../../interchange relative to the repo root
18+
set -euo pipefail
19+
20+
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
21+
VENDORING="$REPO_ROOT/docs/VENDORING.md"
22+
23+
usage() {
24+
sed -n '2,18p' "$0" | sed 's/^# \{0,1\}//'
25+
exit 2
26+
}
27+
28+
UPSTREAM=""
29+
PACKAGES=()
30+
31+
while [[ $# -gt 0 ]]; do
32+
case "$1" in
33+
--upstream)
34+
[[ $# -ge 2 ]] || usage
35+
UPSTREAM="$2"
36+
shift 2
37+
;;
38+
-h|--help)
39+
usage
40+
;;
41+
--)
42+
shift
43+
PACKAGES+=("$@")
44+
break
45+
;;
46+
-*)
47+
echo "unknown flag: $1" >&2
48+
usage
49+
;;
50+
*)
51+
PACKAGES+=("$1")
52+
shift
53+
;;
54+
esac
55+
done
56+
57+
if [[ -z "$UPSTREAM" ]]; then
58+
if [[ -n "${INTERCHANGE_UPSTREAM:-}" ]]; then
59+
UPSTREAM="$INTERCHANGE_UPSTREAM"
60+
elif [[ -d "$REPO_ROOT/../interchange/.git" || -f "$REPO_ROOT/../interchange/.git" ]]; then
61+
UPSTREAM="$(cd "$REPO_ROOT/../interchange" && pwd)"
62+
elif [[ -d "$REPO_ROOT/../../interchange/.git" || -f "$REPO_ROOT/../../interchange/.git" ]]; then
63+
UPSTREAM="$(cd "$REPO_ROOT/../../interchange" && pwd)"
64+
else
65+
echo "error: no upstream Interchange clone found." >&2
66+
echo "Pass --upstream PATH or set INTERCHANGE_UPSTREAM." >&2
67+
exit 1
68+
fi
69+
fi
70+
71+
if [[ ! -d "$UPSTREAM" ]]; then
72+
echo "error: upstream path is not a directory: $UPSTREAM" >&2
73+
exit 1
74+
fi
75+
if ! git -C "$UPSTREAM" rev-parse --git-dir >/dev/null 2>&1; then
76+
echo "error: upstream path is not a git repository: $UPSTREAM" >&2
77+
exit 1
78+
fi
79+
80+
# Rows look like:
81+
# | `@intx/inference` | `vendor/intx-inference/` | LGPL-2.1-only | `SHA` | 2026-08-08 | Yes — see … |
82+
# Capture: name, vendor path, sha, local-patches cell.
83+
mapfile -t ROWS < <(
84+
awk -F'|' '
85+
/^\| `@intx\// {
86+
name=$2; vendor=$3; sha=$5; patches=$7
87+
gsub(/^ +| +$/, "", name)
88+
gsub(/^ +| +$/, "", vendor)
89+
gsub(/^ +| +$/, "", sha)
90+
gsub(/^ +| +$/, "", patches)
91+
gsub(/`/, "", name)
92+
gsub(/`/, "", vendor)
93+
gsub(/`/, "", sha)
94+
gsub(/\/$/, "", vendor)
95+
print name "\t" vendor "\t" sha "\t" patches
96+
}
97+
' "$VENDORING"
98+
)
99+
100+
if [[ ${#ROWS[@]} -eq 0 ]]; then
101+
echo "error: no vendored packages parsed from $VENDORING" >&2
102+
exit 1
103+
fi
104+
105+
normalize_pkg() {
106+
local raw="$1"
107+
raw="${raw#@intx/}"
108+
raw="${raw#vendor/}"
109+
raw="${raw%/}"
110+
if [[ "$raw" == intx-* ]]; then
111+
echo "$raw"
112+
else
113+
echo "intx-$raw"
114+
fi
115+
}
116+
117+
# Build the work list: either explicit packages, or every "Yes" local-patches row.
118+
declare -a WORK_NAMES=()
119+
declare -a WORK_VENDORS=()
120+
declare -a WORK_SHAS=()
121+
122+
if [[ ${#PACKAGES[@]} -eq 0 ]]; then
123+
for row in "${ROWS[@]}"; do
124+
IFS=$'\t' read -r name vendor sha patches <<<"$row"
125+
if [[ "$patches" == Yes* ]]; then
126+
WORK_NAMES+=("$name")
127+
WORK_VENDORS+=("$vendor")
128+
WORK_SHAS+=("$sha")
129+
fi
130+
done
131+
else
132+
for want in "${PACKAGES[@]}"; do
133+
want_norm="$(normalize_pkg "$want")"
134+
found=0
135+
for row in "${ROWS[@]}"; do
136+
IFS=$'\t' read -r name vendor sha patches <<<"$row"
137+
vendor_base="${vendor#vendor/}"
138+
if [[ "$(normalize_pkg "$name")" == "$want_norm" || "$vendor_base" == "$want_norm" ]]; then
139+
WORK_NAMES+=("$name")
140+
WORK_VENDORS+=("$vendor")
141+
WORK_SHAS+=("$sha")
142+
found=1
143+
break
144+
fi
145+
done
146+
if [[ $found -eq 0 ]]; then
147+
echo "error: package not listed in $VENDORING: $want" >&2
148+
exit 1
149+
fi
150+
done
151+
fi
152+
153+
if [[ ${#WORK_NAMES[@]} -eq 0 ]]; then
154+
echo "error: no packages selected (no local-patches rows, or empty filter)" >&2
155+
exit 1
156+
fi
157+
158+
# Map @intx/<x> → packages/<x> in the upstream monorepo.
159+
upstream_pkg_path() {
160+
local name="$1"
161+
echo "packages/${name#@intx/}"
162+
}
163+
164+
TMP="$(mktemp -d "${TMPDIR:-/tmp}/vendor-patch-diff.XXXXXX")"
165+
cleanup() { rm -rf "$TMP"; }
166+
trap cleanup EXIT
167+
168+
OVERALL=0
169+
170+
for i in "${!WORK_NAMES[@]}"; do
171+
name="${WORK_NAMES[$i]}"
172+
vendor="${WORK_VENDORS[$i]}"
173+
sha="${WORK_SHAS[$i]}"
174+
up_path="$(upstream_pkg_path "$name")"
175+
vendor_src="$REPO_ROOT/$vendor/src"
176+
177+
if [[ ! -d "$vendor_src" ]]; then
178+
echo "error: missing vendored src: $vendor_src" >&2
179+
exit 1
180+
fi
181+
182+
if ! git -C "$UPSTREAM" cat-file -e "${sha}^{commit}" 2>/dev/null; then
183+
echo "error: upstream $UPSTREAM has no commit $sha" >&2
184+
echo "Fetch that commit into the clone, then re-run." >&2
185+
exit 1
186+
fi
187+
188+
if ! git -C "$UPSTREAM" cat-file -e "${sha}:${up_path}" 2>/dev/null; then
189+
echo "error: $up_path does not exist at $sha in $UPSTREAM" >&2
190+
exit 1
191+
fi
192+
193+
pristine="$TMP/pristine-$i"
194+
mkdir -p "$pristine"
195+
# Extract only the package tree at the recorded SHA — read-only on upstream.
196+
git -C "$UPSTREAM" archive "$sha" "$up_path" | tar -x -C "$pristine"
197+
198+
pristine_src="$pristine/$up_path/src"
199+
if [[ ! -d "$pristine_src" ]]; then
200+
echo "error: archived tree has no src/: $pristine_src" >&2
201+
exit 1
202+
fi
203+
204+
echo "### $name upstream=$sha vendor=$vendor"
205+
echo "# pristine: $up_path/src vs $vendor/src"
206+
echo
207+
208+
# Unified diff of source only. diff exits 1 on differences — that is success
209+
# for this tool (we expect patches). Exit 2 is a real error.
210+
set +e
211+
diff -ruN "$pristine_src" "$vendor_src"
212+
code=$?
213+
set -e
214+
if [[ $code -eq 0 ]]; then
215+
echo "(no source differences)"
216+
elif [[ $code -eq 1 ]]; then
217+
OVERALL=1
218+
else
219+
echo "error: diff failed for $name (exit $code)" >&2
220+
exit 1
221+
fi
222+
echo
223+
done
224+
225+
# Exit 0 when there are differences (the usual patched case) so pipelines
226+
# treat a successful run as success. Exit 3 when everything is verbatim —
227+
# surprising for a "show our patches" tool, so callers can detect it.
228+
if [[ $OVERALL -eq 0 ]]; then
229+
echo "note: no local source patches found for selected packages." >&2
230+
exit 3
231+
fi
232+
exit 0

docs/VENDORING.md

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,14 @@ paths will show 100% upstream-authored lines.
116116

117117
`@intx/inference` carries local patches — real fixes not yet present
118118
upstream, not workarounds for something upstream has since fixed. Every
119-
patched location carries a one-line comment naming its entry in
120-
`vendor/intx-inference/PATCHES.md`, so `grep -rn "Locally patched" vendor/intx-inference/src`
121-
finds every divergence, and a diff against a fresh upstream checkout at the
122-
same commit should show ONLY those marked lines changed.
119+
patched location carries a one-line comment naming its site-specific entry
120+
in `vendor/intx-inference/PATCHES.md` (e.g. `#reactor-ts-correlating-ids-leak`),
121+
so `grep -rn "Locally patched" vendor/intx-inference/src` finds every
122+
divergence. **Markers are navigation; the SHA-diff is proof.** Run
123+
`bin/vendor-patch-diff` against a pristine upstream checkout at the
124+
recorded SHA to print exactly the lines that are ours. A correspondence
125+
test (`tests/unit/vendor-patch-ledger.test.ts`) fails if a marker anchor
126+
does not resolve to a ledger heading, or if a ledger heading has no marker.
123127

124128
## Re-syncing a vendored package to a newer upstream commit
125129

@@ -132,19 +136,21 @@ same commit should show ONLY those marked lines changed.
132136
diff the two `package.json` files by hand). Run `bun install`,
133137
`bun run typecheck`, `bun run build`, `bun run test`.
134138
3. For a **patched** package (`@intx/inference`): before overwriting
135-
anything, diff the current vendored `src/` against the upstream tag or
136-
commit it was last synced from, to re-derive the exact patch content (do
137-
not trust `PATCHES.md`'s prose alone — diff the code). Then overwrite
138-
`src/` with the new upstream commit's source, and re-apply each patch
139-
from the ledger by hand against the new file shapes. For each patch,
140-
confirm from the new upstream source whether it: (a) still applies
141-
as-is, (b) needs adapting to a changed surrounding shape, or (c) has been
142-
subsumed by an equivalent upstream fix and can be dropped — verify (c) by
143-
reading the new upstream code, never by assumption. Update
144-
`PATCHES.md` to reflect what actually landed, including any patches
145-
dropped as superseded and why. Run the full gate
146-
(`typecheck`/`build`/`test`) and do not consider the sync complete until
147-
it passes clean.
139+
anything, run `bin/vendor-patch-diff` (optionally
140+
`--upstream /path/to/interchange`) to re-derive the exact local
141+
divergences against the recorded SHA — do not trust `PATCHES.md`'s
142+
prose alone. Then overwrite `src/` with the new upstream commit's
143+
source, and re-apply each patch from the ledger by hand against the
144+
new file shapes. For each patch, confirm from the new upstream source
145+
whether it: (a) still applies as-is, (b) needs adapting to a changed
146+
surrounding shape, or (c) has been subsumed by an equivalent upstream
147+
fix and can be dropped — verify (c) by reading the new upstream code,
148+
never by assumption. Update `PATCHES.md` and the site-specific
149+
`Locally patched` markers to reflect what actually landed, including
150+
any patches dropped as superseded and why. Run the full gate
151+
(`typecheck`/`build`/`test`, including
152+
`tests/unit/vendor-patch-ledger.test.ts`) and do not consider the sync
153+
complete until it passes clean.
148154
4. Because `@intx/inference`, `@intx/types`, and `@intx/storage-isogit` are
149155
coupled (see above), a re-sync that moves any one of their commit hashes
150156
should move all three together, even if only one had code changes worth

0 commit comments

Comments
 (0)