Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
285cf59
fix(web): preserve desktop route during Clerk auth (#5770)
wobsoriano Aug 9, 2026
02f4ce5
fix(web): match create theme and import theme buttons to the standard…
UtkarshUsername Aug 9, 2026
deb901d
fix(server): favicon resolution no longer pins the event loop (#5538)
murenovich Aug 9, 2026
062b461
fix(shared): bound the file-link label so bracket runs stop rescannin…
tsouth89 Aug 9, 2026
659986c
fix(web): thread title button no longer eats the drag area (#5857)
nathangerday Aug 9, 2026
bd18d8d
fix(web): unify usage page chrome (#5823)
t3-code[bot] Aug 9, 2026
c8ad4b8
fix(shell): add ~/.local/bin to the Windows CLI resolver so native-in…
arhxam Aug 9, 2026
83d769f
fix(web): match settings search shortcut styling to command palette's…
UtkarshUsername Aug 9, 2026
f993fa1
fix(mobile): long-pressing a thread row no longer navigates into the …
juliusmarminge Aug 9, 2026
0d38866
fix(server): usage no longer double-counts forked Codex sessions (#5887)
t3dotgg Aug 9, 2026
2abe668
fix(server): sandbox user-provided SVGs (#5916)
t3dotgg Aug 9, 2026
9a1472d
fix(web): match usage titlebar text styling (#5897)
t3-code[bot] Aug 10, 2026
f21d5e4
Move project settings to contextual project routes (#5923)
juliusmarminge Aug 10, 2026
5da4533
Preserve back navigation when opening settings (#5930)
juliusmarminge Aug 10, 2026
73b2e8f
Automate production mobile EAS releases (#5609)
juliusmarminge Aug 10, 2026
9690680
Add settings and usage breadcrumbs (#5929)
juliusmarminge Aug 10, 2026
cbd55d6
fix(web): correct model picker trigger padding (#5935)
Chrono-byte Aug 10, 2026
0ca9fb3
fix(web): show worktree icon in sidebar v2 (#5909)
tris203 Aug 10, 2026
ef051bd
fix(web): enable restore defaults after theme mix changes (#5928)
Lucenx9 Aug 10, 2026
d432100
fix(web): trait menu closes after you pick a level (#5879)
t3dotgg Aug 10, 2026
f0e2975
fix(web): align project name with headline (#5864)
carterwsmith Aug 10, 2026
3d74474
fix(web): update pills use readable theme foregrounds (#5938)
chrisdeeming Aug 10, 2026
9821bca
fix(web): use themed confirmation dialogs (#5624)
StiensWout Aug 10, 2026
fbcae59
fix(web): use import/export-appropriate icons for theme buttons (#5964)
UtkarshUsername Aug 10, 2026
67ef189
fix(mobile): detect PowerShell cmdlet errors in work log rows (#5726)
myacoub91 Aug 10, 2026
d440442
fix(mobile): stop Android user bubbles with code blocks from overlapp…
Brechard Aug 10, 2026
3b72d17
fix(mobile): parse EAS fingerprint JSON (#5991)
juliusmarminge Aug 10, 2026
78f462c
chore(release): prepare v0.0.33
t3-code[bot] Aug 10, 2026
e7c3fdd
chore(upstream): merge pingdotgg/t3code@78f462c4e18c
claude Aug 10, 2026
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
108 changes: 103 additions & 5 deletions .github/workflows/mobile-eas-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,23 @@ name: Mobile EAS Production
# in the same OS/pnpm as the EAS build; a macOS `eas build` computes a different
# fingerprint (platform-specific deps + pnpm version) and errors. On this Linux
# runner, with corepack pinning pnpm 10.24 in eas.json, local == build.
#
# Every merge to main that touches the mobile app reconciles, per platform:
# 1. Store builds: if the latest production build's version differs from
# app.config.ts, cut a new build with --auto-submit (TestFlight +
# Play internal track). Bumping `version` is therefore all it takes to
# start the next release train — the first build of a version enters
# external-TestFlight beta review immediately, and later builds of the
# same version auto-approve. Releasing to the App Store stays a manual
# App Store Connect step.
# 2. OTA: publish a production-channel update for each platform where at
# least one finished production build matches the current native
# fingerprint. Old-version binaries with a matching fingerprint receive
# it too. When native drift means no binary could install the update,
# it is skipped and flagged in the job summary instead of published
# into the void.
# workflow_dispatch remains as a manual override for both modes (e.g. to
# retry an errored build or force an OTA).
on:
workflow_dispatch:
inputs:
Expand All @@ -29,10 +46,30 @@ on:
description: "OTA update message (mode=update only)"
required: false
type: string
push:
branches: [main]
paths:
- apps/mobile/**
- packages/client-runtime/**
- packages/contracts/**
- packages/shared/**
- assets/**
- scripts/**
- patches/**
- pnpm-lock.yaml
- pnpm-workspace.yaml
- .github/workflows/mobile-eas-production.yml

# Serialize runs so OTAs publish in merge order. GitHub keeps at most one
# queued run per group, so a burst of merges collapses into one run of the
# newest commit — intermediate commits don't need their own OTA.
concurrency:
group: mobile-eas-production
cancel-in-progress: false

jobs:
production:
name: EAS Production ${{ inputs.mode }}
name: EAS Production ${{ github.event_name == 'push' && 'auto' || inputs.mode }}
runs-on: blacksmith-8vcpu-ubuntu-2404
permissions:
contents: read
Expand Down Expand Up @@ -98,15 +135,15 @@ jobs:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
run: eas env:pull production --non-interactive

- name: Build and submit
if: steps.expo-token.outputs.present == 'true' && inputs.mode == 'build'
- name: Build and submit (manual)
if: steps.expo-token.outputs.present == 'true' && github.event_name == 'workflow_dispatch' && inputs.mode == 'build'
working-directory: apps/mobile
env:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
run: eas build --platform ${{ inputs.platform }} --profile production --auto-submit --non-interactive --no-wait

- name: Publish OTA update
if: steps.expo-token.outputs.present == 'true' && inputs.mode == 'update'
- name: Publish OTA update (manual)
if: steps.expo-token.outputs.present == 'true' && github.event_name == 'workflow_dispatch' && inputs.mode == 'update'
working-directory: apps/mobile
env:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
Expand All @@ -117,3 +154,64 @@ jobs:
--platform ${{ inputs.platform }} \
--message "${{ inputs.message || format('Production OTA ({0})', github.sha) }}" \
--non-interactive

# No --status filter on build:list: an in-queue/in-progress build must
# count as existing, or every merge during the build window would cut a
# duplicate. Builds started here stay attached to this serialized run so
# the queued run for a later merge cannot overtake them and lose its OTA.
# After an errored build, retry via workflow_dispatch mode=build — pushes
# won't re-trigger it until the app version changes.
- id: store_builds
name: Ensure store builds exist for the current app version
if: steps.expo-token.outputs.present == 'true' && github.event_name == 'push'
continue-on-error: true
working-directory: apps/mobile
env:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
run: |
failed=0
version="$(npx expo config --json --type public | jq -r '.version')"
for platform in ios android; do
latest="$(eas build:list --platform "$platform" --build-profile production --limit 1 --json --non-interactive | jq -r '.[0].appVersion // "none"')"
if [ "$latest" = "$version" ]; then
echo "$platform: production build for $version already exists (or is in progress)"
continue
fi
echo "$platform: latest production build is $latest, app.config.ts says $version — building"
if eas build --platform "$platform" --profile production --auto-submit --non-interactive; then
echo ":building_construction: $platform: cut production build for $version (auto-submitted)" >> "$GITHUB_STEP_SUMMARY"
else
failed=1
echo ":x: $platform: production build or submission failed for $version" >> "$GITHUB_STEP_SUMMARY"
fi
done
exit "$failed"

- name: Publish fingerprint-gated OTA
if: steps.expo-token.outputs.present == 'true' && github.event_name == 'push'
working-directory: apps/mobile
env:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
run: |
message="$(git log -1 --pretty=%s | head -c 120) ($(git rev-parse --short=9 HEAD))"
for platform in ios android; do
# eas-cli prints an environment-loaded notice to stdout before the
# JSON even with --json, so discard everything before the document.
hash="$(eas fingerprint:generate --platform "$platform" --environment production --json --non-interactive | sed -n '/^{/,$p' | jq -er '.hash | select(type == "string" and length > 0)')"
matching="$(eas build:list --platform "$platform" --build-profile production --status finished --fingerprint-hash "$hash" --limit 1 --json --non-interactive | jq 'length')"
if [ "$matching" -gt 0 ]; then
eas update \
--channel production \
--environment production \
--platform "$platform" \
--message "$message" \
--non-interactive
echo ":white_check_mark: $platform: OTA published to production (fingerprint \`$hash\`)" >> "$GITHUB_STEP_SUMMARY"
else
echo ":warning: $platform: no finished production build matches fingerprint \`$hash\` — OTA skipped; JS changes reach $platform only once a matching build ships" >> "$GITHUB_STEP_SUMMARY"
fi
done

- name: Propagate store build failure
if: steps.store_builds.outcome == 'failure'
run: exit 1
205 changes: 205 additions & 0 deletions .github/workflows/mobile-fingerprint-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
name: Mobile Fingerprint Check

# Detects whether a PR changes the native fingerprint — i.e. whether merging
# it would leave main un-OTA-able until a new store build ships. Native-change
# PRs get the "📱 Native Change" label so they can be held and merged as a
# batch right before the next store submission, keeping main OTA-able for
# everything else in between. (Once one native PR merges, every later merge
# inherits the drifted fingerprint and loses OTA reach too — that is why the
# signal has to fire before merge, not after.)
#
# The check is advisory: it always passes, the label is the signal. Both
# fingerprints are computed in this one job (same OS, same corepack-pinned
# pnpm), so the comparison is self-consistent; no EXPO_TOKEN needed.
on:
pull_request:
paths:
- apps/mobile/**
- packages/client-runtime/**
- packages/contracts/**
- packages/shared/**
- assets/**
- scripts/**
- patches/**
- pnpm-lock.yaml
- pnpm-workspace.yaml
- .github/workflows/mobile-fingerprint-check.yml

concurrency:
group: mobile-fingerprint-check-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
fingerprint:
name: Native fingerprint diff
runs-on: blacksmith-8vcpu-ubuntu-2404
permissions:
contents: read
issues: write
pull-requests: write
env:
APP_VARIANT: production
NODE_OPTIONS: --max-old-space-size=8192
steps:
- name: Checkout
uses: actions/checkout@v6
with:
# Default pull_request checkout is the merge commit (PR applied on
# top of base), so the "head" fingerprint is the state main would
# actually be in after merging — stale branches compare cleanly.
fetch-depth: 0

- name: Setup Vite+
uses: voidzero-dev/setup-vp@v1
with:
node-version-file: package.json
cache: true
run-install: |
args:
- --filter=@t3tools/mobile...

- name: Expose pnpm
run: |
pnpm_version="$(node --print "require('./package.json').packageManager.split('@').pop()")"
vp_pnpm_bin="$HOME/.vite-plus/package_manager/pnpm/$pnpm_version/pnpm/bin"
echo "$vp_pnpm_bin" >> "$GITHUB_PATH"
"$vp_pnpm_bin/pnpm" --version

- name: Fingerprint merge result
working-directory: apps/mobile
run: |
mkdir -p "$RUNNER_TEMP/fp/head" "$RUNNER_TEMP/fp/base"
for platform in ios android; do
npx expo-updates fingerprint:generate --platform "$platform" > "$RUNNER_TEMP/fp/head/$platform.json"
done

- name: Fingerprint base
run: |
git checkout --quiet "${{ github.event.pull_request.base.sha }}"
# Re-sync node_modules to the base commit's lockfile before
# fingerprinting — a dep-changing PR must not fingerprint the base
# against head's installed packages.
pnpm install --filter=@t3tools/mobile...
cd apps/mobile
for platform in ios android; do
npx expo-updates fingerprint:generate --platform "$platform" > "$RUNNER_TEMP/fp/base/$platform.json"
done

- id: compare
name: Compare fingerprints
run: |
changed=""
{
echo "## Native fingerprint diff"
echo
for platform in ios android; do
head_hash="$(jq -r .hash "$RUNNER_TEMP/fp/head/$platform.json")"
base_hash="$(jq -r .hash "$RUNNER_TEMP/fp/base/$platform.json")"
if [ "$head_hash" = "$base_hash" ]; then
echo "- ✅ **$platform**: unchanged (\`$head_hash\`) — OTA-compatible"
continue
fi
changed="$changed $platform"
echo "- 📱 **$platform**: \`$base_hash\` → \`$head_hash\` — merging requires a new native build before OTAs work again"
jq -r -n \
--slurpfile h "$RUNNER_TEMP/fp/head/$platform.json" \
--slurpfile b "$RUNNER_TEMP/fp/base/$platform.json" '
($b[0].sources | map({ (.filePath // .id): .hash }) | add // {}) as $bm
| $h[0].sources[]
| select($bm[(.filePath // .id)] != .hash)
| " - \(.type): `\(.filePath // .id)`"'
done
} >> "$GITHUB_STEP_SUMMARY"
echo "changed_platforms=${changed# }" >> "$GITHUB_OUTPUT"

- name: Sync native change label
# Fork PRs get a read-only token under pull_request; the check stays
# advisory there (summary only). This workflow must not move to
# pull_request_target — it installs and runs PR code.
if: github.event.pull_request.head.repo.full_name == github.repository
uses: actions/github-script@v8
env:
CHANGED_PLATFORMS: ${{ steps.compare.outputs.changed_platforms }}
with:
script: |
const managedLabel = {
name: "📱 Native Change",
color: "d93f0b",
description:
"Changes the native fingerprint; merging blocks production OTAs until a new store build ships.",
};
const nativeChanged = (process.env.CHANGED_PLATFORMS ?? "").trim() !== "";
const issueNumber = context.payload.pull_request.number;

try {
const { data: existing } = await github.rest.issues.getLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name: managedLabel.name,
});

if (
existing.color !== managedLabel.color ||
(existing.description ?? "") !== managedLabel.description
) {
await github.rest.issues.updateLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name: managedLabel.name,
color: managedLabel.color,
description: managedLabel.description,
});
}
} catch (error) {
if (error.status !== 404) {
throw error;
}

try {
await github.rest.issues.createLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name: managedLabel.name,
color: managedLabel.color,
description: managedLabel.description,
});
} catch (createError) {
if (createError.status !== 422) {
throw createError;
}
}
}

const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
per_page: 100,
});
const hasLabel = currentLabels.some((label) => label.name === managedLabel.name);

if (nativeChanged && !hasLabel) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
labels: [managedLabel.name],
});
} else if (!nativeChanged && hasLabel) {
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
name: managedLabel.name,
});
} catch (removeError) {
if (removeError.status !== 404) {
throw removeError;
}
}
}

core.info(
`PR #${issueNumber}: native fingerprint ${nativeChanged ? `changed (${process.env.CHANGED_PLATFORMS})` : "unchanged"}`,
);
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@t3tools/desktop",
"version": "0.0.32",
"version": "0.0.33",
"private": true,
"type": "module",
"main": "dist-electron/main.cjs",
Expand Down
Loading
Loading