From 546bb51d5251ec09cdb1b8952b64faddd094d8c5 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 6 Nov 2026 11:42:04 +0100 Subject: [PATCH 1/2] ci: add TanStack Source CI workflows Port the PR test job, the E2E shards, the Bun tests and the daily model sync from .github/workflows to .source/workflows. Nx Cloud is off in these jobs, so Nx uses the forge's task cache. Coverage and the pkg-pr-new preview stay on GitHub for now. --- .source/workflows/bun-test.ts | 43 +++++++++ .source/workflows/e2e.ts | 92 ++++++++++++++++++++ .source/workflows/pr.ts | 19 ++++ .source/workflows/sync-models.ts | 144 +++++++++++++++++++++++++++++++ 4 files changed, 298 insertions(+) create mode 100644 .source/workflows/bun-test.ts create mode 100644 .source/workflows/e2e.ts create mode 100644 .source/workflows/pr.ts create mode 100644 .source/workflows/sync-models.ts diff --git a/.source/workflows/bun-test.ts b/.source/workflows/bun-test.ts new file mode 100644 index 0000000000..409fb5558e --- /dev/null +++ b/.source/workflows/bun-test.ts @@ -0,0 +1,43 @@ +// The Bun-only test suite of @tanstack/ai-isolate-quickjs-bun. +// Ported from .github/workflows/bun-test.yml. +import { defineWorkflow, pullRequest, setup } from '@tanstack-source/ci' + +export default defineWorkflow('bun-test', { + on: [ + pullRequest({ + paths: [ + 'packages/ai-isolate-quickjs-bun/**', + 'packages/ai-code-mode/**', + '.source/workflows/bun-test.ts', + ], + }), + ], +}).job('test', { + // ponytail: Nx Cloud off, so Nx uses the forge's own task cache. + env: { NX_NO_CLOUD: 'true' }, + egress: [ + 'registry.npmjs.org', + 'nodejs.org', + // setup.bun() downloads from the GitHub release of Bun. + 'github.com', + 'objects.githubusercontent.com', + 'release-assets.githubusercontent.com', + ], + steps: [ + setup.node(), + setup.pnpm(), + // The driver needs Bun >= 1.3.14 (see the package `engines` field). + setup.bun({ version: '1.4.4' }), + { name: 'Install', run: 'pnpm install --frozen-lockfile' }, + { + // `@tanstack/ai-code-mode` is imported at runtime by the driver, so nx + // builds it to `dist/` first via `^build`. + name: 'Build package and its workspace dependencies', + run: 'pnpm exec nx build @tanstack/ai-isolate-quickjs-bun', + }, + { + name: 'Run Bun test suite', + run: 'pnpm --filter @tanstack/ai-isolate-quickjs-bun run test:bun', + }, + ], +}) diff --git a/.source/workflows/e2e.ts b/.source/workflows/e2e.ts new file mode 100644 index 0000000000..dd98d77cfd --- /dev/null +++ b/.source/workflows/e2e.ts @@ -0,0 +1,92 @@ +// E2E tests. Ported from .github/workflows/e2e.yml. +import { + artifact, + cache, + defineWorkflow, + pullRequest, + push, + setup, +} from '@tanstack-source/ci' + +export default defineWorkflow('e2e', { + on: [pullRequest(), push({ branches: ['main', 'alpha', 'beta', 'rc'] })], +}) + .job('e2e', { + image: 'browser', + size: 'medium', + timeoutMinutes: 30, + checkout: { depth: 0 }, + matrix: { shard: [1, 2, 3, 4] }, + // ponytail: Nx Cloud off, so Nx uses the forge's own task cache. + env: { NX_NO_CLOUD: 'true' }, + egress: [ + 'registry.npmjs.org', + 'nodejs.org', + 'cdn.playwright.dev', + 'playwright.download.prss.microsoft.com', + ], + steps: [ + setup.node(), + setup.pnpm(), + { name: 'Install', run: 'pnpm install --frozen-lockfile' }, + { + name: 'Cache Playwright Browsers', + do: async (ctx) => { + const { stdout } = await ctx.sh`sha256sum testing/e2e/package.json` + await cache({ + key: `playwright-${stdout.slice(0, 16)}`, + paths: ['~/.cache/ms-playwright'], + restoreKeys: ['playwright-'], + }).do(ctx) + // Upload the results also when the tests fail. + const shard = ctx.matrix.shard + ctx.afterJob( + async (after) => { + await artifact({ + name: `e2e-videos-${shard}`, + paths: ['testing/e2e/test-results/**/*.webm'], + retentionDays: 14, + }).do(after) + await artifact({ + name: `e2e-report-${shard}`, + paths: ['testing/e2e/playwright-report/'], + retentionDays: 14, + }).do(after) + await artifact({ + name: `e2e-traces-${shard}`, + paths: ['testing/e2e/test-results/**/*.zip'], + retentionDays: 14, + }).do(after) + }, + { name: 'Upload test results', always: true }, + ) + }, + }, + { name: 'Build Packages', run: 'pnpm run build:all' }, + // A no-op when the cache restored the browser. + { + name: 'Install Playwright Chromium', + run: 'pnpm --filter @tanstack/ai-e2e exec playwright install chromium', + }, + { + name: 'Run E2E Tests', + do: (ctx) => + ctx.sh`pnpm --filter @tanstack/ai-e2e test:e2e ${`--shard=${ctx.matrix.shard}/4`}`, + }, + ], + }) + // One check for all shards, to use in a branch rule. + .job('e2e-result', { + needs: ['e2e'], + when: 'always', + steps: [ + { + name: 'Check shard results', + do: (ctx) => { + if (ctx.needs.e2e.result !== 'success') { + throw new Error(`E2E shards: ${ctx.needs.e2e.result}`) + } + }, + }, + ], + }) diff --git a/.source/workflows/pr.ts b/.source/workflows/pr.ts new file mode 100644 index 0000000000..e31611fb51 --- /dev/null +++ b/.source/workflows/pr.ts @@ -0,0 +1,19 @@ +// CI for pull requests. Ported from .github/workflows/pr.yml. +// Not ported yet: `coverage` (needs comment edits on the forge) and `preview` +// (pkg-pr-new needs the pkg.pr.new GitHub App). Both stay on GitHub for now. +import { defineWorkflow, pullRequest, setup } from '@tanstack-source/ci' + +export default defineWorkflow('pr', { + on: [pullRequest()], +}).job('test', { + checkout: { depth: 0 }, + // ponytail: Nx Cloud off, so Nx uses the forge's own task cache. + env: { NX_NO_CLOUD: 'true' }, + egress: ['registry.npmjs.org', 'nodejs.org'], + steps: [ + setup.node(), + setup.pnpm(), + { name: 'Install', run: 'pnpm install --frozen-lockfile' }, + { name: 'Run Checks', run: 'pnpm run test:pr' }, + ], +}) diff --git a/.source/workflows/sync-models.ts b/.source/workflows/sync-models.ts new file mode 100644 index 0000000000..4b678f0e34 --- /dev/null +++ b/.source/workflows/sync-models.ts @@ -0,0 +1,144 @@ +// Daily sync of model metadata. Ported from .github/workflows/sync-models.yml. +// It force-pushes the changes to automated/sync-models, opens a PR for them, +// and opens (or comments on) a `sync-models` issue when a sync step fails. +import { + defineWorkflow, + forge, + manual, + schedule, + setup, +} from '@tanstack-source/ci' + +const branch = 'automated/sync-models' + +const prBody = `Automated daily sync of model metadata from OpenRouter, Vercel AI Gateway, Lovable AI Gateway, LLM Gateway, Neon AI Gateway, and modelschemas (ElevenLabs text-to-speech, music, sound effects, and voice design). + +- Fetches the latest model list from OpenRouter (chat + \`GET /api/v1/videos/models\`) +- Fetches the latest model list from Vercel AI Gateway +- Fetches the latest model list from Lovable AI Gateway +- Fetches the latest model list from LLM Gateway (\`GET /v1/models\`) +- Fetches the latest model list from Neon AI Gateway +- Converts to the internal adapter format +- Syncs provider-specific model metadata for affected packages +- Inserts new ElevenLabs text-to-speech, music, sound-effect, and voice-design models from modelschemas +- Creates a patch changeset for all changed packages` + +// Each sync runs on its own, so one provider failing does not block the +// others. Whatever succeeded is still committed, then the job fails. +const syncs = { + fetch: 'pnpm generate:models:fetch', + convert: 'pnpm regenerate:models', + elevenlabs: 'pnpm generate:models:elevenlabs', + provider: 'pnpm exec tsx scripts/sync-provider-models.ts', +} + +export default defineWorkflow('sync-models', { + on: [schedule('0 6 * * *'), manual()], +}).job('sync', { + checkout: { depth: 0 }, + env: { NX_NO_CLOUD: 'true' }, + // Optional: the Vercel gateway answers without a key today. + secrets: { AI_GATEWAY_API_KEY: { optional: true } }, + permissions: { + push: [branch], + pulls: 'create', + issues: 'write', + comment: 'any-pr', + }, + egress: [ + 'registry.npmjs.org', + 'nodejs.org', + 'openrouter.ai', + 'ai-gateway.vercel.sh', + 'ai.gateway.lovable.dev', + 'api.llmgateway.io', + 'neon.com', + 'modelschemas.com', + ], + steps: [ + setup.node(), + setup.pnpm(), + { name: 'Install', run: 'pnpm install --frozen-lockfile' }, + { + name: 'Sync models', + do: async (ctx) => { + let succeeded = false + const outcomes: Array = [] + const key = ctx.secrets.AI_GATEWAY_API_KEY ?? '' + for (const [name, command] of Object.entries(syncs)) { + const { code } = await ctx.sh + .nothrow`AI_GATEWAY_API_KEY=${key} bash -c ${command}` + outcomes.push(`${name}=${code === 0 ? 'success' : 'failure'}`) + } + const failed = outcomes.filter((o) => o.endsWith('=failure')) + + // Open or comment on the failure issue, also when a later step fails. + ctx.afterJob( + async (after) => { + if (succeeded) return + const client = forge(after) + await client.labels.ensure({ + name: 'sync-models', + color: 'B60205', + description: 'Daily model sync failures', + }) + const date = new Date().toISOString().slice(0, 10) + const body = [ + `🚨 Sync Model Metadata failed on ${date}.`, + '', + `Job: ${after.env.TANSTACK_SOURCE_JOB_ID ?? 'unknown'}`, + `Step outcomes: ${outcomes.join(', ')}`, + '', + `Steps that passed were still committed to \`${branch}\`. Close this issue when the sync is green again.`, + ].join('\n') + const [issue] = await client.issues.list({ + label: 'sync-models', + state: 'open', + }) + if (issue) await client.comments.create(issue.number, body) + else + await client.issues.create({ + title: + '🚨🚨🚨 DAILY MODEL SYNC IS FAILING 🚨🚨🚨 Model updates are NOT landing', + body, + labels: ['sync-models'], + }) + }, + { name: 'Open or update failure issue', always: true }, + ) + + await ctx.sh`pnpm format` + + const changed = await ctx.sh.nothrow`git diff --quiet -- packages/` + if (changed.code !== 0) { + await ctx.sh`git config user.name ${'TanStack Source'}` + await ctx.sh`git config user.email ${'ci@source.tanstack.com'}` + await ctx.sh`git add packages/ scripts/openrouter.models.json scripts/openrouter.video-models.json scripts/vercel-gateway.models.json scripts/lovable-gateway.models.json scripts/llmgateway.models.json scripts/neon.models.json scripts/.sync-models-last-run .changeset/` + await ctx.sh`git commit -m ${'chore: sync model metadata'}` + await ctx.sh`git push --force origin ${`HEAD:${branch}`}` + + const client = forge(ctx) + const [open] = await client.pulls.list({ + head: branch, + state: 'open', + }) + if (open === undefined) { + await client.pulls.create({ + title: 'chore: sync model metadata', + head: branch, + base: 'main', + body: prBody, + }) + } + } + + if (failed.length > 0) { + throw new Error( + `A model sync step failed (${failed.join(', ')}). Passing steps were still committed.`, + ) + } + succeeded = true + }, + }, + ], +}) From 2e1375ba11483a5d517a79a301ffbe00d3da9695 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 6 Nov 2026 17:19:48 +0100 Subject: [PATCH 2/2] ci: fix the Source test and E2E workflows, and ignore .source in knip Run the test jobs on the browser image, because pnpm install runs the Playwright postinstall of testing/e2e. Upload the E2E videos, report and traces after each shard, also when it fails. knip ignores .source/, so the Test check does not report the workflow files. Nx Cloud stays off (NX_NO_CLOUD), so the jobs need no secret. --- .source/workflows/bun-test.ts | 27 ++++++------ .source/workflows/e2e.ts | 79 +++++++++++++++-------------------- .source/workflows/pr.ts | 24 ++++++++--- knip.json | 1 + 4 files changed, 68 insertions(+), 63 deletions(-) diff --git a/.source/workflows/bun-test.ts b/.source/workflows/bun-test.ts index 409fb5558e..a6fc09a001 100644 --- a/.source/workflows/bun-test.ts +++ b/.source/workflows/bun-test.ts @@ -1,5 +1,5 @@ -// The Bun-only test suite of @tanstack/ai-isolate-quickjs-bun. -// Ported from .github/workflows/bun-test.yml. +// .github/workflows/bun-test.yml on TanStack Source: the Bun-only tests of +// @tanstack/ai-isolate-quickjs-bun, which the Node test job skips. import { defineWorkflow, pullRequest, setup } from '@tanstack-source/ci' export default defineWorkflow('bun-test', { @@ -13,25 +13,28 @@ export default defineWorkflow('bun-test', { }), ], }).job('test', { - // ponytail: Nx Cloud off, so Nx uses the forge's own task cache. + // The browser image has the Chromium of Playwright 1.57.0. `pnpm install` runs the + // `playwright install chromium` postinstall of testing/e2e, which then downloads nothing. + image: 'browser', + // Nx Cloud is off, so Nx uses the task cache of the forge, and the job needs no secret. env: { NX_NO_CLOUD: 'true' }, + // npm for packages and pnpm, nodejs.org when .nvmrc names another Node than the image, + // and the Playwright CDNs when the locked Playwright changes. egress: [ 'registry.npmjs.org', 'nodejs.org', - // setup.bun() downloads from the GitHub release of Bun. - 'github.com', - 'objects.githubusercontent.com', - 'release-assets.githubusercontent.com', + 'cdn.playwright.dev', + 'playwright.download.prss.microsoft.com', ], steps: [ + // The steps of TanStack/config/.github/setup: pnpm, Node from .nvmrc, then the install. setup.node(), setup.pnpm(), - // The driver needs Bun >= 1.3.14 (see the package `engines` field). - setup.bun({ version: '1.4.4' }), - { name: 'Install', run: 'pnpm install --frozen-lockfile' }, + { name: 'Install dependencies', run: 'pnpm install --frozen-lockfile' }, + // The repository has no .bun-version, so the job uses the bun of the image (1.4.4). + // The driver needs bun 1.3.14 or later. + setup.bun(), { - // `@tanstack/ai-code-mode` is imported at runtime by the driver, so nx - // builds it to `dist/` first via `^build`. name: 'Build package and its workspace dependencies', run: 'pnpm exec nx build @tanstack/ai-isolate-quickjs-bun', }, diff --git a/.source/workflows/e2e.ts b/.source/workflows/e2e.ts index dd98d77cfd..0f79824495 100644 --- a/.source/workflows/e2e.ts +++ b/.source/workflows/e2e.ts @@ -1,7 +1,6 @@ -// E2E tests. Ported from .github/workflows/e2e.yml. +// .github/workflows/e2e.yml on TanStack Source: the E2E shards and the `E2E Tests` gate. import { artifact, - cache, defineWorkflow, pullRequest, push, @@ -12,13 +11,18 @@ export default defineWorkflow('e2e', { on: [pullRequest(), push({ branches: ['main', 'alpha', 'beta', 'rc'] })], }) .job('e2e', { + // The browser image has the browsers of Playwright 1.57.0, the version in pnpm-lock.yaml. + // It replaces the Playwright browser cache of the GitHub workflow. image: 'browser', - size: 'medium', timeoutMinutes: 30, checkout: { depth: 0 }, + // Each shard runs to the end, as with `fail-fast: false` on GitHub. matrix: { shard: [1, 2, 3, 4] }, - // ponytail: Nx Cloud off, so Nx uses the forge's own task cache. + // Nx Cloud is off, so Nx uses the task cache of the forge, and the job needs no secret. env: { NX_NO_CLOUD: 'true' }, + // npm for packages and pnpm, nodejs.org when .nvmrc names another Node than the image, + // and the Playwright CDNs when the locked Playwright changes. + // The tests talk to aimock on 127.0.0.1 only, so they need no provider host and no API key. egress: [ 'registry.npmjs.org', 'nodejs.org', @@ -26,44 +30,31 @@ export default defineWorkflow('e2e', { 'playwright.download.prss.microsoft.com', ], steps: [ - setup.node(), - setup.pnpm(), - { name: 'Install', run: 'pnpm install --frozen-lockfile' }, { - name: 'Cache Playwright Browsers', - do: async (ctx) => { - const { stdout } = await ctx.sh`sha256sum testing/e2e/package.json` - await cache({ - key: `playwright-${stdout.slice(0, 16)}`, - paths: ['~/.cache/ms-playwright'], - restoreKeys: ['playwright-'], - }).do(ctx) - // Upload the results also when the tests fail. - const shard = ctx.matrix.shard - ctx.afterJob( - async (after) => { - await artifact({ - name: `e2e-videos-${shard}`, - paths: ['testing/e2e/test-results/**/*.webm'], - retentionDays: 14, - }).do(after) - await artifact({ - name: `e2e-report-${shard}`, - paths: ['testing/e2e/playwright-report/'], - retentionDays: 14, - }).do(after) - await artifact({ - name: `e2e-traces-${shard}`, - paths: ['testing/e2e/test-results/**/*.zip'], - retentionDays: 14, - }).do(after) - }, - { name: 'Upload test results', always: true }, - ) + // First, so that the uploads are registered before a step can fail. + name: 'Upload results after the job', + do: (ctx) => { + const upload = (name: string, paths: string[]) => + ctx.afterJob( + (after) => + artifact({ + name: `${name}-${ctx.matrix.shard}`, + paths, + retentionDays: 14, + }).do(after), + { always: true }, + ) + upload('e2e-videos', ['testing/e2e/test-results/**/*.webm']) + upload('e2e-report', ['testing/e2e/playwright-report/']) + upload('e2e-traces', ['testing/e2e/test-results/**/*.zip']) }, }, + // The steps of TanStack/config/.github/setup: pnpm, Node from .nvmrc, then the install. + setup.node(), + setup.pnpm(), + { name: 'Install dependencies', run: 'pnpm install --frozen-lockfile' }, { name: 'Build Packages', run: 'pnpm run build:all' }, - // A no-op when the cache restored the browser. + // Downloads nothing while the image has the browser of the locked Playwright version. { name: 'Install Playwright Chromium', run: 'pnpm --filter @tanstack/ai-e2e exec playwright install chromium', @@ -71,22 +62,20 @@ export default defineWorkflow('e2e', { { name: 'Run E2E Tests', do: (ctx) => - ctx.sh`pnpm --filter @tanstack/ai-e2e test:e2e ${`--shard=${ctx.matrix.shard}/4`}`, + ctx.sh`SHARD=${ctx.matrix.shard} bash -ec ${'pnpm --filter @tanstack/ai-e2e test:e2e --shard="$SHARD/4"'}`, }, ], }) - // One check for all shards, to use in a branch rule. + // One result for all shards, as the `E2E Tests` job on GitHub. .job('e2e-result', { needs: ['e2e'], when: 'always', + size: 'small', steps: [ { name: 'Check shard results', - do: (ctx) => { - if (ctx.needs.e2e.result !== 'success') { - throw new Error(`E2E shards: ${ctx.needs.e2e.result}`) - } - }, + do: (ctx) => + ctx.sh`RESULT=${ctx.needs.e2e.result} bash -ec ${'test "$RESULT" = success'}`, }, ], }) diff --git a/.source/workflows/pr.ts b/.source/workflows/pr.ts index e31611fb51..a350ff1758 100644 --- a/.source/workflows/pr.ts +++ b/.source/workflows/pr.ts @@ -1,19 +1,31 @@ -// CI for pull requests. Ported from .github/workflows/pr.yml. -// Not ported yet: `coverage` (needs comment edits on the forge) and `preview` -// (pkg-pr-new needs the pkg.pr.new GitHub App). Both stay on GitHub for now. +// The `Test` check of .github/workflows/pr.yml, on TanStack Source. +// The Coverage and Preview jobs of pr.yml stay on GitHub for now. import { defineWorkflow, pullRequest, setup } from '@tanstack-source/ci' export default defineWorkflow('pr', { on: [pullRequest()], }).job('test', { + // The browser image has the Chromium of Playwright 1.57.0. `pnpm install` runs the + // `playwright install chromium` postinstall of testing/e2e, which then downloads nothing. + image: 'browser', + // `nx affected` compares with the base commit, so the job needs the history. + // nrwl/nx-set-shas is built in: NX_BASE and NX_HEAD come from ctx.base and ctx.head. checkout: { depth: 0 }, - // ponytail: Nx Cloud off, so Nx uses the forge's own task cache. + // Nx Cloud is off, so Nx uses the task cache of the forge, and the job needs no secret. env: { NX_NO_CLOUD: 'true' }, - egress: ['registry.npmjs.org', 'nodejs.org'], + // npm for packages and pnpm, nodejs.org when .nvmrc names another Node than the image, + // and the Playwright CDNs when the locked Playwright changes. + egress: [ + 'registry.npmjs.org', + 'nodejs.org', + 'cdn.playwright.dev', + 'playwright.download.prss.microsoft.com', + ], steps: [ + // The steps of TanStack/config/.github/setup: pnpm, Node from .nvmrc, then the install. setup.node(), setup.pnpm(), - { name: 'Install', run: 'pnpm install --frozen-lockfile' }, + { name: 'Install dependencies', run: 'pnpm install --frozen-lockfile' }, { name: 'Run Checks', run: 'pnpm run test:pr' }, ], }) diff --git a/knip.json b/knip.json index 35d03df499..78bcc67dce 100644 --- a/knip.json +++ b/knip.json @@ -14,6 +14,7 @@ ], "ignore": [ "kiira.config.ts", + ".source/**", "scripts/**", "**/*.test-d.ts", "packages/ai-code-mode-snippets/test-cli/**",