diff --git a/.agents/skills/trogonstack-sync-upstream/SKILL.md b/.agents/skills/trogonstack-sync-upstream/SKILL.md index a2c99ae0b21f..ff898c44c5d0 100644 --- a/.agents/skills/trogonstack-sync-upstream/SKILL.md +++ b/.agents/skills/trogonstack-sync-upstream/SKILL.md @@ -7,6 +7,8 @@ description: Merge the latest pingdotgg/t3code upstream main into this TrogonSta This fork tracks [pingdotgg/t3code](https://github.com/pingdotgg/t3code) as the `upstream` remote and carries a small set of intentional divergences. Syncing is always a **merge**, never a rebase: rebasing would rewrite fork history and break published PR references. +**The goal is to stay in sync with upstream.** Every fork-only behavior is a standing cost: it conflicts on every sync and drifts further over time. A ledger entry in `docs/fork/` is the strong reason to keep a divergence. Fork-only code without one is not automatically wrong, so never roll it back wholesale or by reflex. Compare it against upstream's version, then decide and say why: take upstream's, keep the fork's, or merge the two. Default to upstream when the behavior is equivalent or the fork's gain is marginal. Keep the fork's when it is clearly better, and then add a ledger entry for it or propose the change upstream. List each such comparison in the PR body. + ## 1. Prepare Every sync runs in its own linked worktree, never in the main checkout. The main checkout is often serving the developer's live T3 Code session and carries their uncommitted work; a sync installs dependencies, rewrites the lockfile, and runs the full suite, none of which should touch that. @@ -39,11 +41,22 @@ Default to upstream's shape and re-apply the fork's intent on top of it. Concret - If the conflicting area belongs to a ledger divergence, keep the fork behavior and adapt it to upstream's new structure. Do not silently drop a divergence. - If the fork's version of something is a superset of what upstream added, keep the fork's version and skip the redundant upstream piece (this happened in PR #9 with bootstrap-dispatch logic already covered by the fork's `ThreadBootstrapService`). - If upstream merged an equivalent of a fork divergence, take upstream's version and delete the corresponding ledger entry outright (numbers are never reused; git history is the record). -- Everything else: take upstream. +- Everything else: take upstream. Fork-only code with no ledger entry gets a short comparison first (what each side does and what the fork gains, if anything). If upstream does the same job, or the fork's gain is marginal, converge on upstream. If the fork's version is clearly better, keep it and either add a ledger entry or propose it upstream. Do not revert it without that comparison, and surface the call in the PR body. - For conflicts under `.repos/` (vendored subtrees), do not hand-merge; take either side to complete the merge, then regenerate with `vpr sync:repos` (or `vpr sync:repos --repo `) so `.repos/` matches the installed dependency versions. Record every nontrivial resolution decision as you go; each one becomes a PR body bullet. +### Keep the ledger honest + +The ledger drifts when divergences ship without an entry and when upstream catches up without anyone deleting one. Every sync reconciles it, in the same PR, so the decision is written down while the context is fresh: + +- Fork-only behavior you keep that has no entry: add one (product focused, per `docs/fork/README.md`), including why the fork's version is worth the sync cost. +- An entry whose divergence upstream now covers, or that the sync reverted: delete it. +- An entry whose reason no longer holds, or that no longer matches the code: rewrite or delete it. +- Fork-only code you reverted to upstream's version: nothing to add to the ledger, but the PR body states what the fork did, what upstream does, and why converging was right. + +If you cannot tell why a divergence exists, say so in the PR body and ask rather than guessing in either direction. + ### Generated files - `apps/web/src/routeTree.gen.ts` is written by the TanStack router plugin whenever Vite runs, including the test suite in step 4. Accept git's merge result for it, and if the verification run rewrites it, commit the rewritten file. Never resolve it by hand and never restore the fork's copy to quiet a diff: the merge base then carries the stale content and every later sync keeps it, which is how the generator header went missing for a week. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eef33e1774d4..8699200e991c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,7 +92,7 @@ jobs: # electron's install.js (run by ensure:electron) reads this cache before it # downloads from GitHub. The key changes only with the Electron version. - name: Cache Electron download - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.cache/electron key: electron-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('node_modules/.pnpm/electron@*/node_modules/electron/package.json') }} @@ -140,10 +140,16 @@ jobs: - name: Start installing browser secret helper build libraries if: needs.changes.outputs.build != 'false' run: | - sudo sed -i 's|http://|https://|g' /etc/apt/ubuntu-mirrors.txt /etc/apt/sources.list.d/ubuntu.sources + # Fetch only the indexes these main and universe packages need. The full set is ~42 MB, + # which a slow mirror can stretch past the wait below. + sudo sed -i -e 's|http://|https://|g' -e 's/^Components: .*/Components: main universe/' \ + /etc/apt/ubuntu-mirrors.txt /etc/apt/sources.list.d/ubuntu.sources ( set +e - sudo apt-get update && sudo apt-get install -y libsecret-1-dev pkg-config xvfb + sudo apt-get update -o Acquire::Languages=none \ + -o Acquire::IndexTargets::deb::DEP-11::DefaultEnabled=false \ + -o Acquire::IndexTargets::deb::CNF::DefaultEnabled=false && + sudo apt-get install -y libsecret-1-dev pkg-config xvfb echo "$?" > "$RUNNER_TEMP/apt-status" ) > "$RUNNER_TEMP/apt.log" 2>&1 < /dev/null & @@ -155,7 +161,7 @@ jobs: # electron's install.js (run by ensure:electron) reads this cache before it # downloads from GitHub. The key changes only with the Electron version. - name: Cache Electron download - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.cache/electron key: electron-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('node_modules/.pnpm/electron@*/node_modules/electron/package.json') }} @@ -287,10 +293,16 @@ jobs: # the background while Vite+ installs; the step before the tests waits for it. - name: Start installing browser secret helper build libraries run: | - sudo sed -i 's|http://|https://|g' /etc/apt/ubuntu-mirrors.txt /etc/apt/sources.list.d/ubuntu.sources + # Fetch only the indexes these main and universe packages need. The full set is ~42 MB, + # which a slow mirror can stretch past the wait below. + sudo sed -i -e 's|http://|https://|g' -e 's/^Components: .*/Components: main universe/' \ + /etc/apt/ubuntu-mirrors.txt /etc/apt/sources.list.d/ubuntu.sources ( set +e - sudo apt-get update && sudo apt-get install -y libsecret-1-dev pkg-config + sudo apt-get update -o Acquire::Languages=none \ + -o Acquire::IndexTargets::deb::DEP-11::DefaultEnabled=false \ + -o Acquire::IndexTargets::deb::CNF::DefaultEnabled=false && + sudo apt-get install -y libsecret-1-dev pkg-config echo "$?" > "$RUNNER_TEMP/apt-status" ) > "$RUNNER_TEMP/apt.log" 2>&1 < /dev/null & @@ -302,7 +314,7 @@ jobs: # electron's install.js (run by ensure:electron) reads this cache before it # downloads from GitHub. The key changes only with the Electron version. - name: Cache Electron download - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.cache/electron key: electron-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('node_modules/.pnpm/electron@*/node_modules/electron/package.json') }} @@ -343,16 +355,22 @@ jobs: # The web suite is the largest outside the server. In the shared Test job it # started last and ran alone for about 40s after everything else finished. - # release.yml mirrors this test, test_web, and test_server split. + # Browser tests are bound by the Vite server transforming modules for each + # file, not by Chromium, so a second runner (and Vite server) halves the job + # that gated every run. release.yml mirrors this test, test_web, and test_server split. test_web: permissions: contents: read id-token: write - name: Test Web + name: Test Web ${{ matrix.shard }} needs: changes if: ${{ !cancelled() && needs.changes.outputs.test_web != 'false' }} runs-on: ubuntu-24.04 timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + shard: [1, 2] steps: - name: Checkout uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 @@ -394,7 +412,7 @@ jobs: fi - name: Test - run: vp run --filter @t3tools/web test + run: vp run --filter @t3tools/web test --shard ${{ matrix.shard }}/${{ strategy.job-total }} # apps/server sets `fileParallelism: false`, so its test files run strictly # one at a time. Sharding spreads them over separate runners instead of @@ -469,6 +487,17 @@ jobs: node apps/server/node_modules/playwright-core/cli.js install-deps chromium fi + # Each server test file runs in its own Vitest process, and a fresh install starts + # Vitest's content-keyed module cache empty, so every file re-transformed its imports. + # Only pushes to main save it, so pull requests never evict each other's entries. + - name: Restore Vitest module cache + if: steps.changes.outputs.test_server != 'false' + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: node_modules/.vitest-cache + key: vitest-modules-server-${{ matrix.shard }}-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}-${{ github.sha }} + restore-keys: vitest-modules-server-${{ matrix.shard }}-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}- + - name: Test if: steps.changes.outputs.test_server != 'false' env: @@ -487,6 +516,13 @@ jobs: path: ${{ runner.temp }}/test-cache-${{ matrix.shard }}.json if-no-files-found: error + - name: Save Vitest module cache + if: github.event_name == 'push' && steps.changes.outputs.test_server != 'false' + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: node_modules/.vitest-cache + key: vitest-modules-server-${{ matrix.shard }}-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}-${{ github.sha }} + # integration/transferBudgetV2.integration.test.ts writes the report; exactly one shard # produces these files. Gating the upload on their presence keeps a # single `thread-transfer-results` artifact per run, which is the name diff --git a/.github/workflows/mobile-eas-production.yml b/.github/workflows/mobile-eas-production.yml index f8c28ebac493..037ad81553bb 100644 --- a/.github/workflows/mobile-eas-production.yml +++ b/.github/workflows/mobile-eas-production.yml @@ -230,9 +230,7 @@ jobs: git config user.name "${APP_SLUG}[bot]" git config user.email "${user_id}+${APP_SLUG}[bot]@users.noreply.github.com" git add apps/mobile/app.config.ts - git commit --no-verify \ - -m "chore(mobile): bump app version to $RELEASE_VERSION" \ - -m "Co-authored-by: codex " + git commit --no-verify -m "chore(mobile): bump app version to $RELEASE_VERSION" git push origin "HEAD:refs/heads/${GITHUB_REF_NAME}" - name: Summarize manual build version @@ -505,8 +503,21 @@ jobs: with: ref: ${{ needs.production.outputs.android_sha }} fetch-depth: 0 - # Dependency scripts and the build run in this job; only Record build gets a token. - persist-credentials: false + + # Dependency scripts and the build run in this job; only Record build gets a token. + # persist-credentials: false would clean up via `git submodule foreach`, which fails on .repos gitlinks. + - name: Drop checkout credentials + run: | + git config --local --unset-all http.https://github.com/.extraheader || true + { git config --local --name-only --get-regexp '^includeif\.' || true; } | while read -r key; do + path=$(git config --local --get "$key") + git config --local --unset-all "$key" + case "$path" in "$RUNNER_TEMP"/*) rm -f "$path" ;; esac + done + if git config --local --get-regexp '^(http\..*\.extraheader|includeif\.)'; then + echo "::error::Checkout credentials are still configured" + exit 1 + fi # A queued run can follow a build of the same fingerprint; manual runs always build. - id: record @@ -622,6 +633,16 @@ jobs: exit 1 fi + # A --path submission doesn't keep its archive on EAS; this is the only downloadable copy. + - name: Keep the AAB + if: steps.record.outputs.built != 'true' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: android-store-aab-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ runner.temp }}/t3code.aab + retention-days: 14 + if-no-files-found: error + - name: Submit to Google Play if: steps.record.outputs.built != 'true' working-directory: apps/mobile @@ -682,8 +703,21 @@ jobs: with: ref: ${{ needs.production.outputs.ios_sha }} fetch-depth: 0 - # Dependency scripts and the build run in this job; only Record build gets a token. - persist-credentials: false + + # Dependency scripts and the build run in this job; only Record build gets a token. + # persist-credentials: false would clean up via `git submodule foreach`, which fails on .repos gitlinks. + - name: Drop checkout credentials + run: | + git config --local --unset-all http.https://github.com/.extraheader || true + { git config --local --name-only --get-regexp '^includeif\.' || true; } | while read -r key; do + path=$(git config --local --get "$key") + git config --local --unset-all "$key" + case "$path" in "$RUNNER_TEMP"/*) rm -f "$path" ;; esac + done + if git config --local --get-regexp '^(http\..*\.extraheader|includeif\.)'; then + echo "::error::Checkout credentials are still configured" + exit 1 + fi # A queued run can follow a build of the same fingerprint; manual runs always build. - id: record diff --git a/.github/workflows/mobile-showcase-screenshots.yml b/.github/workflows/mobile-showcase-screenshots.yml index f323041259f1..96a21bb9ee08 100644 --- a/.github/workflows/mobile-showcase-screenshots.yml +++ b/.github/workflows/mobile-showcase-screenshots.yml @@ -88,7 +88,7 @@ jobs: axe --version - name: Install foldable simulator controls - run: npm install --global agent-device@0.21.25 + run: npm install --global agent-device@0.21.36 - name: Capture iOS showcase run: pnpm screenshots:mobile --platform ios --appearance "${{ inputs.appearance }}" --theme "${{ inputs.theme }}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 38d5f2b1577d..dbf49780fc6c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -300,11 +300,15 @@ jobs: # Web browser tests time out when they share a runner with the other package # suites: Chromium gets too little CPU and diff workers miss their 10s start budget. test_web: - name: Release web tests + name: Release web tests ${{ matrix.shard }} needs: [preflight] if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' }} runs-on: ubuntu-24.04 timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + shard: [1, 2] steps: - name: Checkout uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 @@ -344,10 +348,11 @@ jobs: fi - name: Test - run: vp run --filter @t3tools/web test + run: vp run --filter @t3tools/web test --shard ${{ matrix.shard }}/${{ strategy.job-total }} # Browser tests run source modules; this boots the production chunk graph. - name: Boot the built web client + if: matrix.shard == 1 run: | vp run --filter @t3tools/web build node apps/web/scripts/verify-built-boot.ts @@ -1497,7 +1502,18 @@ jobs: git add apps/server/package.json apps/desktop/package.json apps/web/package.json packages/contracts/package.json pnpm-lock.yaml git commit --no-verify -m "chore(release): prepare $RELEASE_TAG" - git push origin HEAD:main + + # main moves while this job runs, so a rejected push rebases and retries. + for attempt in 1 2 3 4 5; do + if git push origin HEAD:main; then + exit 0 + fi + echo "Push rejected (attempt $attempt). Rebasing onto latest main." + git pull --rebase origin main + done + + echo "Could not push the version bump after 5 attempts." >&2 + exit 1 announce_discord: name: Announce release on Discord diff --git a/.gitignore b/.gitignore index 05ac43c32f1e..07ec2e549a7c 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ apps/mobile/.android-cache/ artifacts/app-store/screenshots/ artifacts/mobile-e2e/ artifacts/mobile-behavior/ +scripts/.e2e/ .github/pr-assets/ native/**/target/ node_modules/ diff --git a/.macroscope/check-run-agents/ui-consistency.md b/.macroscope/check-run-agents/ui-consistency.md index 905823d1d1a9..4326d4e8391b 100644 --- a/.macroscope/check-run-agents/ui-consistency.md +++ b/.macroscope/check-run-agents/ui-consistency.md @@ -44,7 +44,7 @@ The reference for how this codebase wants UI built is the composer banner system - `ComposerBanner.tsx` exports one object of small slot components (`Root`, `Row`, `Icon`, `Content`, `Actions`, `Dismiss`, `Children`, and so on). Each slot owns its own Tailwind classes, exposes a `data-slot` attribute, takes `className` and spreads the rest of its props, and uses `cn` so callers can adjust without overriding. Variants and density are props on `Root`, not ad hoc class strings at call sites. Where a slot needs a real control it renders `Button` from `ui/` rather than a styled `