diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 52b3735624..7a052d8f72 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -306,7 +306,8 @@ jobs: contents: read actions: write concurrency: - group: capgo-local-services-backend-${{ github.repository }}-${{ matrix.shard }} + # Isolate by event_name so push + pull_request on the same branch do not cancel pending jobs. + group: capgo-local-services-backend-${{ github.event_name }}-${{ github.repository }}-${{ matrix.shard }} cancel-in-progress: false env: SUPABASE_WORKTREE_INSTANCE: backend-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.shard_id }} @@ -442,7 +443,8 @@ jobs: permissions: contents: read concurrency: - group: capgo-local-services-backend-sql-${{ github.repository }} + # Isolate by event_name so push + pull_request on the same branch do not cancel pending jobs. + group: capgo-local-services-backend-sql-${{ github.event_name }}-${{ github.repository }} cancel-in-progress: false env: SUPABASE_WORKTREE_INSTANCE: backend-sql-${{ github.run_id }}-${{ github.run_attempt }} @@ -526,7 +528,8 @@ jobs: permissions: contents: read concurrency: - group: capgo-local-services-backend-sql-catalog-${{ github.repository }} + # Isolate by event_name so push + pull_request on the same branch do not cancel pending jobs. + group: capgo-local-services-backend-sql-catalog-${{ github.event_name }}-${{ github.repository }} cancel-in-progress: false env: SUPABASE_WORKTREE_INSTANCE: backend-sql-catalog-${{ github.run_id }}-${{ github.run_attempt }} @@ -606,7 +609,8 @@ jobs: permissions: contents: read concurrency: - group: capgo-local-services-cloudflare-${{ github.repository }}-${{ matrix.shard }} + # Isolate by event_name so push + pull_request on the same branch do not cancel pending jobs. + group: capgo-local-services-cloudflare-${{ github.event_name }}-${{ github.repository }}-${{ matrix.shard }} cancel-in-progress: false env: SUPABASE_WORKTREE_INSTANCE: cloudflare-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.shard_id }} @@ -755,7 +759,8 @@ jobs: permissions: contents: read concurrency: - group: capgo-local-services-playwright-${{ github.repository }}-${{ matrix.shard }} + # Isolate by event_name so push + pull_request on the same branch do not cancel pending jobs. + group: capgo-local-services-playwright-${{ github.event_name }}-${{ github.repository }}-${{ matrix.shard }} cancel-in-progress: false strategy: fail-fast: false @@ -1203,7 +1208,8 @@ jobs: contents: read actions: write concurrency: - group: capgo-local-services-cli-${{ github.repository }} + # Isolate by event_name so push + pull_request on the same branch do not cancel pending jobs. + group: capgo-local-services-cli-${{ github.event_name }}-${{ github.repository }} cancel-in-progress: false env: SUPABASE_WORKTREE_INSTANCE: cli-${{ github.run_id }}-${{ github.run_attempt }} diff --git a/scripts/setup-bun.sh b/scripts/setup-bun.sh index ff4229eeee..2ff1f24aed 100755 --- a/scripts/setup-bun.sh +++ b/scripts/setup-bun.sh @@ -34,7 +34,21 @@ archive_path="$tmp_dir/$asset_name" extract_path="$tmp_dir/extract" asset_url="https://github.com/oven-sh/bun/releases/download/bun-v${BUN_VERSION}/${asset_name}" -curl -fsSL "$asset_url" -o "$archive_path" +download_attempts=5 +download_ok=0 +for attempt in $(seq 1 "$download_attempts"); do + if curl --retry 3 --retry-delay 2 --retry-all-errors -fsSL "$asset_url" -o "$archive_path"; then + download_ok=1 + break + fi + echo "Bun download failed (attempt ${attempt}/${download_attempts}), retrying..." >&2 + sleep $((attempt * 2)) +done + +if [ "$download_ok" -ne 1 ]; then + echo "Failed to download Bun from $asset_url after ${download_attempts} attempts" >&2 + exit 1 +fi if command -v shasum >/dev/null 2>&1; then echo "$asset_sha256 $archive_path" | shasum -a 256 -c - diff --git a/scripts/supabase-worktree.ts b/scripts/supabase-worktree.ts index addee1ee91..eb08bfd070 100644 --- a/scripts/supabase-worktree.ts +++ b/scripts/supabase-worktree.ts @@ -322,13 +322,60 @@ function isTransientDockerPortBindFailure(output: string): boolean { || /failed to bind host port/i.test(output) } +/** + * Keep worktree host ports out of the kernel ephemeral pool. + * + * CI offsets land in 32k–60k, which overlaps Linux's default local port range. + * Without reserving them, outbound sockets can steal a Supabase port and make + * `docker` fail with "address already in use" on an otherwise idle runner. + */ +function reserveWorktreePortsFromEphemeralPool(repoRoot: string): void { + if (process.platform !== 'linux') + return + if (!process.env.SUPABASE_WORKTREE_PORT_OFFSET && !process.env.CI) + return + + const { cfg } = ensureWorktreeSupabaseDir(repoRoot) + const ports = Object.values(cfg.ports).filter(port => Number.isFinite(port)).sort((a, b) => a - b) + if (ports.length === 0) + return + + const reserved = ports.join(',') + const result = spawnSync('sudo', ['sysctl', '-w', `net.ipv4.ip_local_reserved_ports=${reserved}`], { + encoding: 'utf8', + }) + if ((result.status ?? 1) !== 0) { + const fallback = spawnSync('sysctl', ['-w', `net.ipv4.ip_local_reserved_ports=${reserved}`], { + encoding: 'utf8', + }) + if ((fallback.status ?? 1) !== 0) { + console.warn(`Could not reserve Supabase ports from the ephemeral pool: ${reserved}`) + return + } + } + console.error(`Reserved Supabase worktree ports from ephemeral pool: ${reserved}`) +} + +function freeHostPorts(ports: number[]): void { + if (process.platform === 'win32' || ports.length === 0) + return + + for (const port of ports) { + spawnSync('fuser', ['-k', `${port}/tcp`], { stdio: 'ignore' }) + } +} + /** * `supabase start` can fail on GitHub runners with a transient Docker port bind * (`address already in use`) after a partial start/stop. Retry only that class of * failure so permanent start errors fail fast. */ function runSupabaseStartWithRetry(args: string[], repoRoot: string): number { - const maxAttempts = 3 + const { cfg } = ensureWorktreeSupabaseDir(repoRoot) + const ports = Object.values(cfg.ports).filter(port => Number.isFinite(port)) + reserveWorktreePortsFromEphemeralPool(repoRoot) + + const maxAttempts = 5 for (let attempt = 1; attempt <= maxAttempts; attempt++) { const { status, output } = runSupabase(args, repoRoot, { captureOutput: true }) if (status === 0) @@ -338,6 +385,7 @@ function runSupabaseStartWithRetry(args: string[], repoRoot: string): number { return status console.error(`Supabase start hit a transient Docker port bind (attempt ${attempt}/${maxAttempts}); stopping and retrying...`) runSupabase(['stop', '--no-backup'], repoRoot) + freeHostPorts(ports) spawnSync(process.platform === 'win32' ? 'timeout' : 'sleep', process.platform === 'win32' ? ['/T', '2', '/NOBREAK'] : ['2']) } return 1 diff --git a/src/components/dashboard/AppOnboardingFlow.vue b/src/components/dashboard/AppOnboardingFlow.vue index 9e4f5f687d..8a8f8fa294 100644 --- a/src/components/dashboard/AppOnboardingFlow.vue +++ b/src/components/dashboard/AppOnboardingFlow.vue @@ -853,11 +853,10 @@ function goToInstallStep() { flowStep.value = 'install' } -function openDashboard() { - if (!createdApp.value) - return - - router.push(`/app/${encodeURIComponent(createdApp.value.app_id)}`) +async function openDashboard() { + // Skipping the CLI step should still populate the app with demo data + // so the dashboard is usable to explore Capgo before a real upload. + await seedDemoData() } onMounted(async () => { @@ -1468,9 +1467,12 @@ watch(suggestedAppId, (value) => {
-
@@ -1599,12 +1601,15 @@ watch(suggestedAppId, (value) => {
- -