diff --git a/.github/workflows/docs-e2e.yml b/.github/workflows/docs-e2e.yml index 2db833df0705d..7a6b299186afe 100644 --- a/.github/workflows/docs-e2e.yml +++ b/.github/workflows/docs-e2e.yml @@ -5,11 +5,12 @@ on: types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] branches: ['master'] paths: - - 'apps/docs/content/guides/getting-started/quickstarts/nextjs.mdx' - - 'apps/docs/content/_partials/quickstart_db_setup.mdx' - - 'apps/docs/content/_partials/api_settings.mdx' + - 'apps/docs/content/guides/**/*.mdx' + - 'apps/docs/content/troubleshooting/**/*.mdx' + - 'apps/docs/content/_partials/**' - 'e2e/docs/features/**' - 'e2e/docs/utils/**' + - 'e2e/docs/scripts/**' - 'e2e/docs/playwright.config.ts' - 'e2e/docs/package.json' - 'e2e/docs/tsconfig.json' @@ -22,6 +23,11 @@ on: required: false default: 'https://supabase.com' type: string + page_paths: + description: 'Comma-separated /docs/... paths to test (required for manual runs)' + required: false + default: '' + type: string concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -46,17 +52,59 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false + # Need full history on PRs so we can diff against the base branch. + # Use string '0' — numeric 0 is falsy in GitHub Actions expressions. + fetch-depth: ${{ github.event_name == 'pull_request' && '0' || '1' }} sparse-checkout: | e2e/docs scripts patches + apps/docs/content/guides + apps/docs/content/troubleshooting + apps/docs/content/_partials + apps/docs/scripts/federated-content/sources + + - name: Use Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version-file: '.nvmrc' + + # Map changed owned content (guides, troubleshooting, partials) to page + # URLs. Harness-only PRs resolve to skip=true and exit before Playwright. + - name: Resolve docs E2E scope + id: scope + env: + EVENT_NAME: ${{ github.event_name }} + BASE_REF: ${{ github.base_ref }} + PAGE_PATHS_INPUT: ${{ inputs.page_paths }} + run: | + if [ "$EVENT_NAME" = "workflow_dispatch" ]; then + if [ -z "$PAGE_PATHS_INPUT" ]; then + echo "skip=true" >> "$GITHUB_OUTPUT" + echo "paths=" >> "$GITHUB_OUTPUT" + echo "Manual run requires the page_paths input." + exit 0 + fi + echo "skip=false" >> "$GITHUB_OUTPUT" + printf 'paths=%s\n' "$PAGE_PATHS_INPUT" >> "$GITHUB_OUTPUT" + exit 0 + fi + + git diff --name-only --diff-filter=ACMR "origin/$BASE_REF"...HEAD \ + | node --experimental-strip-types e2e/docs/scripts/resolve-docs-scope.ts + + - name: Skip Playwright (no in-scope pages) + if: steps.scope.outputs.skip == 'true' + run: echo "No in-scope docs pages changed; skipping Playwright suite." - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + if: steps.scope.outputs.skip != 'true' name: Install pnpm with: run_install: false - - name: Use Node.js + - name: Enable pnpm store cache + if: steps.scope.outputs.skip != 'true' uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: '.nvmrc' @@ -65,7 +113,7 @@ jobs: # Vercel skips the docs preview when a PR only changes the harness # (e2e/docs, workflow). Wait for a preview only when apps/docs changed. - name: Detect docs app changes - if: github.event_name == 'pull_request' + if: steps.scope.outputs.skip != 'true' && github.event_name == 'pull_request' id: filter uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 with: @@ -80,7 +128,7 @@ jobs: # those — then resolve the deployment it points to via Vercel's own API # to get the actual preview URL. See scripts/waitForVercelDocsPreview.js. - name: Wait for Vercel docs preview - if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && steps.filter.outputs.docs_app == 'true' + if: steps.scope.outputs.skip != 'true' && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && steps.filter.outputs.docs_app == 'true' id: deployment run: node scripts/waitForVercelDocsPreview.js env: @@ -90,6 +138,7 @@ jobs: VERCEL_TEAM_ID: ${{ secrets.VERCEL_TEAM_ID }} - name: Resolve base URL + if: steps.scope.outputs.skip != 'true' id: base-url env: EVENT_NAME: ${{ github.event_name }} @@ -110,20 +159,24 @@ jobs: fi - name: Install dependencies + if: steps.scope.outputs.skip != 'true' run: pnpm install --frozen-lockfile --filter=e2e-docs... - name: Install Playwright Chromium + if: steps.scope.outputs.skip != 'true' run: pnpm -C e2e/docs exec playwright install chromium --with-deps --only-shell - name: Run docs E2E + if: steps.scope.outputs.skip != 'true' working-directory: e2e/docs run: pnpm run e2e:docs env: PLAYWRIGHT_BASE_URL: ${{ steps.base-url.outputs.url }} + DOCS_E2E_PAGE_PATHS: ${{ steps.scope.outputs.paths }} VERCEL_AUTOMATION_BYPASS_SECRET: ${{ steps.base-url.outputs.use_bypass == 'true' && secrets.VERCEL_AUTOMATION_BYPASS_DOCS || '' }} - name: Upload Playwright report - if: failure() + if: failure() && steps.scope.outputs.skip != 'true' uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: docs-playwright-report diff --git a/apps/docs/public/humans.txt b/apps/docs/public/humans.txt index 43e4768008df4..47fd7062104b8 100644 --- a/apps/docs/public/humans.txt +++ b/apps/docs/public/humans.txt @@ -282,6 +282,7 @@ Sugu Sougoumarane Supun Sudaraka Kalidasa Taha Le Bras Tanun Chalermsinsuwan +Tarun Khandelwal Taryn King Terry Sutton Terry Wilcox diff --git a/apps/studio/components/interfaces/Integrations/Vercel/VercelIntegration.utils.test.ts b/apps/studio/components/interfaces/Integrations/Vercel/VercelIntegration.utils.test.ts new file mode 100644 index 0000000000000..00cd23ecbf713 --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/Vercel/VercelIntegration.utils.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, test } from 'vitest' + +import { + getValidVercelReturnUrl, + isVercelUrl, +} from '@/components/interfaces/Integrations/Vercel/VercelIntegration.utils' + +describe('isVercelUrl', () => { + test('accepts https vercel.com urls', () => { + expect(isVercelUrl('https://vercel.com/callback')).toBe(true) + }) + + test('rejects non-vercel and invalid urls', () => { + expect(isVercelUrl('https://example.com')).toBe(false) + expect(isVercelUrl('http://vercel.com')).toBe(false) + expect(isVercelUrl('not-a-url')).toBe(false) + }) +}) + +describe('getValidVercelReturnUrl', () => { + test('returns the url when it is a valid vercel return url', () => { + expect(getValidVercelReturnUrl('https://vercel.com/callback')).toBe( + 'https://vercel.com/callback' + ) + }) + + test('returns undefined for missing or invalid next values', () => { + expect(getValidVercelReturnUrl(undefined)).toBeUndefined() + expect(getValidVercelReturnUrl('https://example.com')).toBeUndefined() + expect(getValidVercelReturnUrl('not-a-url')).toBeUndefined() + }) +}) diff --git a/apps/studio/components/interfaces/Integrations/Vercel/VercelIntegration.utils.ts b/apps/studio/components/interfaces/Integrations/Vercel/VercelIntegration.utils.ts index a6802341ab751..05db949804dd3 100644 --- a/apps/studio/components/interfaces/Integrations/Vercel/VercelIntegration.utils.ts +++ b/apps/studio/components/interfaces/Integrations/Vercel/VercelIntegration.utils.ts @@ -10,6 +10,12 @@ export function isVercelUrl(url: string): boolean { } } +/** Returns `next` when it is a safe Vercel return URL; otherwise undefined. */ +export function getValidVercelReturnUrl(next: string | undefined): string | undefined { + if (typeof next === 'string' && isVercelUrl(next)) return next + return undefined +} + export function findVercelIntegrationByConfigurationId( integrations: Integration[] | undefined, configurationId: string | undefined diff --git a/apps/studio/components/interfaces/ProjectCreation/FreeProjectLimitWarning.tsx b/apps/studio/components/interfaces/ProjectCreation/FreeProjectLimitWarning.tsx index e3b8389232c73..ba554365f5a95 100644 --- a/apps/studio/components/interfaces/ProjectCreation/FreeProjectLimitWarning.tsx +++ b/apps/studio/components/interfaces/ProjectCreation/FreeProjectLimitWarning.tsx @@ -6,9 +6,13 @@ import type { MemberWithFreeProjectLimit } from '@/data/organizations/free-proje interface FreeProjectLimitWarningProps { membersExceededLimit: MemberWithFreeProjectLimit[] + showVercelReturnHint?: boolean } -export const FreeProjectLimitWarning = ({ membersExceededLimit }: FreeProjectLimitWarningProps) => { +export const FreeProjectLimitWarning = ({ + membersExceededLimit, + showVercelReturnHint = false, +}: FreeProjectLimitWarningProps) => { return ( + {showVercelReturnHint && ( +

+ Or return to Vercel and restart with a different organization. +

+ )} canCreateProject: boolean @@ -29,7 +32,7 @@ interface ProjectCreationFooterProps { organizationProjects: OrgProject[] isCreatingNewProject: boolean isSuccessNewProject: boolean - hideCancelButton: boolean + cancelAction?: ProjectCreationCancelAction } export const ProjectCreationFooter = ({ @@ -39,9 +42,10 @@ export const ProjectCreationFooter = ({ organizationProjects, isCreatingNewProject, isSuccessNewProject, - hideCancelButton, + cancelAction = 'studio', }: ProjectCreationFooterProps) => { const router = useRouter() + const { next } = useParams() const { data: currentOrg } = useSelectedOrganizationQuery() const isFreePlan = currentOrg?.plan?.id === 'free' const { lastVisitedOrganization } = useLastVisitedOrganization() @@ -53,6 +57,9 @@ export const ProjectCreationFooter = ({ ? 0 : monthlyInstancePrice(instanceSize) - availableComputeCredits + const vercelReturnUrl = getValidVercelReturnUrl(next) + const canReturnToVercel = cancelAction === 'vercel' && vercelReturnUrl !== undefined + // [kevin] This will eventually all be provided by a new API endpoint to preview and validate project creation, this is just for kaizen now const monthlyComputeCosts = // current project costs @@ -66,6 +73,17 @@ export const ProjectCreationFooter = ({ // compute credits 10 + const onCancel = () => { + if (canReturnToVercel && vercelReturnUrl) { + window.location.href = vercelReturnUrl + return + } + + // Fall back to Studio when cancelAction is studio, or when vercel next is missing/invalid + if (!!lastVisitedOrganization) router.push(`/org/${lastVisitedOrganization}`) + else router.push('/organizations') + } + return (
@@ -169,16 +187,13 @@ export const ProjectCreationFooter = ({
- {!hideCancelButton && ( + {cancelAction !== 'hidden' && ( )}