Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 63 additions & 6 deletions tanstack-migrator/server/db/catalog.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
/**
* Read-only client for the decocms platform `sites` catalog (a DIFFERENT
* Supabase project than the MCP's own sitemig_* DB).
* Client for the decocms platform `sites` catalog (a DIFFERENT Supabase project
* than the MCP's own sitemig_* DB).
*
* Powers the register modal's repo autocomplete: search by site name and
* pre-fill both the GitHub repo and the production URL — no GitHub API calls,
* so no rate limit. Opt-in: only active when DECOCMS_SUPABASE_URL +
* DECOCMS_SUPABASE_KEY are set. This module ONLY ever runs SELECTs.
* Mostly reads (register-modal repo autocomplete: search by site name to
* pre-fill the GitHub repo + production URL, no GitHub API calls / no rate
* limit). The ONE write is setSitePlatform — it flags the migrated -tanstack
* repo as a Cloudflare Workers Builds site so the Fresh/Deno k8s deployer stops
* watching it. Opt-in: active only when DECOCMS_SUPABASE_URL +
* DECOCMS_SUPABASE_KEY (a service-role key) are set.
*/

import { createClient, type SupabaseClient } from "@supabase/supabase-js";
Expand Down Expand Up @@ -150,3 +152,58 @@ export async function catalogByNames(
}
return map;
}

export const CFWORKERS_BUILDS_PLATFORM = "cfworkers-builds";

/**
* Flag a repo's catalog row with `metadata.platform` (default
* "cfworkers-builds") so the deco Fresh/Deno k8s deployer stops watching the
* migrated -tanstack repo (it keeps trying to deploy it and leaves a mess).
*
* Read-merge-write on the single row matched by github_repo_url — never clobbers
* other metadata keys. Fully guarded + best-effort: returns {ok:false, reason}
* instead of throwing (catalog not configured / row not indexed yet / RLS), so
* it can never block a migration. Idempotent.
*/
export async function setSitePlatform(
repoFull: string,
platform: string = CFWORKERS_BUILDS_PLATFORM,
): Promise<{ ok: boolean; reason: string }> {
const client = getCatalogClient();
if (!client) return { ok: false, reason: "catalog not configured" };
const repo = repoFull.trim().replace(/\.git$/, "");
if (!repo.includes("/"))
return { ok: false, reason: `invalid repo ${repoFull}` };

try {
const { data, error } = await client
.from("sites")
.select("github_repo_url, metadata")
.ilike("github_repo_url", `%${repo}%`)
.limit(5);
if (error) return { ok: false, reason: error.message };
const rows = (data ?? []) as Array<{
github_repo_url: string | null;
metadata: Record<string, unknown> | null;
}>;
// exact owner/repo match (ilike is fuzzy — avoid touching a similarly-named repo)
const row = rows.find((r) => toOwnerRepo(r.github_repo_url) === repo);
if (!row) return { ok: false, reason: "repo not in catalog yet" };

const meta =
row.metadata && typeof row.metadata === "object" ? row.metadata : {};
if (meta.platform === platform) return { ok: true, reason: "already set" };

const { error: upErr } = await client
.from("sites")
.update({ metadata: { ...meta, platform } })
.eq("github_repo_url", row.github_repo_url);
if (upErr) return { ok: false, reason: upErr.message };
return { ok: true, reason: "updated" };
} catch (err) {
return {
ok: false,
reason: err instanceof Error ? err.message : String(err),
};
}
}
9 changes: 9 additions & 0 deletions tanstack-migrator/server/engine/phases/creating-repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* durable GitHub grant used by the sandbox for pushes.
*/

import { setSitePlatform } from "../../db/catalog.ts";
import { addEvent } from "../../db/events.ts";
import type { SiteRow } from "../../db/types.ts";
import { updateSite } from "../../db/sites.ts";
Expand Down Expand Up @@ -59,6 +60,14 @@ export async function creatingRepo(
}
await addEvent(site.id, `Repo ${targetRepo} ensured on GitHub`);

// Flag it as a CF Workers Builds site so the Fresh/Deno k8s deployer ignores
// it. Best-effort + early — the catalog row often isn't indexed yet at this
// point (the deploy phase re-asserts it once it is).
const plat = await setSitePlatform(targetRepo);
if (plat.ok && plat.reason === "updated") {
await addEvent(site.id, `Catalog: ${targetRepo} marked cfworkers-builds`);
}

// main must exist (repos are created empty) — it's the PR base — and the
// work branch must exist BEFORE SANDBOX_START: sandbox clone + decopilot
// threads are pinned to it.
Expand Down
22 changes: 22 additions & 0 deletions tanstack-migrator/server/engine/phases/deploying-cf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { createRun, finishRun } from "../../db/runs.ts";
import { getSite, updateSite } from "../../db/sites.ts";
import type { SiteRow } from "../../db/types.ts";
import { manualCfInstructions } from "../../lib/cloudflare.ts";
import { setSitePlatform } from "../../db/catalog.ts";
import { parseRepo } from "../../lib/github.ts";
import type { WorkerCtx } from "../../lib/mesh.ts";
import { previewRendersRealHtml } from "../../lib/preview.ts";
Expand Down Expand Up @@ -150,6 +151,27 @@ export async function deployingCf(
);
}

// Now that the repo is pushed + indexed, flag it as CF Workers Builds so
// the Fresh/Deno k8s deployer stops watching it (best-effort, never blocks).
const plat = await setSitePlatform(current.target_repo!);
if (plat.ok && plat.reason === "updated") {
await addEvent(
site.id,
`Catalog: ${current.target_repo} marked cfworkers-builds — Fresh/Deno bot will ignore it`,
);
} else if (
!plat.ok &&
!["catalog not configured", "repo not in catalog yet"].includes(
plat.reason,
)
) {
await addEvent(
site.id,
`Catalog platform flag not set (${plat.reason.slice(0, 120)})`,
"warn",
);
}

await advanceAfterDeploy(current, {
cf_project_name: workerName,
cf_deploy_url: deployUrl,
Expand Down
2 changes: 2 additions & 0 deletions tanstack-migrator/server/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
createSiteResumeTool,
createSiteResetTool,
createSiteRetryTool,
createSiteSetPlatformTool,
createSiteTerminalTool,
} from "./sites.ts";
import { createSyncDecofileInstallTool } from "./sync.ts";
Expand All @@ -41,6 +42,7 @@ export const tools = [
createSiteResumeTool,
createSiteRetryTool,
createSiteResetTool,
createSiteSetPlatformTool,
createSiteEnqueueTool,
createSiteReorderTool,
createSiteMarkDoneTool,
Expand Down
54 changes: 54 additions & 0 deletions tanstack-migrator/server/tools/sites.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import { createTool } from "@decocms/runtime/tools";
import { z } from "zod";
import {
catalogByNames,
CFWORKERS_BUILDS_PLATFORM,
isCatalogConfigured,
searchSiteCatalog,
setSitePlatform,
} from "../db/catalog.ts";
import { loadConnection, saveConnection } from "../db/connections.ts";
import { getCostSnapshot, refreshCostSnapshotIfStale } from "../db/cost.ts";
Expand Down Expand Up @@ -438,6 +440,58 @@ export const createSiteResetTool = (env: Env) =>
},
});

export const createSiteSetPlatformTool = (env: Env) =>
createTool({
id: "SITE_SET_PLATFORM",
description:
"Flag the migrated -tanstack repo in the decocms catalog with metadata.platform (default 'cfworkers-builds') so the deco Fresh/Deno k8s deployer stops watching it (it keeps trying to deploy the -tanstack repo and leaves a broken deployment). Use it on existing -tanstack sites that the bot is fighting. New migrations set this automatically at repo creation + deploy. Pass a siteId (uses its -tanstack repo) or repo (owner/name) directly.",
inputSchema: z.object({
siteId: z
.string()
.optional()
.describe("Migration site id (uses its -tanstack repo)."),
repo: z
.string()
.optional()
.describe('Target -tanstack repo "owner/name" (overrides the site).'),
platform: z
.string()
.optional()
.describe(`Platform value (default ${CFWORKERS_BUILDS_PLATFORM}).`),
}),
outputSchema: z.object({
ok: z.boolean(),
repo: z.string(),
platform: z.string(),
reason: z.string(),
}),
execute: async ({ context }) => {
requireConnectionId(env);
if (!isCatalogConfigured()) {
throw new Error(
"Catalog not configured (DECOCMS_SUPABASE_URL/KEY missing) — cannot set the platform flag.",
);
}
const site = context.siteId ? await getSite(context.siteId) : null;
if (context.siteId && !site) throw new Error("Site not found");
const repo = (context.repo ?? site?.target_repo ?? "")
.trim()
.replace(/^https?:\/\/github\.com\//, "")
.replace(/\.git$/, "");
if (!repo) {
throw new Error(
"No target repo — pass repo, or a siteId whose migration has a -tanstack repo.",
);
}
const platform = context.platform ?? CFWORKERS_BUILDS_PLATFORM;
const result = await setSitePlatform(repo, platform);
if (site && result.ok && result.reason === "updated") {
await addEvent(site.id, `Catalog: ${repo} marked ${platform}`);
}
return { ok: result.ok, repo, platform, reason: result.reason };
},
});

export const createSiteMarkDoneTool = (env: Env) =>
createTool({
id: "SITE_MARK_DONE",
Expand Down
Loading