Skip to content

Commit ec196dd

Browse files
committed
Merge remote-tracking branch 'origin/main' into fix/remote-launch-recovery-main-20261102
2 parents f6d072b + 9efd960 commit ec196dd

912 files changed

Lines changed: 68856 additions & 45011 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.

‎.github/VOUCHED.td‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ github:btsouth
2222
github:cestercian
2323
github:chrisdeeming
2424
github:chuks-qua
25+
-github:coygeek Filed over 270 automated issues, about 50 a day at peak, far past what maintainers can triage
2526
github:cursoragent
2627
github:D3OXY
2728
github:dbalders

‎.github/scripts/check-nightly-release.cjs‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,54 @@ async function resolveLatestNightlyCommit({ github, context, core }) {
121121
return { tag, sha: commit.sha, version };
122122
}
123123

124+
// The two places a nightly install looks for an update: the `update-feed` pointer, and
125+
// releases.atom, which holds the 10 newest git tags of any kind. Older installs take the first
126+
// nightly in the feed. Reports what each names beside the newest published nightly.
127+
async function readNightlyUpdateFeed({ github, context, fetch = globalThis.fetch }) {
128+
const latest = await findLatestNightly({ github, context });
129+
if (!latest) return {};
130+
131+
const base = `https://github.com/${context.repo.owner}/${context.repo.repo}`;
132+
// Only a missing or malformed pointer is stale. Rewriting deletes the old file first, so a
133+
// GitHub outage must not look like one.
134+
// Node waits five minutes for a stalled response, which is this job's whole time limit.
135+
const signal = () => AbortSignal.timeout(30_000);
136+
const pointer = await fetch(`${base}/releases/download/update-feed/nightly.json`, {
137+
signal: signal(),
138+
});
139+
if (!pointer.ok && pointer.status !== 404) {
140+
throw new Error(`The update-feed pointer answered HTTP ${pointer.status}.`);
141+
}
142+
let pointerTag;
143+
if (pointer.ok) {
144+
// A body that fails to arrive throws here; only one that is not JSON counts as malformed.
145+
const body = await pointer.text();
146+
try {
147+
pointerTag = JSON.parse(body)?.tag_name;
148+
} catch {}
149+
}
150+
151+
// An unreadable feed leaves `feedTags` unset, so the pointer is still repaired. An error page
152+
// can answer HTTP 200, so only an Atom document counts as read.
153+
let feedTags;
154+
try {
155+
const feed = await fetch(`${base}/releases.atom`, { signal: signal() });
156+
const body = feed.ok ? await feed.text() : "";
157+
if (/<feed[\s>]/.test(body)) {
158+
feedTags = Array.from(body.matchAll(/href="[^"]*\/releases\/tag\/([^"]+)"/g), (match) =>
159+
decodeURIComponent(match[1]),
160+
);
161+
}
162+
} catch {}
163+
const feedNightly = feedTags?.find((tag) => /^v?\d+\.\d+\.\d+-nightly\./.test(tag));
164+
165+
return { tag: latest.tagName, pointerTag, feedNightly, feedTags };
166+
}
167+
124168
module.exports = {
125169
assertCommitOnDefaultBranch,
126170
assertReleaseSource,
127171
shouldReleaseNightly,
128172
resolveLatestNightlyCommit,
173+
readNightlyUpdateFeed,
129174
};

‎.github/scripts/check-nightly-release.test.cjs‎

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,3 +237,81 @@ test("stable releases fail when no nightly is published", async () => {
237237
const { options } = nightlyCommitFixture({ releases: [nightly(1, { tagName: "v1.0.0" })] });
238238
await assert.rejects(resolveLatestNightlyCommit(options), /No published nightly found/);
239239
});
240+
241+
const { readNightlyUpdateFeed } = require("./check-nightly-release.cjs");
242+
243+
// GitHub percent-encodes tag names in the feed, and any tag in the repository takes a slot.
244+
const atomEntry = (tag) =>
245+
`<entry><link rel="alternate" type="text/html" href="https://github.com/example/app/releases/tag/${encodeURIComponent(tag)}"/></entry>`;
246+
const atomFeed = (tags) => new Response(`<feed>${tags.map(atomEntry).join("")}</feed>`);
247+
const NEWEST = "v1.0.1-nightly.20260905.123";
248+
const OLDER = "v1.0.1-nightly.20260905.100";
249+
250+
function updateFeedFixture({ feed, pointer }) {
251+
const { options } = fixture({
252+
releases: [nightly(1), nightly(0, { tagName: "v1.0.2-preview.1" })],
253+
});
254+
options.fetch = async (url) =>
255+
url === "https://github.com/example/app/releases.atom"
256+
? feed
257+
: url === "https://github.com/example/app/releases/download/update-feed/nightly.json"
258+
? pointer
259+
: assert.fail(`unexpected fetch ${url}`);
260+
return options;
261+
}
262+
263+
const missing = () => new Response("Not Found", { status: 404 });
264+
// HTTP 200, then the connection drops before the body arrives.
265+
const brokenBody = () =>
266+
new Response(new ReadableStream({ start: (controller) => controller.error(new Error("reset")) }));
267+
for (const [name, { feed, pointer }, expected] of [
268+
[
269+
"a nightly crowded out of the feed and a missing pointer",
270+
{ feed: atomFeed(["v1.0.2-preview.1", "mobile-android-build/2.3.0/abc"]), pointer: missing() },
271+
{ pointerTag: undefined, feedNightly: undefined },
272+
],
273+
[
274+
"the first nightly in the feed, which is the one older installs take",
275+
{ feed: atomFeed(["update-feed", OLDER, NEWEST]), pointer: Response.json({ tag_name: OLDER }) },
276+
{ pointerTag: OLDER, feedNightly: OLDER },
277+
],
278+
[
279+
"a malformed pointer as stale and an unreadable feed as naming nothing",
280+
{ feed: new Response("", { status: 503 }), pointer: Response.json(null) },
281+
{ pointerTag: undefined, feedNightly: undefined, unreadable: true },
282+
],
283+
[
284+
"an HTML page with release links as an unreadable feed",
285+
{
286+
feed: new Response(
287+
`<html><feedback-form></feedback-form><a href="/example/app/releases/tag/${OLDER}">x</a></html>`,
288+
),
289+
pointer: Response.json({ tag_name: NEWEST }),
290+
},
291+
{ pointerTag: NEWEST, feedNightly: undefined, unreadable: true },
292+
],
293+
[
294+
"the pointer when the feed's body fails to arrive",
295+
{ feed: brokenBody(), pointer: Response.json({ tag_name: NEWEST }) },
296+
{ pointerTag: NEWEST, feedNightly: undefined, unreadable: true },
297+
],
298+
]) {
299+
test(`update feed check reports ${name}`, async () => {
300+
const { tag, pointerTag, feedNightly, feedTags } = await readNightlyUpdateFeed(
301+
updateFeedFixture({ feed, pointer }),
302+
);
303+
const { unreadable = false, ...named } = expected;
304+
assert.deepEqual({ tag, pointerTag, feedNightly }, { tag: NEWEST, ...named });
305+
assert.equal(feedTags === undefined, unreadable);
306+
});
307+
}
308+
309+
for (const [name, pointer, error] of [
310+
["answers with an error", () => new Response("", { status: 503 }), /pointer answered HTTP 503/],
311+
["drops the connection", brokenBody, /reset/],
312+
]) {
313+
test(`update feed check does not call a pointer stale when GitHub ${name}`, async () => {
314+
const options = updateFeedFixture({ feed: atomFeed([NEWEST]), pointer: pointer() });
315+
await assert.rejects(readNightlyUpdateFeed(options), error);
316+
});
317+
}

‎.github/workflows/agent-context-budget.yml‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,7 @@ on:
1010
permissions:
1111
contents: read
1212

13-
concurrency:
14-
group: agent-context-budget-${{ github.event.pull_request.number }}
15-
cancel-in-progress: true
16-
13+
# No concurrency group: label events arrive in bursts, and a cancelled run reports as a failed check.
1714
jobs:
1815
budget:
1916
name: Agent context budget

‎.github/workflows/issue-vouch.yml‎

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
name: Issue Vouch
2+
3+
on:
4+
issues:
5+
types: [opened, reopened]
6+
push:
7+
branches:
8+
- main
9+
paths:
10+
- .github/VOUCHED.td
11+
- .github/workflows/issue-vouch.yml
12+
workflow_dispatch:
13+
14+
permissions:
15+
contents: read
16+
issues: write
17+
18+
concurrency:
19+
group: issue-vouch-${{ github.event.issue.number || 'all' }}
20+
cancel-in-progress: true
21+
22+
jobs:
23+
label:
24+
name: Label issues by author trust
25+
runs-on: ubuntu-24.04
26+
steps:
27+
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
28+
with:
29+
script: |
30+
const { owner, repo } = context.repo;
31+
// Only listed authors get a label; open issues number in the thousands,
32+
// so unvouched is the default and stays unlabelled.
33+
const labelByStatus = { vouched: "vouch:trusted", denounced: "vouch:denounced" };
34+
const managedLabelNames = new Set(["vouch:trusted", "vouch:unvouched", "vouch:denounced"]);
35+
36+
const { data: file } = await github.rest.repos.getContent({
37+
owner,
38+
repo,
39+
path: ".github/VOUCHED.td",
40+
ref: context.payload.repository.default_branch,
41+
});
42+
const statusByUser = new Map();
43+
for (const raw of Buffer.from(file.content, "base64").toString("utf8").split("\n")) {
44+
const match = /^(-?)github:(\S+)/.exec(raw.trim());
45+
if (match) statusByUser.set(match[2].toLowerCase(), match[1] ? "denounced" : "vouched");
46+
}
47+
48+
const issues =
49+
context.eventName === "issues"
50+
? [context.payload.issue]
51+
: (
52+
await github.paginate(github.rest.issues.listForRepo, {
53+
owner,
54+
repo,
55+
state: "open",
56+
per_page: 100,
57+
})
58+
).filter((issue) => !issue.pull_request);
59+
60+
for (const issue of issues) {
61+
const status = statusByUser.get(issue.user.login.toLowerCase());
62+
const next = labelByStatus[status];
63+
const current = issue.labels.map((label) => label.name).filter((name) => managedLabelNames.has(name));
64+
65+
for (const name of current) {
66+
if (name === next) continue;
67+
try {
68+
await github.rest.issues.removeLabel({ owner, repo, issue_number: issue.number, name });
69+
} catch (error) {
70+
if (error.status !== 404) throw error;
71+
}
72+
}
73+
if (next && !current.includes(next)) {
74+
await github.rest.issues.addLabels({ owner, repo, issue_number: issue.number, labels: [next] });
75+
core.info(`Issue #${issue.number} (${issue.user.login}): ${status} -> ${next}`);
76+
}
77+
}

‎.github/workflows/pr-vouch.yml‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,17 +101,17 @@ jobs:
101101
{
102102
name: "vouch:trusted",
103103
color: "1f883d",
104-
description: "PR author is trusted by repo permissions or the VOUCHED list.",
104+
description: "Author is trusted by repo permissions or the VOUCHED list.",
105105
},
106106
{
107107
name: "vouch:unvouched",
108108
color: "fbca04",
109-
description: "PR author is not yet trusted in the VOUCHED list.",
109+
description: "Author is not yet trusted in the VOUCHED list.",
110110
},
111111
{
112112
name: "vouch:denounced",
113113
color: "d1242f",
114-
description: "PR author is explicitly blocked by the VOUCHED list.",
114+
description: "Author is explicitly blocked by the VOUCHED list.",
115115
},
116116
];
117117

‎.github/workflows/release.yml‎

Lines changed: 82 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ jobs:
4949
# published nightly, so stable only ever ships a build that nightly users
5050
# have already run. Scheduled runs also decide here whether a nightly is due.
5151
# The PAUSE_NIGHTLY repository variable skips scheduled runs only; every later
52-
# job needs this one, so a skip here skips the whole run.
52+
# job needs this one, so a skip here skips the whole run. `update_feed` runs
53+
# without it and checks the variable itself.
5354
resolve_commit:
5455
name: Resolve release commit
5556
if: github.repository == 'pingdotgg/t3code' && (github.event_name != 'schedule' || vars.PAUSE_NIGHTLY != 'true')
@@ -165,14 +166,12 @@ jobs:
165166
echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
166167
echo "make_latest=false" >> "$GITHUB_OUTPUT"
167168
elif [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" && "${DISPATCH_CHANNEL:-preview}" == "preview" ]]; then
168-
# Manual-only test train: exercises the whole release flow for a
169-
# commit end users must never receive. Never scheduled.
169+
# Manual-only test train: exercises the release flow up to a draft
170+
# release for a commit end users must never receive. Never scheduled.
170171
# Same versioning as nightly under its own prerelease identifier.
171-
# A preview release is reachable only by asking for it: npm gets it
172-
# under the `preview` dist-tag, which nothing resolves by default,
173-
# its desktop builds carry no update feed, and no updater manifest
174-
# is attached to the release, so neither stable nor nightly
175-
# installs can ever be offered one.
172+
# A preview release stays a draft (see "Publish prerelease"), its
173+
# desktop builds carry no update feed, no updater manifest is
174+
# attached, and npm gets nothing, so no install can be offered one.
176175
nightly_date="$(date -u -d "$NIGHTLY_DATE" +%Y%m%d)"
177176
178177
node scripts/resolve-nightly-release.ts \
@@ -183,7 +182,6 @@ jobs:
183182
--github-output
184183
185184
echo "release_channel=preview" >> "$GITHUB_OUTPUT"
186-
echo "cli_dist_tag=preview" >> "$GITHUB_OUTPUT"
187185
echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
188186
echo "make_latest=false" >> "$GITHUB_OUTPUT"
189187
else
@@ -1038,22 +1036,93 @@ jobs:
10381036
# The action keeps a stable release a draft until its assets finish uploading, but publishes a
10391037
# prerelease first. Desktop clients update connected servers from the CLI archives as soon as
10401038
# they see the release, so a prerelease also goes public only once every asset is attached.
1039+
# Previews stay drafts, because a draft has no git tag. releases.atom lists the 10 newest tags,
1040+
# a day's `preview` tags can sort above its `nightly` ones, and older updaters find nightlies only there.
10411041
- name: Publish prerelease
1042-
if: needs.preflight.outputs.is_prerelease == 'true'
1042+
if: needs.preflight.outputs.is_prerelease == 'true' && needs.preflight.outputs.release_channel != 'preview'
10431043
env:
10441044
GH_TOKEN: ${{ github.token }}
10451045
GH_REPO: ${{ github.repository }}
10461046
RELEASE_TAG: ${{ needs.preflight.outputs.tag }}
10471047
run: gh release edit "$RELEASE_TAG" --draft=false
10481048

1049+
# A nightly install finds updates in releases.atom, which holds the 10 newest git tags of any
1050+
# kind, so a burst of other tags hides every nightly. The `update-feed` release is the fixed URL
1051+
# newer installs also read. Every unpaused scheduled tick and every nightly repairs it, then fails when
1052+
# the feed's first nightly is not the newest one. See docs/operations/release.md#update-feed.
1053+
update_feed:
1054+
name: Check the update feed
1055+
needs: [preflight, release]
1056+
if: ${{ !cancelled() && github.repository == 'pingdotgg/t3code' && ((github.event_name == 'schedule' && vars.PAUSE_NIGHTLY != 'true') || needs.preflight.outputs.release_channel == 'nightly') }}
1057+
runs-on: blacksmith-2vcpu-ubuntu-2404
1058+
timeout-minutes: 5
1059+
permissions:
1060+
contents: write
1061+
steps:
1062+
- name: Checkout
1063+
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
1064+
with:
1065+
sparse-checkout: .github/scripts
1066+
1067+
- id: feed
1068+
name: Read the update feed
1069+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
1070+
with:
1071+
retries: 3
1072+
script: |
1073+
const { readNightlyUpdateFeed } = require('./.github/scripts/check-nightly-release.cjs');
1074+
const { tag, pointerTag, feedNightly, feedTags } = await readNightlyUpdateFeed({ github, context });
1075+
if (!tag) return;
1076+
core.info(`Newest nightly: ${tag}. Pointer: ${pointerTag}. Feed: ${feedTags?.join(', ')}.`);
1077+
core.setOutput('tag', tag);
1078+
core.setOutput('pointer_stale', pointerTag !== tag);
1079+
core.setOutput('feed', feedTags === undefined ? 'unreadable' : feedNightly === tag ? 'ok' : 'hidden');
1080+
1081+
- name: Point the update feed at the newest nightly
1082+
if: steps.feed.outputs.pointer_stale == 'true'
1083+
env:
1084+
GH_TOKEN: ${{ github.token }}
1085+
GH_REPO: ${{ github.repository }}
1086+
RELEASE_TAG: ${{ steps.feed.outputs.tag }}
1087+
run: |
1088+
gh api "repos/$GH_REPO/releases/tags/$RELEASE_TAG" --jq '{tag_name, name, body}' > nightly.json
1089+
gh release view update-feed > /dev/null 2>&1 ||
1090+
gh release create update-feed --prerelease --latest=false --title "Update feed" \
1091+
--notes "The desktop updater reads this release to find the newest nightly. Do not delete it. See docs/operations/release.md."
1092+
gh release upload update-feed nightly.json --clobber
1093+
1094+
# Every run, because the pointer can be current while the release's flags are wrong. A draft's
1095+
# files are not public, and a release marked "latest" is the one stable installs update to.
1096+
- name: Keep the update feed release public and never latest
1097+
if: steps.feed.outputs.tag != ''
1098+
env:
1099+
GH_TOKEN: ${{ github.token }}
1100+
GH_REPO: ${{ github.repository }}
1101+
run: gh release edit update-feed --draft=false --prerelease --latest=false
1102+
1103+
# An unreadable feed says nothing about its tags and the next tick reads it again, so only a
1104+
# feed that was read and names another nightly first fails the run.
1105+
- name: Fail when the feed hides the newest nightly
1106+
if: steps.feed.outputs.tag != '' && steps.feed.outputs.feed != 'ok'
1107+
env:
1108+
RELEASE_TAG: ${{ steps.feed.outputs.tag }}
1109+
FEED: ${{ steps.feed.outputs.feed }}
1110+
run: |
1111+
if [[ "$FEED" == "unreadable" ]]; then
1112+
echo "::warning::releases.atom could not be read, so this run did not check it."
1113+
exit 0
1114+
fi
1115+
echo "::error::The first nightly in releases.atom is not $RELEASE_TAG. Nightly installs older than 0.1.10-nightly.20261112.4603 cannot reach it, and neither can later builds that do not read the pointer when the feed names an older nightly. A preview from a branch not rebased onto main is the usual cause. See docs/operations/release.md#update-feed."
1116+
exit 1
1117+
10491118
# The `t3` npm package is a launcher that downloads this version's CLI
10501119
# archive from the GitHub Release, so it publishes only once that release
1051-
# exists. Preview publishes too, under the `preview` dist-tag, which
1052-
# nothing resolves unless asked for by name.
1120+
# exists. A preview release stays a draft, whose files are not public, so
1121+
# a preview never publishes the launcher.
10531122
publish_cli:
10541123
name: Publish CLI to npm
10551124
needs: [preflight, build_bundle, release]
1056-
if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.release.result == 'success' }}
1125+
if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.release.result == 'success' && needs.preflight.outputs.release_channel != 'preview' }}
10571126
runs-on: ubuntu-24.04 # blacksmith-8vcpu-ubuntu-2404
10581127
timeout-minutes: 15
10591128
permissions:

‎.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,5 @@ node_modules/
4949

5050
# pnpm content-addressable store; never belongs in the repo.
5151
.pnpm-store/
52+
53+
/packages/ssh/.ssh-launcher-test-*/

0 commit comments

Comments
 (0)