Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/fork-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ jobs:
git sparse-checkout set --no-cone '/*' '!/.repos/'
git -c checkout.workers=0 checkout -q --detach "$GITHUB_SHA"

- name: Test the release repair script
run: bash fork/resolve-merge.test.sh

- name: Setup Vite+
uses: voidzero-dev/setup-vp@250f29ce396baf5e8f24498e17c0dfdebabc26eb # v1
with:
Expand Down
7 changes: 5 additions & 2 deletions FORK.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,11 @@ side calls what the other renamed) or broken on `collin/main` itself. Fixes go i
only. It needs the repository secret `CLAUDE_CODE_OAUTH_TOKEN` (from `claude setup-token`; bills
the subscription's monthly Agent SDK credit) or `ANTHROPIC_API_KEY` (Console API credits; wins
when both are set). Each fork/upstream pair is checked once and cached, so retries cost nothing.
When Claude cannot make it typecheck, the run fails: sync `collin/main` by hand, then
`gh workflow run fork-release.yml` (`-f force=true` asks Claude again instead).
When Claude cannot make it typecheck, the run fails. The T3 scheduled task "Fork repair" looks for
a red `Fork release` or `Fork check` on `collin/main` every four hours, fixes the cause in a pull
request and merges it once `Fork check` is green; it needs the machine with T3 Code running. By
hand: sync `collin/main`, then `gh workflow run fork-release.yml` (`-f force=true` asks Claude
again instead).

Pull requests into `collin/main` and pushes to it run `.github/workflows/fork-check.yml` (frozen
install and the same typecheck). Upstream's CI needs Blacksmith runners a fork does not have; its
Expand Down
32 changes: 27 additions & 5 deletions fork/resolve-merge.sh
Original file line number Diff line number Diff line change
Expand Up @@ -90,17 +90,34 @@ VP_GIT_HOOKS=0 vp install
# typecheck. Claude fixes what the typecheck reports, in the files it names. Same apps as the
# desktop build bundles.
broken=()
# The tracked files a typecheck log blames, from the repository root. tsc paths are relative to the
# app, and the layout depends on the tool and on whether it prints to a terminal:
# src/x.ts(12,5): error TS... or src/x.ts:12:5 - error TS...
# A layout nothing here knows must still reach Claude, so any path on an error line counts.
blamed_files() {
local app=$1 log_file=$2 text paths path
text=$(sed 's/\x1b\[[0-9;]*m//g' "$log_file")
paths=$(grep -oE '^[^ :(]+\.[cm]?tsx?(\([0-9]+,[0-9]+\):|:[0-9]+:[0-9]+ -) error' <<<"$text" |
sed -E 's/[(:].*$//' || true)
if [[ -z "$paths" ]]; then
paths=$(grep -E 'error TS[0-9]+' <<<"$text" | grep -oE '[^ :(]+\.[cm]?tsx?' || true)
fi
for path in $(sort -u <<<"$paths"); do
path=$(realpath -m --relative-to=. "apps/$app/$path")
if git ls-files --error-unmatch -- "$path" > /dev/null 2>&1; then
echo "$path"
fi
done
}

typecheck() {
local app status=0
broken=()
: > "$log"
for app in server desktop web; do
if ! (cd "apps/$app" && vp run typecheck) > "$log.$app" 2>&1; then
status=1
# tsc prints paths relative to the app: src/x.ts:12:5 - error TS...
mapfile -t -O "${#broken[@]}" broken < <(sed 's/\x1b\[[0-9;]*m//g' "$log.$app" |
grep -oE '^[^ :(]+\.[cm]?tsx?:[0-9]+:[0-9]+ - error' |
cut -d: -f1 | sort -u | sed "s|^|apps/$app/|")
mapfile -t -O "${#broken[@]}" broken < <(blamed_files "$app" "$log.$app")
fi
cat "$log.$app" >> "$log"
done
Expand Down Expand Up @@ -149,7 +166,12 @@ if ! typecheck; then
exit 1
fi

git add -A -- "${files[@]}" "${fixable[@]}"
# git rm already staged a deleted conflict file, and git add refuses a path that is gone.
for file in "${files[@]}" "${fixable[@]}"; do
if [[ -e "$file" ]] || git ls-files --error-unmatch -- "$file" > /dev/null 2>&1; then
git add -A -- "$file"
fi
done
if git rev-parse -q --verify MERGE_HEAD > /dev/null; then
git commit -q --no-edit
elif ! git diff --cached --quiet; then
Expand Down
89 changes: 89 additions & 0 deletions fork/resolve-merge.test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/usr/bin/env bash
# Runs fork/resolve-merge.sh against throwaway repositories with stubbed vp and claude. Its repair
# path only runs when a release is already broken, so a regression there stays silent until then.
set -euo pipefail

script=$(realpath "$(dirname "$0")/resolve-merge.sh")
root=$(mktemp -d)
trap 'rm -rf "$root"' EXIT
failed=0

mkdir "$root/bin"
# Reports whatever $TSC_LINE says while the file it names (relative to the app) holds BROKEN.
cat > "$root/bin/vp" <<'STUB'
#!/usr/bin/env bash
[[ "$1" == run && "$(basename "$PWD")" == server ]] || exit 0
if grep -rqs BROKEN src ../../packages; then
printf '~/apps/server$ tsc --noEmit\n%b\n' "$TSC_LINE"
exit 2
fi
STUB
cat > "$root/bin/claude" <<'STUB'
#!/usr/bin/env bash
for file in $(git diff --name-only --diff-filter=U); do git rm -q -f -- "$file"; done
grep -rl BROKEN apps packages | xargs -r sed -i 's/BROKEN/fixed/'
STUB
chmod +x "$root/bin/vp" "$root/bin/claude"
export PATH="$root/bin:$PATH" CLAUDE_CODE_OAUTH_TOKEN=test

new_repo() {
git init -q -b base "$1"
git -C "$1" config user.name test
git -C "$1" config user.email test@example.com
mkdir -p "$1"/apps/{server,desktop,web}/src "$1/packages/shared/src"
touch "$1/pnpm-lock.yaml" "$1"/apps/{server,desktop,web}/src/keep.ts
}

report() {
if [[ "$2" == 0 ]]; then
echo "ok: $1"
else
echo "FAILED: $1"
cat "$root/output" || true
failed=1
fi
}

check_repair() {
local name=$1 path=$2 line=$3 repo status=0
repo="$root/repo$RANDOM"
new_repo "$repo"
echo "const x = BROKEN;" > "$repo/$path"
git -C "$repo" add -A
git -C "$repo" commit -qm base
(cd "$repo" && RUNNER_TEMP="$root" TSC_LINE="$line" bash "$script") > "$root/output" 2>&1 || status=1
if [[ $status == 0 && "$(git -C "$repo" show "HEAD:$path")" != "const x = fixed;" ]]; then
status=1
fi
grep -qxF "Typecheck round 1 failed in: $path" "$root/output" || status=1
report "$name" "$status"
}

# Names a second file, so only the path at the start of the line may count.
error="error TS2304: Cannot find name 'BROKEN' from src/keep.ts."
check_repair "parenthesized errors reach Claude" apps/server/src/a.ts "src/a.ts(1,11): $error"
check_repair "pretty errors reach Claude" apps/server/src/a.ts "\033[96msrc/a.ts\033[0m:1:11 - $error"
check_repair "errors in a package reach Claude" packages/shared/src/a.ts "../../packages/shared/src/a.ts(1,11): $error"
check_repair "an unknown layout still reaches Claude" apps/server/src/a.ts "error TS2304 in src/a.ts"

# A modify/delete conflict Claude settles with git rm must not break the commit.
repo="$root/deleted"
new_repo "$repo"
echo one > "$repo/apps/web/src/gone.ts"
git -C "$repo" add -A
git -C "$repo" commit -qm base
git -C "$repo" checkout -qb upstream
git -C "$repo" rm -q apps/web/src/gone.ts
git -C "$repo" commit -qm "upstream deletes"
git -C "$repo" checkout -q base
echo two > "$repo/apps/web/src/gone.ts"
git -C "$repo" commit -qam "fork edits"
git -C "$repo" merge -q --no-commit --no-ff upstream > /dev/null 2>&1 || true
status=0
(cd "$repo" && RUNNER_TEMP="$root" TSC_LINE="" bash "$script") > "$root/output" 2>&1 || status=1
if [[ $status == 0 && "$(git -C "$repo" rev-list --parents -n 1 HEAD | wc -w)" != 3 ]]; then
status=1
fi
report "a conflict settled with git rm commits the merge" "$status"

exit "$failed"
Loading