From 8da0f0bc0e70f1f4c725bfde245881979c05ae8a Mon Sep 17 00:00:00 2001 From: collissimo <148455189+collissimo@users.noreply.github.com> Date: Fri, 23 Oct 2026 17:40:05 +0200 Subject: [PATCH 1/2] Fork release: build each upstream nightly within minutes via a Cloudflare Worker GitHub cannot notify a fork of upstream releases, npm registry hooks are gone, and scheduled runs in the fork fired a few times a day or not at all. A Worker checks every minute and sends repository_dispatch once per new upstream nightly; pushes to collin/main build directly, and the cron drops to a 6-hour fallback. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/fork-release.yml | 20 ++++++-- FORK.md | 17 +++++-- fork/upstream-nightly-watcher/worker.js | 54 +++++++++++++++++++++ fork/upstream-nightly-watcher/wrangler.toml | 11 +++++ 4 files changed, 93 insertions(+), 9 deletions(-) create mode 100644 fork/upstream-nightly-watcher/worker.js create mode 100644 fork/upstream-nightly-watcher/wrangler.toml diff --git a/.github/workflows/fork-release.yml b/.github/workflows/fork-release.yml index ae15957ff9ddd..12d668184c00e 100644 --- a/.github/workflows/fork-release.yml +++ b/.github/workflows/fork-release.yml @@ -8,18 +8,28 @@ # release and version, so a fork client and a fork server on the other OS update in step. # # Each build is collin/main merged with the commit of upstream's newest nightly, so the fork -# follows upstream release by release. GitHub cannot wake a fork when upstream publishes, so a -# cheap check runs every hour and builds only when that nightly or collin/main is new. +# follows upstream release by release. GitHub cannot wake a fork when upstream publishes (and npm +# registry hooks are gone), and scheduled runs in a fork fire hours late or not at all. So +# fork/upstream-nightly-watcher, a Cloudflare Worker, checks every minute and sends +# repository_dispatch once per new upstream nightly; the cron is only a fallback. Every trigger +# builds only when that nightly or collin/main is new. # Nothing is pushed to a branch. Claude Code resolves a merge conflict for the build only # (fork/resolve-conflicts.sh, with the ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN secret); # collin/main itself is still synced by fork/sync.sh. When Claude cannot resolve it, the run fails # (and GitHub mails the owner): then sync collin/main by hand. name: Fork release +# The watcher reads this title to dispatch each upstream nightly only once. +run-name: ${{ github.event.client_payload.upstream_tag && format('Fork release of {0}', github.event.client_payload.upstream_tag) || 'Fork release' }} on: + repository_dispatch: + types: [upstream-nightly] + push: + branches: [collin/main] schedule: - # A run without a new upstream nightly or fork commit ends after two API calls. - - cron: "23 * * * *" + # Fallback for a missed or failed dispatch. A run without a new upstream nightly or fork + # commit ends after two API calls. + - cron: "23 */6 * * *" workflow_dispatch: inputs: force: @@ -113,7 +123,7 @@ jobs: echo "conflict=true" >> "$GITHUB_OUTPUT" fi - # One resolution per fork/upstream pair: a failed build leaves the pair due again every hour, + # One resolution per fork/upstream pair: a failed build leaves the pair due again on every run, # and a retry must neither pay for nor re-roll the resolution. force asks Claude again. - name: Restore an earlier resolution of these commits id: earlier diff --git a/FORK.md b/FORK.md index d92e6a85961cb..f696614307422 100644 --- a/FORK.md +++ b/FORK.md @@ -46,9 +46,13 @@ The installed app is a fork build, so the update button stays on the fork. The d for updates in the repository it was built in (`app-update.yml`, from `GITHUB_REPOSITORY`); an official Nightly would update to upstream and lose the fork features. -`.github/workflows/fork-release.yml` checks every hour (and on dispatch) for a new upstream -nightly or a new `collin/main` commit. It merges the commit of upstream's newest nightly into -`collin/main` for the build only, skips when the last release already has both commits, builds +`.github/workflows/fork-release.yml` runs when upstream publishes a nightly, on every push to +`collin/main`, every 6 hours as a fallback, and on dispatch. GitHub cannot notify a fork of +upstream releases and its cron fires hours late in a fork, so the Cloudflare Worker in +`fork/upstream-nightly-watcher/` checks every minute and sends one `repository_dispatch` per new +upstream nightly (a failed build is retried by the 6-hour cron, not redispatched). + +Each run merges the commit of upstream's newest nightly into `collin/main` for the build only, skips when the last release already has both commits, builds Windows x64 (NSIS, `nightly.yml`) and Linux x64 (AppImage, `nightly-linux.yml`) side by side, and publishes both in one prerelease `v-nightly..<10000 + run>`. The version scheme is upstream's (the offset sorts a fork build above an upstream nightly of the same day), so the app's @@ -58,10 +62,15 @@ version. `-f force=true` builds without new commits. A merge conflict is resolved by Claude Code (`fork/resolve-conflicts.sh`, Opus, at most $5 per run) for the build only; the typecheck job still gates the release. It needs the repository secret `CLAUDE_CODE_OAUTH_TOKEN` (from `claude setup-token`; bills the subscription's monthly Agent SDK -credit) or `ANTHROPIC_API_KEY` (Console API credits; wins when both are set). Each fork/upstream pair is resolved once and cached, so hourly retries cost +credit) or `ANTHROPIC_API_KEY` (Console API credits; wins when both are set). Each fork/upstream pair is resolved once and cached, so retries cost nothing. When Claude cannot resolve it, every run of that pair fails: sync `collin/main` by hand, then `gh workflow run fork-release.yml` (`-f force=true` asks Claude again instead). +Watcher setup (once, or after the token expires), from `fork/upstream-nightly-watcher/`: +`npx wrangler deploy`, then `npx wrangler secret put GITHUB_TOKEN` with a fine-grained token +for `collissimo/t3code` (Contents: read and write, Actions: read). `npx wrangler tail` shows +each check. + - Every machine must run a fork build once (installed by hand); from then on the update button, including "update everything" for connected servers, stays on the fork. A desktop app on the other feed (an official build) is never offered a server update from a fork client: it could diff --git a/fork/upstream-nightly-watcher/worker.js b/fork/upstream-nightly-watcher/worker.js new file mode 100644 index 0000000000000..e1d64a4d5210f --- /dev/null +++ b/fork/upstream-nightly-watcher/worker.js @@ -0,0 +1,54 @@ +// Starts fork-release.yml once per new upstream nightly. GitHub cannot notify a fork of upstream +// releases and scheduled runs in a fork are unreliable, so this checks every minute. +// Secret GITHUB_TOKEN: fine-grained token for the fork with Contents read/write (dispatch) and +// Actions read (runs); the unauthenticated API limit (60/h) is too low for these reads. + +async function github(env, path, init = {}) { + const response = await fetch(`https://api.github.com/${path}`, { + ...init, + headers: { + accept: "application/vnd.github+json", + authorization: `Bearer ${env.GITHUB_TOKEN}`, + "user-agent": "t3code-upstream-nightly-watcher", + "x-github-api-version": "2022-11-28", + ...init.headers, + }, + }); + if (!response.ok) { + throw new Error(`${init.method ?? "GET"} ${path}: ${response.status} ${await response.text()}`); + } + return response.status === 204 ? null : response.json(); +} + +async function check(env) { + // Same choice as fork-release.yml: the newest published nightly. + const upstream = await github(env, `repos/${env.UPSTREAM}/releases?per_page=30`); + const nightly = upstream + .filter((release) => !release.draft && release.tag_name.includes("-nightly.")) + .sort((a, b) => Date.parse(b.published_at) - Date.parse(a.published_at))[0]; + if (!nightly) return "no upstream nightly"; + const tag = nightly.tag_name; + + // fork-release.yml writes "builds of upstream with" into each release. + const [latest] = await github(env, `repos/${env.FORK}/releases?per_page=1`); + if (latest?.body?.includes(`upstream ${tag} `)) return `${tag} already released`; + + // A failed build must not be redispatched every minute; the workflow's cron retries it. + const workflow = `repos/${env.FORK}/actions/workflows/fork-release.yml`; + const runs = await github(env, `${workflow}/runs?event=repository_dispatch&per_page=5`); + if (runs.workflow_runs.some((run) => run.display_title === `Fork release of ${tag}`)) { + return `${tag} already dispatched`; + } + + await github(env, `repos/${env.FORK}/dispatches`, { + method: "POST", + body: JSON.stringify({ event_type: "upstream-nightly", client_payload: { upstream_tag: tag } }), + }); + return `dispatched ${tag}`; +} + +export default { + async scheduled(_controller, env) { + console.log(await check(env)); + }, +}; diff --git a/fork/upstream-nightly-watcher/wrangler.toml b/fork/upstream-nightly-watcher/wrangler.toml new file mode 100644 index 0000000000000..13b31288ecaea --- /dev/null +++ b/fork/upstream-nightly-watcher/wrangler.toml @@ -0,0 +1,11 @@ +name = "t3code-upstream-nightly-watcher" +main = "worker.js" +compatibility_date = "2026-10-01" +workers_dev = false + +[triggers] +crons = ["* * * * *"] + +[vars] +UPSTREAM = "pingdotgg/t3code" +FORK = "collissimo/t3code" From 7920c2f5525df6d71174b7a5f7b694cd4fa2a30e Mon Sep 17 00:00:00 2001 From: collissimo <148455189+collissimo@users.noreply.github.com> Date: Fri, 23 Oct 2026 17:42:14 +0200 Subject: [PATCH 2/2] Fork release: ignore the watcher's wrangler state Co-Authored-By: Claude Opus 5.5 --- fork/upstream-nightly-watcher/.gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 fork/upstream-nightly-watcher/.gitignore diff --git a/fork/upstream-nightly-watcher/.gitignore b/fork/upstream-nightly-watcher/.gitignore new file mode 100644 index 0000000000000..b75a0fa9c318b --- /dev/null +++ b/fork/upstream-nightly-watcher/.gitignore @@ -0,0 +1 @@ +.wrangler/