Skip to content

Commit fde661e

Browse files
committed
Merge remote-tracking branch 'github/main' into fix/import-full-history
# Conflicts: # apps/server/src/project/AgentSessionScanner.test.ts
2 parents c54e745 + d2c2595 commit fde661e

1,004 files changed

Lines changed: 201064 additions & 89491 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.agents/skills/test-t3-mobile/SKILL.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@ vp run mobile:install <ios|android> <device-id>
3636

3737
This installs the dev client for the checkout's native fingerprint: from the
3838
device, the local cache, or the shared cache in seconds, building once only
39-
when no machine has that fingerprint yet (5-15 minutes, logged to the file it
40-
names). The device id may be omitted when exactly one device is booted. It
39+
when no machine has that fingerprint yet: in the `Mobile Dev Client` workflow
40+
on Blacksmith when HEAD is pushed and `gh` may dispatch it (the command waits),
41+
otherwise here (5-15 minutes, logged to the file it names). The device id may be omitted when exactly one device is booted. It
4142
prints one JSON line; on failure it names the next command. Mobile verification
4243
includes that step unless the user prohibits it.
4344

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// The Android half of the mobile preview comment. The android job writes the fingerprint and
2+
// the update; the android-build job adds the client's install link a few minutes later. Each
3+
// run merges what it knows into the state kept in the comment's first line.
4+
const MARKER = "mobile-preview-android";
5+
const PROJECT_ID = "d763fcb8-d37c-41ea-a773-b54a0ab4a454";
6+
const APP_SCHEME = "t3-code";
7+
8+
function readState(body) {
9+
const match = new RegExp(`^<!-- ${MARKER} (\\{.*\\}) -->`).exec(body ?? "");
10+
if (!match) return null;
11+
try {
12+
const state = JSON.parse(match[1]);
13+
const text = (value) => (typeof value === "string" ? value : "");
14+
return {
15+
fingerprint: text(state.fingerprint),
16+
client: text(state.client),
17+
group: text(state.group),
18+
};
19+
} catch {
20+
return null;
21+
}
22+
}
23+
24+
/**
25+
* New values win; a new fingerprint drops the old fingerprint's client. A build that finishes
26+
* after the PR moved to another fingerprint changes nothing: its client cannot run the update
27+
* the comment now shows.
28+
*/
29+
function mergeState(previous, next) {
30+
const buildOnly = Boolean(next.client) && !next.group;
31+
if (
32+
buildOnly &&
33+
previous?.fingerprint &&
34+
next.fingerprint &&
35+
previous.fingerprint !== next.fingerprint
36+
) {
37+
return previous;
38+
}
39+
const fingerprint = next.fingerprint || previous?.fingerprint || "";
40+
const sameFingerprint = !previous?.fingerprint || previous.fingerprint === fingerprint;
41+
return {
42+
fingerprint,
43+
client: next.client || (sameFingerprint ? (previous?.client ?? "") : ""),
44+
group: next.group || previous?.group || "",
45+
};
46+
}
47+
48+
function render(state, runUrl) {
49+
const qr = `https://qr.expo.dev/eas-update?appScheme=${APP_SCHEME}&projectId=${PROJECT_ID}&groupId=${state.group}`;
50+
const rows = [
51+
`<!-- ${MARKER} ${JSON.stringify(state)} -->`,
52+
"🤖 Android preview",
53+
"",
54+
"&nbsp; | 🤖 Android",
55+
"--- | ---",
56+
`Fingerprint | ${state.fingerprint || "n/a"}`,
57+
`Dev client | ${state.client ? `[Install](${state.client})` : `Building on Blacksmith ([run](${runUrl})); this comment gets the install link when it finishes`}`,
58+
];
59+
if (state.group) {
60+
rows.push(
61+
`Update Details | [Update Permalink](https://expo.dev/projects/${PROJECT_ID}/updates/${state.group})`,
62+
`Update QR | <a href="${qr}"><img src="${qr}" width="250px" height="250px" /></a>`,
63+
);
64+
}
65+
return rows.join("\n");
66+
}
67+
68+
async function github(path, init = {}) {
69+
const response = await fetch(`https://api.github.com${path}`, {
70+
...init,
71+
headers: {
72+
authorization: `Bearer ${process.env.GH_TOKEN}`,
73+
accept: "application/vnd.github+json",
74+
"content-type": "application/json",
75+
...init.headers,
76+
},
77+
});
78+
if (!response.ok) {
79+
throw new Error(
80+
`GitHub ${init.method ?? "GET"} ${path}: ${response.status} ${await response.text()}`,
81+
);
82+
}
83+
return response.json();
84+
}
85+
86+
async function main() {
87+
const repository = process.env.GITHUB_REPOSITORY;
88+
const pr = process.env.PR;
89+
if (!repository || !pr || !process.env.GH_TOKEN) {
90+
throw new Error("GITHUB_REPOSITORY, PR and GH_TOKEN are required.");
91+
}
92+
const runUrl = `${process.env.GITHUB_SERVER_URL}/${repository}/actions/runs/${process.env.GITHUB_RUN_ID}`;
93+
let existing = null;
94+
for (let page = 1; existing === null; page++) {
95+
const comments = await github(
96+
`/repos/${repository}/issues/${pr}/comments?per_page=100&page=${page}`,
97+
);
98+
// Only this workflow's own comment: anyone can post the marker, and nobody else's is editable.
99+
existing =
100+
comments.find(
101+
(comment) =>
102+
comment.user?.login === "github-actions[bot]" && readState(comment.body) !== null,
103+
) ?? null;
104+
if (comments.length < 100) break;
105+
}
106+
const state = mergeState(readState(existing?.body), {
107+
fingerprint: process.env.FINGERPRINT ?? "",
108+
client: process.env.CLIENT ?? "",
109+
group: process.env.GROUP ?? "",
110+
});
111+
const body = JSON.stringify({ body: render(state, runUrl) });
112+
if (existing) {
113+
await github(`/repos/${repository}/issues/comments/${existing.id}`, { method: "PATCH", body });
114+
} else {
115+
await github(`/repos/${repository}/issues/${pr}/comments`, { method: "POST", body });
116+
}
117+
}
118+
119+
if (require.main === module) {
120+
main().catch((error) => {
121+
console.error(error);
122+
process.exitCode = 1;
123+
});
124+
}
125+
126+
module.exports = { mergeState, readState, render };
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
const assert = require("node:assert/strict");
2+
const test = require("node:test");
3+
4+
const { mergeState, readState, render } = require("./mobile-preview-comment.cjs");
5+
6+
const fingerprint = "c31608b37a8a2dca35ccee39cfd2ec7305042629";
7+
const run = "https://github.com/pingdotgg/t3code/actions/runs/1";
8+
9+
test("the comment round-trips its state", () => {
10+
const state = { fingerprint, client: "", group: "8e413a0a" };
11+
const body = render(state, run);
12+
assert.deepEqual(readState(body), state);
13+
assert.match(
14+
body,
15+
/Building on Blacksmith \(\[run\]\(https:\/\/github\.com\/pingdotgg\/t3code\/actions\/runs\/1\)\)/,
16+
);
17+
assert.match(
18+
body,
19+
/qr\.expo\.dev\/eas-update\?appScheme=t3-code&projectId=[0-9a-f-]+&groupId=8e413a0a/,
20+
);
21+
assert.equal(readState("🚀 Expo continuous deployment is ready!"), null);
22+
});
23+
24+
test("the build job adds the install link without losing the update", () => {
25+
const first = mergeState(null, { fingerprint, client: "", group: "8e413a0a" });
26+
const built = mergeState(first, { fingerprint, client: "https://expo.dev/a/b", group: "" });
27+
assert.deepEqual(built, { fingerprint, client: "https://expo.dev/a/b", group: "8e413a0a" });
28+
assert.match(render(built, run), /Dev client \| \[Install\]\(https:\/\/expo\.dev\/a\/b\)/);
29+
});
30+
31+
test("a new fingerprint drops the old client; the same one keeps it", () => {
32+
const built = { fingerprint, client: "https://expo.dev/a/b", group: "old" };
33+
assert.deepEqual(mergeState(built, { fingerprint, client: "", group: "new" }), {
34+
fingerprint,
35+
client: "https://expo.dev/a/b",
36+
group: "new",
37+
});
38+
assert.deepEqual(mergeState(built, { fingerprint: "f".repeat(40), client: "", group: "new" }), {
39+
fingerprint: "f".repeat(40),
40+
client: "",
41+
group: "new",
42+
});
43+
});
44+
45+
test("a build that finishes after the PR moved to another fingerprint changes nothing", () => {
46+
const current = { fingerprint: "f".repeat(40), client: "", group: "new" };
47+
assert.deepEqual(
48+
mergeState(current, { fingerprint, client: "https://expo.dev/a/b", group: "" }),
49+
current,
50+
);
51+
});
52+
53+
test("state from the comment is always three strings", () => {
54+
assert.deepEqual(
55+
readState('<!-- mobile-preview-android {"fingerprint":5,"client":null} -->\nx'),
56+
{
57+
fingerprint: "",
58+
client: "",
59+
group: "",
60+
},
61+
);
62+
});

‎.github/workflows/ci.yml‎

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,16 @@ jobs:
140140
- name: Start installing browser secret helper build libraries
141141
if: needs.changes.outputs.build != 'false'
142142
run: |
143-
sudo sed -i 's|http://|https://|g' /etc/apt/blacksmith-ubuntu-mirrors.txt /etc/apt/sources.list.d/ubuntu.sources
143+
# Fetch only the indexes these main and universe packages need. The full set is ~42 MB,
144+
# which a slow mirror can stretch past the wait below.
145+
sudo sed -i -e 's|http://|https://|g' -e 's/^Components: .*/Components: main universe/' \
146+
/etc/apt/blacksmith-ubuntu-mirrors.txt /etc/apt/sources.list.d/ubuntu.sources
144147
(
145148
set +e
146-
sudo apt-get update && sudo apt-get install -y libsecret-1-dev pkg-config xvfb
149+
sudo apt-get update -o Acquire::Languages=none \
150+
-o Acquire::IndexTargets::deb::DEP-11::DefaultEnabled=false \
151+
-o Acquire::IndexTargets::deb::CNF::DefaultEnabled=false &&
152+
sudo apt-get install -y libsecret-1-dev pkg-config xvfb
147153
echo "$?" > "$RUNNER_TEMP/apt-status"
148154
) > "$RUNNER_TEMP/apt.log" 2>&1 < /dev/null &
149155
@@ -287,10 +293,16 @@ jobs:
287293
# the background while Vite+ installs; the step before the tests waits for it.
288294
- name: Start installing browser secret helper build libraries
289295
run: |
290-
sudo sed -i 's|http://|https://|g' /etc/apt/blacksmith-ubuntu-mirrors.txt /etc/apt/sources.list.d/ubuntu.sources
296+
# Fetch only the indexes these main and universe packages need. The full set is ~42 MB,
297+
# which a slow mirror can stretch past the wait below.
298+
sudo sed -i -e 's|http://|https://|g' -e 's/^Components: .*/Components: main universe/' \
299+
/etc/apt/blacksmith-ubuntu-mirrors.txt /etc/apt/sources.list.d/ubuntu.sources
291300
(
292301
set +e
293-
sudo apt-get update && sudo apt-get install -y libsecret-1-dev pkg-config
302+
sudo apt-get update -o Acquire::Languages=none \
303+
-o Acquire::IndexTargets::deb::DEP-11::DefaultEnabled=false \
304+
-o Acquire::IndexTargets::deb::CNF::DefaultEnabled=false &&
305+
sudo apt-get install -y libsecret-1-dev pkg-config
294306
echo "$?" > "$RUNNER_TEMP/apt-status"
295307
) > "$RUNNER_TEMP/apt.log" 2>&1 < /dev/null &
296308
@@ -314,7 +326,7 @@ jobs:
314326
run: python3 -B .github/scripts/stage-preview-bundle.test.py
315327

316328
- name: Test release checks
317-
run: node --test .github/scripts/check-nightly-release.test.cjs .github/scripts/vercel-deployment-output.test.cjs
329+
run: node --test .github/scripts/check-nightly-release.test.cjs .github/scripts/vercel-deployment-output.test.cjs .github/scripts/mobile-preview-comment.test.cjs
318330

319331
- name: Finish installing browser secret helper build libraries
320332
timeout-minutes: 5
@@ -592,7 +604,7 @@ jobs:
592604
needs: changes
593605
if: ${{ !cancelled() && needs.changes.outputs.release_smoke != 'false' }}
594606
runs-on: blacksmith-8vcpu-ubuntu-2404
595-
timeout-minutes: 10
607+
timeout-minutes: 15
596608
steps:
597609
- name: Checkout
598610
uses: actions/checkout@v6

0 commit comments

Comments
 (0)