Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
145 commits
Select commit Hold shift + click to select a range
237498c
fix(ci): Android merges stop cutting a new Play Store build every tim…
juliusmarminge Oct 28, 2026
886d8f6
fix(mobile): dictation confirm button no longer hugs the small compos…
juliusmarminge Oct 28, 2026
be6ff17
feat(devices): lease simulators and emulators from a Simlock pool (al…
juliusmarminge Oct 28, 2026
ba18706
fix(mobile): use a native split button for Android merges (#26373)
PixPMusic Oct 28, 2026
9e09926
docs: settings paths name the Environments and Sharing & access pages…
juliusmarminge Oct 28, 2026
9826dbb
docs(agents): agents verify in a browser or simulator without asking …
juliusmarminge Oct 28, 2026
5b89a53
feat(pull-requests): manage GitHub maintainer edits (#25588)
Bil0000 Oct 28, 2026
d464631
fix(web): image chips drop the pill and match other chips (#26379)
flamboh Oct 28, 2026
e34067e
fix(mobile): align settled and active thread rows (#26622)
PixPMusic Oct 28, 2026
e677e97
fix(server): publish local git status before remote lookup (#21046)
JustYannicc Oct 29, 2026
26caea2
fix(server): outside MCP clients get thread links they can open (#26642)
t3dotgg Oct 29, 2026
725573f
fix(web): double-click selects the whole inline code span (#26639)
t3dotgg Oct 29, 2026
b287102
fix(acp): ID-only command approvals keep the tracked command and gran…
juliusmarminge Oct 29, 2026
37a9ca0
fix(cursor): subagent ls, lint and MCP results no longer fail the par…
juliusmarminge Oct 29, 2026
29f33af
perf(web): long-thread timeline rebuilds 45 → 9 ms, tool updates 12 →…
juliusmarminge Oct 29, 2026
a42bdd4
fix(mobile): a thread opened while reconnecting no longer turns into …
shivamhwp Oct 29, 2026
221610f
fix(web): keep settled drop hint inside the sidebar viewport (#26736)
Yash-Singh1 Oct 29, 2026
be7d86f
feat(devices): Simlock setup uses each machine's agent-device token, …
juliusmarminge Oct 29, 2026
a5448f4
fix(web): pull request diff follows the file tree, with a hide-tests …
juliusmarminge Oct 29, 2026
8ff9ae1
fix(mobile): delivered messages no longer reappear at the bottom of l…
juliusmarminge Oct 29, 2026
d8f4980
ci(mobile): fork PRs now get the Native Change label (#26755)
juliusmarminge Oct 29, 2026
5eebf77
fix(claude): command approvals show the command, not Claude's descrip…
maria-rcks Oct 29, 2026
7b87466
docs: add nixpkgs installation instructions (#26750)
0x4bs3nt Oct 29, 2026
861a51a
feat(server,web,mobile): fall back when the default provider is out o…
maria-rcks Oct 29, 2026
783581d
fix(ci): fork PRs now resolve in workflow_run PR lookups (#26779)
juliusmarminge Oct 29, 2026
8298664
fix(server,cursor,opencode): refresh provider usage when a turn hits …
maria-rcks Oct 29, 2026
fe219c4
fix(web): keep tool groups collapsed after a usage-limit pause (#24937)
shivamhwp Oct 29, 2026
7632c3d
fix(mobile): photo-library PNGs no longer fail after resizing (#26522)
shivamhwp Oct 29, 2026
ab1e221
fix(server): guest links no longer return file diffs (#26538)
nexxeln Oct 29, 2026
eb565e1
fix(claude): Open diff shows Claude edits as a diff instead of raw JS…
nexxeln Oct 29, 2026
dac8b11
chore: vouch nexxeln (#26812)
t3dotgg Oct 29, 2026
f855f01
chore(release): prepare v0.1.4
t3-code-bot[bot] Oct 29, 2026
310c128
feat(web): Labs toggle keeps 5h, weekly, and context limits in view (…
t3dotgg Oct 29, 2026
6d82a22
fix(desktop): bump Electron to 44.10.0 for the Node.js Brotli fix (CV…
nabettu Oct 29, 2026
da1eab0
chore: make agent mistakes fail early (#26167)
t3dotgg Oct 29, 2026
ccaa3ad
feat(marketing): hero shows four agents running at once (#26875)
t3dotgg Oct 29, 2026
35b298a
fix(web): undo notice moves to the sidebar footer so it never covers …
t3dotgg Oct 29, 2026
58e60bf
feat: turn a No project thread into a project (#26767)
t3dotgg Oct 29, 2026
4432fbe
feat(antigravity): Claude and GPT models, thinking levels, and diff e…
t3dotgg Oct 29, 2026
d7935f1
feat(web): choose what the thread details card shows (#26904)
t3dotgg Oct 29, 2026
f2943ee
feat(updates): switch your whole fleet between Stable and Nightly in …
t3dotgg Oct 29, 2026
c1876b5
fix(antigravity): file reads no longer show unrelated errors (#26910)
t3dotgg Oct 29, 2026
b5fc7be
feat(release): release notes and the update popover open with a short…
t3dotgg Oct 29, 2026
48247dd
fix(cli): t3 starts on a fresh Ubuntu server without libatomic (#26912)
t3dotgg Oct 29, 2026
e6ca170
fix(web): update everything no longer waits on npm for service hosts …
t3dotgg Oct 29, 2026
3748c5c
test(web): pin the npm wait floor for update everything (#26921)
t3dotgg Oct 29, 2026
7f55281
fix(antigravity): served reads stop spinning and duplicate model name…
t3dotgg Oct 29, 2026
9acb769
fix(server): queue Copilot follow-ups without killing background agen…
shivamhwp Oct 29, 2026
4453f53
fix(web): Antigravity effort no longer shows a speedometer in the com…
t3dotgg Oct 29, 2026
6d450a4
fix(release): Linux CLI archives build on runners without package doc…
t3dotgg Oct 29, 2026
88ab83e
fix(web): reconnecting notice no longer shifts the new thread compose…
t3dotgg Oct 29, 2026
20497e4
feat(marketing): homepage looks like the app (#26960)
t3dotgg Oct 29, 2026
37fbefd
chore(release): prepare v0.1.5
t3dotgg Oct 29, 2026
960d576
fix(cli): restore Windows desktop launcher console input (#24870)
t3-code-bot[bot] Oct 29, 2026
7a4f5b6
feat(settings): add and list 1Password secret providers behind T3CODE…
yordis Oct 29, 2026
d152784
fix(mobile): use monospace in the Android terminal preview (#23401)
only21mil Oct 29, 2026
ecea6ca
fix(web): edit from here uses a pencil icon (#21878)
voltcrash Oct 29, 2026
b4da896
fix(desktop): clear Cache Storage when deleting browser profile data …
only21mil Oct 29, 2026
3d5baaf
fix(web): respect reduced motion in thread loading controls (#22100)
saphid Oct 29, 2026
49d0142
fix(provider-acp): redact API keys with underscores in ACP stderr log…
only21mil Oct 29, 2026
67afdd5
fix(provider-core): redact compound credential keys in provider error…
only21mil Oct 29, 2026
456892d
fix(web): show rounded diff counts as 1m instead of 1000k (#23723)
only21mil Oct 29, 2026
5e35f6e
fix(desktop): a timed-out login-shell probe no longer skips the serve…
tiliakoos Oct 29, 2026
55e7b5f
fix(server): keep the previous preview browser version on a shared ho…
only21mil Oct 29, 2026
ee81ea4
fix(settings): servers check secret provider writes and keep unknown …
juliusmarminge Oct 29, 2026
bfee446
fix(web): keep a closed split terminal's tab in view (#22106)
only21mil Oct 29, 2026
69310db
fix(web): the ~ breadcrumb opens the home folder (#22412)
Onkarj012 Oct 29, 2026
505d022
fix(gitlab): accept merge request URLs from self-hosted hosts (#23848)
only21mil Oct 29, 2026
2c37797
fix(server): commits no longer land on a branch switched in mid-actio…
only21mil Oct 29, 2026
3b066da
fix(server): image attachments report their size when the T3 home is …
MarcoDotIO Oct 29, 2026
ebc34da
fix(ssh): skip directories in config includes (#25187)
maria-rcks Oct 29, 2026
80dfe47
fix(server): Authorized clients names Edge, Firefox and Opera on phon…
MarcoDotIO Oct 29, 2026
1d7af65
fix(mobile): fork rows show a readable label instead of a thread ID (…
JeremiahDJordan Oct 29, 2026
1d63134
fix(markdown): formulas ending in a comma render (#22883)
NikitaMGrimm Oct 29, 2026
2049052
fix(scripts): dev runner expands ~ in --home-dir and T3CODE_HOME (#25…
MarcoDotIO Oct 29, 2026
1dd8c67
fix(desktop): keep development protocol requests on the configured se…
only21mil Oct 29, 2026
e0f972e
fix(mobile): apply terminal toolbar modifiers to arrow keys (#22161)
only21mil Oct 29, 2026
40f6f5b
fix(server): clean old logs and artifacts under a symlinked T3 home (…
only21mil Oct 29, 2026
b02b436
fix(server): preserve URL arguments in Android device actions (#25205)
maria-rcks Oct 29, 2026
c839797
fix(server): keep terminal history after cancelled sequences (#25193)
maria-rcks Oct 29, 2026
5aa81a3
fix(auth): accept a typed pairing code regardless of letter case (#22…
only21mil Oct 29, 2026
a03e630
fix(web): find in thread opens from the thread title menu, so phones …
mkantautas Oct 29, 2026
e692729
fix(desktop): resolve executable commands when checking CLI shadowing…
only21mil Oct 29, 2026
a8e4188
fix(server): SQLite temp files no longer block the event loop (#24194)
only21mil Oct 29, 2026
849d2b5
fix(server): a branch named like a tag no longer reads as heads/<name…
MarcoDotIO Oct 29, 2026
0ed5540
fix(server): cap generated worktree folder names (#25212)
maria-rcks Oct 29, 2026
c6f2b7d
perf(server): checkpoint capture scans the index listing by record in…
josephv123 Oct 29, 2026
aaba9de
fix(server): PR discovery sweeps skip hidden subagent threads (#26983)
juliusmarminge Oct 29, 2026
8084e00
fix(shared): stop parsing CLI flags after -- (#25204)
maria-rcks Oct 29, 2026
293669d
fix(server): remove abandoned v1 database copies on startup (#25301)
maria-rcks Oct 29, 2026
6ca95bd
fix(web): Enter no longer skips an unanswered question (#23626)
mkantautas Oct 29, 2026
d152335
fix(web): keep wide characters in terminal links (#24013)
only21mil Oct 29, 2026
a49a3f7
fix(server): session stops no longer trigger storage sweeps unless a …
juliusmarminge Oct 29, 2026
443edd5
fix(provider-core): subagent threads no longer inherit the parent's p…
only21mil Oct 29, 2026
1b3e330
fix(device): report failed Android permission changes (#22358)
only21mil Oct 29, 2026
0d51375
fix(server): thread lists load beyond SQLite variable limit (#25696)
juliusmarminge Oct 29, 2026
1e3e3a8
fix(server): allow large git status metadata (#25207)
maria-rcks Oct 29, 2026
781cc55
perf(mobile): flatten large file trees without copying descendants (#…
luke2x Oct 29, 2026
80ae9b5
fix(web): show theme conflicts even when another import fails (#23223)
only21mil Oct 29, 2026
061dc79
fix(usage): reject cyclic model mappings before saving (#23963)
only21mil Oct 29, 2026
bae3c19
fix(settings): offer Use existing CLI in managed Codex settings (#23160)
only21mil Oct 29, 2026
1e95da7
fix(git): honor project provider settings (#23672)
parmartejass Oct 29, 2026
efb940d
fix(desktop): force-close stalled Linux MIME cache helpers (#25210)
maria-rcks Oct 29, 2026
bdf84df
fix(cli): auth pairing create rejects a --base-url without http(s)://…
MarcoDotIO Oct 29, 2026
4afdaba
fix(server): signed asset and upload URLs stay out of HTTP traces (#2…
only21mil Oct 29, 2026
1cc9d41
fix(server): clone errors no longer show a token from the URL's query…
only21mil Oct 29, 2026
c9d3f7c
fix(claude): pass every --add-dir launch directory to Claude (#23281)
only21mil Oct 29, 2026
4b7a347
fix(composer): retry expired catalogs after failed discovery (#23174)
only21mil Oct 29, 2026
7860ee9
fix(auth): preserve pairing codes after scope rejection (#25222)
maria-rcks Oct 29, 2026
04f3538
fix(mobile): discard unchanged queued edits after starting (#25232)
maria-rcks Oct 29, 2026
b7c7a78
fix(server): t3 project commands wait for a busy server's reply (#24090)
only21mil Oct 29, 2026
9d38d68
fix(web): wait for fresh diff pages in review guides (#23225)
only21mil Oct 29, 2026
99ff58a
fix(server): Antigravity stops retrying session startup while signed …
only21mil Oct 29, 2026
de386ad
fix(web): create tasks on a permitted connected environment (#23244)
only21mil Oct 29, 2026
fa2a2dc
fix(mobile): recognize this thread's main checkout (#23764)
only21mil Oct 29, 2026
06ded0b
fix(web): keep usage hubs that share a host (#23767)
only21mil Oct 29, 2026
f8646e0
fix(github): read text and mixed-case pull request templates (#25025)
maria-rcks Oct 29, 2026
de2b1d3
fix(web): keep HTML prototype selection rings visible (#23745)
stevan-borus Oct 29, 2026
12bd9d6
fix(mobile): list pull request repositories once per environment (#25…
maria-rcks Oct 29, 2026
4e274ae
fix(shared): Open in Zed prefers zeditor over the ZFS zed on Linux (#…
pujitha24 Oct 29, 2026
ef51594
fix(server): native Codex subagents no longer show the parent's reaso…
koushikxd Oct 29, 2026
3bd64cf
fix(server): mark unread works after an automatic compaction (#25286)
flamboh Oct 29, 2026
a1621a8
fix(server): show MCP App forms from failed tool results (#25225)
maria-rcks Oct 29, 2026
ad5ba43
fix(server): restore Linux session display for editor launches (#25228)
maria-rcks Oct 29, 2026
9e60e94
fix(usage): keep host identity across container recreation (#25220)
maria-rcks Oct 29, 2026
d796063
fix(opencode): authenticate with the resolved server password (#25230)
maria-rcks Oct 29, 2026
f06dd4b
feat(web): allow resizing the file explorer (#25737)
maria-rcks Oct 29, 2026
f4147dd
fix(server): update shared Codex installs from their owning home (#25…
maria-rcks Oct 29, 2026
8bcc991
fix(cli): keep published runtimes in place during installation (#24092)
only21mil Oct 29, 2026
e836ab1
fix(web): right panel launcher letters work on non-Latin keyboard lay…
MarMarLabs Oct 29, 2026
5b4f2aa
fix(server): report a deleted or replaced ownership lock (#21886)
ScottN-PV Oct 29, 2026
cb5fac9
fix(server): mark unread no longer brings back a dismissed wake (#24080)
fixfon Oct 29, 2026
fa4f3a2
fix(mobile): stop refreshes in hidden PR Guide panes (#25486)
saphid Oct 29, 2026
b6fbd18
fix(desktop): installing the t3 command no longer reports its own lau…
Gourav224 Oct 29, 2026
1fe8ddd
test(mobile): load the branch occupancy test without native sources (…
maria-rcks Oct 29, 2026
08a463e
fix(settings): project edits keep overrides the client does not know …
only21mil Oct 29, 2026
4092c4a
fix(client): retry the permissions check after a reconnect (#27008)
juliusmarminge Oct 29, 2026
da04ebc
fix(mobile): tab bar spans the screen on iOS before 26 (#27089)
juliusmarminge Oct 29, 2026
624e70e
perf(server): thread recovery reads are 10x faster (#27099)
juliusmarminge Oct 29, 2026
3898c32
perf(server): source control discovery no longer stalls up to 10s on …
juliusmarminge Oct 29, 2026
7f39a86
perf: huge checkpoints ship up to 75x smaller to clients (#27110)
juliusmarminge Oct 29, 2026
8bf4822
perf(server): sidebar shell loads 3x faster and startup thread counts…
juliusmarminge Oct 29, 2026
22c37a8
perf(server): preparing a run is 15x faster and uses 200x less memory…
juliusmarminge Oct 29, 2026
6edf5d3
chore: sync upstream through 22c37a8da4
andrewcai8 Oct 29, 2026
a007208
chore: move the cloud box host id rationale to where it is read
andrewcai8 Oct 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 2 additions & 2 deletions .agents/skills/test-t3-mobile/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ This installs the dev client for the checkout's native fingerprint: from the
device, the local cache, or the shared cache in seconds, building once only
when no machine has that fingerprint yet (5-15 minutes, logged to the file it
names). The device id may be omitted when exactly one device is booted. It
prints one JSON line; on failure it names the next command. Authorized mobile
verification includes that step unless the user prohibits it.
prints one JSON line; on failure it names the next command. Mobile verification
includes that step unless the user prohibits it.

For a device you opened in an agent-device session elsewhere (a device pool,
cloud, or remote host), run `vp run mobile:install <ios|android> --session <s>`
Expand Down
1 change: 1 addition & 0 deletions .github/VOUCHED.td
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ github:myacoub91
github:naMqe-h
github:nateEc
github:naveed949
github:nexxeln
github:nmggithub
github:Noojuno
github:notkainoa
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/agent-context-budget.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,5 @@ jobs:
echo "Accepted by the agent-context-increase label."
exit 0
fi
echo "::error file=$BUDGET::Agent context grew compared to main. Cut the added text, or ask a maintainer to add the agent-context-increase label."
echo "::error file=$BUDGET::Agent context grew compared to main. Cut the added text and rerun the evals that cover it (evals/README.md), or ask a maintainer to add the agent-context-increase label."
exit 1
19 changes: 11 additions & 8 deletions .github/workflows/desktop-macos-preview-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,16 +77,19 @@ jobs:
return;
}
// workflow_run.pull_requests is empty for fork PRs, so resolve the
// PR from the built commit instead and require exactly one open PR
// from the same head repository and branch. The build baked its
// workflow_run.pull_requests and the commit-to-PR lookup are both
// empty for fork PRs, so list open PRs by head owner and branch
// and require exactly one at the built commit. The build baked its
// PR number into the version, so two candidates would mean the
// asset name could belong to either.
const associated = await github.paginate(
github.rest.repos.listPullRequestsAssociatedWithCommit,
{ owner, repo, commit_sha: run.head_sha, per_page: 100 },
);
const matching = associated.filter(
const open = await github.paginate(github.rest.pulls.list, {
owner,
repo,
state: "open",
head: `${run.head_repository?.owner?.login}:${run.head_branch}`,
per_page: 100,
});
const matching = open.filter(
(candidate) =>
candidate.state === "open" &&
candidate.head.sha === run.head_sha &&
Expand Down
15 changes: 14 additions & 1 deletion .github/workflows/mobile-eas-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,20 @@ jobs:
working-directory: apps/mobile
env:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
run: eas env:pull production --non-interactive
run: |
eas env:pull production --non-interactive
# EAS commands skip dotenv, and the Android fingerprint hashes this
# file. Without it every merge misses the build's hash and rebuilds.
node --input-type=module <<'NODE'
import { appendFileSync, readFileSync } from "node:fs";
import { parseEnv } from "node:util";
const env = parseEnv(readFileSync(".env.local", "utf8"));
const googleServicesFile = env.T3CODE_ANDROID_GOOGLE_SERVICES_FILE;
if (!googleServicesFile) {
throw new Error("T3CODE_ANDROID_GOOGLE_SERVICES_FILE is missing from the production environment");
}
appendFileSync(process.env.GITHUB_ENV, `T3CODE_ANDROID_GOOGLE_SERVICES_FILE=${googleServicesFile}\n`);
NODE

- name: Apply manual version override
if: steps.version_app_token.outcome == 'success'
Expand Down
106 changes: 15 additions & 91 deletions .github/workflows/mobile-fingerprint-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ 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
# PRs get the "📱 Native Change" label (applied by mobile-fingerprint-label.yml,
# which runs from main so fork PRs get it too) 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
Expand All @@ -27,8 +28,6 @@ jobs:
runs-on: ${{ github.repository == 'pingdotgg/t3code' && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
permissions:
contents: read
issues: write
pull-requests: write
env:
APP_VARIANT: production
NODE_OPTIONS: --max-old-space-size=8192
Expand Down Expand Up @@ -127,94 +126,19 @@ jobs:
} >> "$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
# mobile-fingerprint-label.yml reads this from main with a write token
# and syncs the label; this job's token is read-only for fork PRs.
- name: Record changed platforms
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;
}
}
}
run: |
mkdir -p "$RUNNER_TEMP/fingerprint-result"
printf '%s\n' "$CHANGED_PLATFORMS" > "$RUNNER_TEMP/fingerprint-result/changed-platforms"

core.info(
`PR #${issueNumber}: native fingerprint ${nativeChanged ? `changed (${process.env.CHANGED_PLATFORMS})` : "unchanged"}`,
);
- name: Upload changed platforms
uses: actions/upload-artifact@v7
with:
name: fingerprint-result
path: ${{ runner.temp }}/fingerprint-result
if-no-files-found: error
retention-days: 1
189 changes: 189 additions & 0 deletions .github/workflows/mobile-fingerprint-label.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
name: Mobile Fingerprint Label

# Trusted half of mobile-fingerprint-check.yml. That workflow runs PR code
# (fork PRs included) with a read-only token and records which platforms'
# fingerprints changed. This one runs from main with a write token and syncs
# the "📱 Native Change" label. It never checks out or runs PR code.
#
# A PR can edit the check workflow it runs under, so the recorded result is
# untrusted data: it is validated, and at worst a PR mislabels itself.

on:
workflow_run:
workflows: [Mobile Fingerprint Check]
types: [completed]

permissions:
contents: read

jobs:
label:
name: Sync native change label
# A cancelled run was superseded by a newer push, whose run labels instead.
if: >-
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
actions: read
contents: read
issues: write
pull-requests: write
steps:
- id: pr
name: Resolve the pull request behind the check
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const run = context.payload.workflow_run;
const { owner, repo } = context.repo;
const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, {
owner,
repo,
run_id: run.id,
per_page: 100,
});
const results = artifacts.filter(
(artifact) => artifact.name === "fingerprint-result" && !artifact.expired,
);
if (results.length !== 1) {
core.info(`Expected one fingerprint-result artifact; found ${results.length}. Skipping.`);
return;
}
// workflow_run.pull_requests and the commit-to-PR lookup are both
// empty for fork PRs, so list open PRs by head owner and branch.
// Requiring the PR's current head to be the checked commit keeps a
// slow run for an older push from overwriting a newer result.
const open = await github.paginate(github.rest.pulls.list, {
owner,
repo,
state: "open",
head: `${run.head_repository?.owner?.login}:${run.head_branch}`,
per_page: 100,
});
const matching = open.filter(
(candidate) =>
candidate.state === "open" &&
candidate.head.sha === run.head_sha &&
candidate.head.ref === run.head_branch &&
candidate.head.repo?.full_name === run.head_repository?.full_name,
);
if (matching.length !== 1) {
core.info(`Expected one open PR at ${run.head_sha}; found ${matching.length}. Skipping.`);
return;
}
core.setOutput("artifact_id", String(results[0].id));
core.setOutput("pr_number", String(matching[0].number));
# Read the archive as bytes; never extract PR-produced files into a
# workspace this job runs code from.
- id: result
name: Read changed platforms
if: steps.pr.outputs.pr_number != ''
shell: bash
env:
GH_TOKEN: ${{ github.token }}
ARTIFACT_ID: ${{ steps.pr.outputs.artifact_id }}
run: |
set -euo pipefail
gh api "repos/${GITHUB_REPOSITORY}/actions/artifacts/${ARTIFACT_ID}/zip" > "$RUNNER_TEMP/fingerprint-result.zip"
changed="$(unzip -p "$RUNNER_TEMP/fingerprint-result.zip" changed-platforms)"
if [[ ! "$changed" =~ ^((ios|android)( (ios|android))?)?$ ]]; then
echo "Unexpected changed-platforms value '$changed'." >&2
exit 1
fi
echo "changed_platforms=$changed" >> "$GITHUB_OUTPUT"
- name: Sync native change label
if: steps.pr.outputs.pr_number != ''
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
PR_NUMBER: ${{ steps.pr.outputs.pr_number }}
CHANGED_PLATFORMS: ${{ steps.result.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 = Number(process.env.PR_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"}`,
);
Loading
Loading