diff --git a/.github/workflows/cd-preview.yml b/.github/workflows/cd-preview.yml index 9986225..90a01f3 100644 --- a/.github/workflows/cd-preview.yml +++ b/.github/workflows/cd-preview.yml @@ -96,6 +96,7 @@ jobs: run: npm run iac:deploy -- --stage "$ALCHEMY_STAGE" - name: Comment preview URLs + continue-on-error: true run: | if [ "${{ steps.deploy-secrets.outputs.ready }}" = "true" ]; then cat > /tmp/code-colony-preview.md <> "$GITHUB_OUTPUT" + elif [ "${{ github.event_name }}" = "push" ]; then + echo "::error::CONVEX_DEPLOY_KEY missing on push to main — convex leg cannot run." + exit 1 else echo "ready=false" >> "$GITHUB_OUTPUT" echo "::notice::CONVEX_DEPLOY_KEY not configured (e.g. fork PR); skipping convex leg with success." diff --git a/__tests__/runCommandTool.test.ts b/__tests__/runCommandTool.test.ts index 4d5b31c..b098b5e 100644 --- a/__tests__/runCommandTool.test.ts +++ b/__tests__/runCommandTool.test.ts @@ -92,6 +92,28 @@ describe("rejectionReason / isAllowed", () => { } }); + it("rejects git --output (=PATH and space-PATH bypass)", () => { + for (const cmd of [ + "git diff --output=/tmp/pwn", + "git diff --output /tmp/pwn", + ]) { + expect(isAllowed(cmd, DEFAULT_ALLOWED_COMMANDS)).toBe(false); + const reason = rejectionReason(cmd, DEFAULT_ALLOWED_COMMANDS); + expect(reason).toMatch(/--output is not allowed/); + } + }); + + it("rejects rg --pre (=CMD and space-CMD subprocess bypass)", () => { + for (const cmd of [ + "rg hello --pre=touch .", + "rg hello --pre touch", + ]) { + expect(isAllowed(cmd, DEFAULT_ALLOWED_COMMANDS)).toBe(false); + const reason = rejectionReason(cmd, DEFAULT_ALLOWED_COMMANDS); + expect(reason).toMatch(/--pre is not allowed/); + } + }); + it("does NOT include node --version in the default allow-list (--eval bypass)", () => { expect(DEFAULT_ALLOWED_COMMANDS).not.toContain("node --version"); expect( @@ -252,4 +274,20 @@ describe("runCommand tool", () => { path: "/exec", } satisfies Partial); }); + + it("strips trailing slash from baseUrl to avoid double-slash paths", async () => { + let requestedUrl = ""; + const fetchImpl: typeof fetch = vi.fn(async (input) => { + requestedUrl = String(input); + return new Response(JSON.stringify({ ok: true, command: "ls", sandboxId: "x", exitCode: 0, stdout: "", stderr: "", durationMs: 1 }), { status: 200 }); + }) as unknown as typeof fetch; + const sandbox = new SandboxClient({ + baseUrl: "https://sandbox.example/", + hmacSecret: "secret", + fetchImpl, + now: () => 1700000000000, + }); + await sandbox.exec({ command: "ls" }); + expect(requestedUrl).toBe("https://sandbox.example/exec"); + }); }); diff --git a/__tests__/triageRouteCore.test.ts b/__tests__/triageRouteCore.test.ts index 101dc9d..12bba44 100644 --- a/__tests__/triageRouteCore.test.ts +++ b/__tests__/triageRouteCore.test.ts @@ -43,6 +43,8 @@ describe("triage route core", () => { it("isBacklogIssueLike validates required fields", () => { expect(isBacklogIssueLike(sampleIssue)).toBe(true); expect(isBacklogIssueLike({ ...sampleIssue, id: 42 })).toBe(false); + expect(isBacklogIssueLike({ ...sampleIssue, source: 42 })).toBe(false); + expect(isBacklogIssueLike({ ...sampleIssue, externalRef: 42 })).toBe(false); expect(isBacklogIssueLike(null)).toBe(false); }); diff --git a/alchemy/dns.ts b/alchemy/dns.ts index 43ac333..473f751 100644 --- a/alchemy/dns.ts +++ b/alchemy/dns.ts @@ -24,7 +24,6 @@ */ import { CustomDomain, - DnsRecords, createCloudflareApi, getZoneByDomain, } from "alchemy/cloudflare"; @@ -53,15 +52,10 @@ export async function dnsStack( `verify delegation before redeploying.`, ); } - const zoneId = zone.id; - // Landing apex/www: deliberately not managed by alchemy. Cloudflare's - // zone-create flow auto-provisioned `@`/`www` CNAME records (Cloudflare's - // standard zone bootstrap), and recreating them collides with code 81053. - // The Pages project (alchemy/landing.ts adopts it) is independently routed - // by those existing records; alchemy can re-take ownership in a follow-up - // PR after the conflicting records are deleted manually. - void args.landing; + void zone.id; + + // Landing apex/www: deliberately not managed by alchemy. if (args.app.hostname) { await CustomDomain("app", { diff --git a/app/server/repoScan.ts b/app/server/repoScan.ts index 67f2df3..9d4a730 100644 --- a/app/server/repoScan.ts +++ b/app/server/repoScan.ts @@ -22,7 +22,7 @@ const REPO_SCAN_TTL_MS = 3_000; const repoScanMemo = createKeyedTtlMemo( (key) => runRepoScanRequest({ - searchParams: new URLSearchParams(key === "__default__" ? "" : `cwd=${key}`), + searchParams: new URLSearchParams(key === "__default__" ? undefined : { cwd: key }), data: demoData, }), { ttlMs: REPO_SCAN_TTL_MS }, diff --git a/apps/landing/src/App.tsx b/apps/landing/src/App.tsx index 9ff33fa..8040c82 100644 --- a/apps/landing/src/App.tsx +++ b/apps/landing/src/App.tsx @@ -58,9 +58,9 @@ export function App() { <> setWaitlistOpen(true)} /> {tw.tickerOn && } - +