mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 01:15:03 +03:00
docs: warn that default signOut scope revokes all sessions (#50119)
Warn that default signOut scope revokes all sessions. Motivation: https://github.com/supabase/ssr/issues/68 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Documentation** * Clarified that signing out without a specified scope ends all sessions by default. * Added guidance for using a local sign-out scope to preserve sessions on other devices and browsers. * Documented the invalid refresh token errors that may occur when other sessions are revoked. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
045f29ecb6
commit
028e05205b
9 files changed
+411
-85
No files matched your search
@@ -129,6 +129,10 @@ jobs:
|
||||
|
||||
# Vercel skips the docs preview when a PR only changes the harness
|
||||
# (e2e/docs, workflow), so wait for a preview only when apps/docs changed.
|
||||
# When apps/docs changed earlier in the PR but not in the head commit,
|
||||
# Vercel skips the head build as not affected and its URL serves a
|
||||
# placeholder page; the script then uses the newest READY preview from
|
||||
# an earlier commit of the PR, which serves the same docs content.
|
||||
#
|
||||
# Vercel's GitHub App stopped writing GitHub Deployment objects on
|
||||
# 2026-02-17 (broken app auth), so vercel/wait-for-deployment-action
|
||||
@@ -146,6 +150,7 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
VERCEL_STATUS_CONTEXT: 'Vercel – docs'
|
||||
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
|
||||
VERCEL_TEAM_ID: ${{ secrets.VERCEL_TEAM_ID }}
|
||||
|
||||
@@ -120,7 +120,10 @@ jobs:
|
||||
cache: 'pnpm'
|
||||
|
||||
# Vercel skips the preview when only the harness changed, so wait for one
|
||||
# only when apps/www changed. See scripts/waitForVercelPreview.js.
|
||||
# only when apps/www changed. A head commit that leaves apps/www untouched
|
||||
# gets a skipped build whose URL serves a placeholder page, so the script
|
||||
# falls back to the newest READY preview from an earlier commit of the PR.
|
||||
# See scripts/waitForVercelPreview.js.
|
||||
- name: Wait for Vercel www preview
|
||||
if: steps.scope.outputs.skip == 'false' && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && steps.changes.outputs.www_app == 'true'
|
||||
id: deployment
|
||||
@@ -129,6 +132,7 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
VERCEL_STATUS_CONTEXT: 'Vercel – zone-www-dot-com'
|
||||
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
|
||||
VERCEL_TEAM_ID: ${{ secrets.VERCEL_TEAM_ID }}
|
||||
|
||||
@@ -46,6 +46,8 @@ It is likely that the refresh token sent from the browser to your server is stal
|
||||
|
||||
When you receive this error on the server-side, try to defer rendering to the browser where the client library can access an up-to-date refresh token and present the user with a better experience.
|
||||
|
||||
A common cause is calling `supabase.auth.signOut()` without a `scope`. It defaults to `scope: 'global'`, which revokes the refresh token for every session that user has. That includes other tabs, devices, and any SSR context still holding the old token. See [Sign out and scopes](/docs/guides/auth/signout#sign-out-and-scopes). Pass `{ scope: 'local' }` if you only want to end the current session.
|
||||
|
||||
### Should I set a shorter `Max-Age` parameter on the cookies?
|
||||
|
||||
The `Max-Age` or `Expires` cookie parameters only control whether the browser sends the value to the server. Since a refresh token represents the long-lived authentication session of the user on that browser, setting a short `Max-Age` or `Expires` parameter on the cookies only results in a degraded user experience.
|
||||
|
||||
@@ -89,6 +89,16 @@ Supabase Auth allows you to specify three different scopes for when a user invok
|
||||
|
||||
You can invoke these by providing the `scope` option:
|
||||
|
||||
<Admonition type="caution">
|
||||
|
||||
JavaScript, Swift, Python, and C# default to the `global` scope. Dart and Kotlin default to `local`.
|
||||
|
||||
With the `global` scope, calling `signOut()` on one device revokes the refresh tokens for every session that user has, including other devices and browsers. Those other sessions fail with `AuthApiError: Invalid Refresh Token: Refresh Token Not Found` the next time they try to refresh, even though no one signed out on that device.
|
||||
|
||||
If your app expects independent sessions per device, pass the `local` scope explicitly, as shown in the examples below.
|
||||
|
||||
</Admonition>
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
|
||||
+4
-2
@@ -211,8 +211,10 @@ owned docs content, partials, or `e2e/docs`.
|
||||
1. Diff the pull request against its base branch and resolve in-scope page paths.
|
||||
2. Skip Playwright when nothing in scope changed.
|
||||
3. When `apps/docs` changed, wait for the Vercel docs preview and set
|
||||
`PLAYWRIGHT_BASE_URL` to that preview. When no preview resolves, skip rather than
|
||||
test against production.
|
||||
`PLAYWRIGHT_BASE_URL` to that preview. If Vercel skipped the head commit's
|
||||
build as not affected, the newest ready preview from an earlier commit of the
|
||||
pull request is used instead. When no preview resolves, skip rather than test
|
||||
against production.
|
||||
4. Run the suite with `DOCS_E2E_PAGE_PATHS` set to the resolved list.
|
||||
|
||||
Draft pull requests stay skipped until you mark them ready for review. Manual
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
} from '../utils/axe-helpers.js'
|
||||
import {
|
||||
articleSelectorForPagePath,
|
||||
browserLikeUserAgent,
|
||||
checkLinkFromBrowser,
|
||||
collectDocsOwnedLinks,
|
||||
} from '../utils/docs-links.js'
|
||||
|
||||
@@ -56,22 +56,17 @@ test.describe('Docs owned pages', () => {
|
||||
await expect(article, 'Page article should be present').toBeVisible()
|
||||
|
||||
const links = await collectDocsOwnedLinks(page, baseURL!, articleSelector)
|
||||
const userAgent = await browserLikeUserAgent(page)
|
||||
|
||||
for (const url of links) {
|
||||
try {
|
||||
const linkResponse = await page.request.get(url, { headers: { 'user-agent': userAgent } })
|
||||
expect
|
||||
.soft(linkResponse.ok(), `${url} should resolve (status ${linkResponse.status()})`)
|
||||
.toBeTruthy()
|
||||
} catch (error) {
|
||||
expect
|
||||
.soft(
|
||||
null,
|
||||
`${url} should be reachable (${error instanceof Error ? error.message : error})`
|
||||
)
|
||||
.toBeTruthy()
|
||||
}
|
||||
const result = await checkLinkFromBrowser(page, url)
|
||||
expect
|
||||
.soft(
|
||||
result.ok,
|
||||
result.error
|
||||
? `${url} should be reachable (${result.error})`
|
||||
: `${url} should resolve (status ${result.status})`
|
||||
)
|
||||
.toBeTruthy()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -55,8 +55,23 @@ export async function collectDocsOwnedLinks(
|
||||
return [...links].sort()
|
||||
}
|
||||
|
||||
// Vercel bot protection blocks the HeadlessChrome UA on some routes; strip it.
|
||||
export async function browserLikeUserAgent(page: Page): Promise<string> {
|
||||
const userAgent = await page.evaluate(() => navigator.userAgent)
|
||||
return userAgent.replace('HeadlessChrome', 'Chrome')
|
||||
export type LinkCheckResult = {
|
||||
ok: boolean
|
||||
status: number
|
||||
error?: string
|
||||
}
|
||||
|
||||
// The docs middleware rewrites /reference/* requests whose user agent looks like a bot
|
||||
// (isbot, which matches HeadlessChrome) to /api/crawlers. Fetching from inside the page
|
||||
// keeps the browser's own user agent and network stack, so a link resolves the same way
|
||||
// page.goto resolves it in this suite.
|
||||
export async function checkLinkFromBrowser(page: Page, url: string): Promise<LinkCheckResult> {
|
||||
return page.evaluate(async (linkUrl) => {
|
||||
try {
|
||||
const response = await fetch(linkUrl)
|
||||
return { ok: response.ok, status: response.status }
|
||||
} catch (error) {
|
||||
return { ok: false, status: 0, error: error instanceof Error ? error.message : String(error) }
|
||||
}
|
||||
}, url)
|
||||
}
|
||||
+160
-63
@@ -4,56 +4,162 @@
|
||||
// builds fine. Poll the project's Vercel commit status instead, then resolve
|
||||
// the actual preview URL via Vercel's own deployments API.
|
||||
//
|
||||
// A build that Vercel's Ignored Build Step skips still gets a successful
|
||||
// commit status, but the deployment behind it is CANCELED and its URL serves
|
||||
// a placeholder page with a 200 status. Such a preview is not testable, so
|
||||
// the URL is resolved from the newest earlier commit of the pull request
|
||||
// whose deployment is READY. Vercel skips a build only when the commit does
|
||||
// not affect the project, so that earlier preview serves the same content.
|
||||
//
|
||||
// Set VERCEL_STATUS_CONTEXT to the project's commit status name, e.g.
|
||||
// "Vercel – docs" or "Vercel – zone-www-dot-com".
|
||||
// "Vercel – docs" or "Vercel – zone-www-dot-com". PR_NUMBER enables the
|
||||
// fallback to earlier commits of the pull request.
|
||||
const { appendFileSync } = require('fs')
|
||||
|
||||
const TIMEOUT_MS = 900_000
|
||||
const POLL_INTERVAL_MS = 15_000
|
||||
const GITHUB_PAGE_SIZE = 100
|
||||
|
||||
async function fetchLatestStatus(repository, sha, githubToken, statusContext) {
|
||||
const url = `https://api.github.com/repos/${repository}/commits/${sha}/statuses`
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${githubToken}`,
|
||||
Accept: 'application/vnd.github+json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch commit statuses: ${response.status} ${response.statusText}`)
|
||||
function deploymentIdFromTargetUrl(targetUrl) {
|
||||
let rawId
|
||||
try {
|
||||
rawId = new URL(targetUrl).pathname.split('/').filter(Boolean).pop()
|
||||
} catch {
|
||||
rawId = undefined
|
||||
}
|
||||
|
||||
const statuses = await response.json()
|
||||
|
||||
return statuses
|
||||
.filter((status) => status.context === statusContext)
|
||||
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())[0]
|
||||
}
|
||||
|
||||
async function resolveDeploymentUrl(targetUrl, vercelToken, teamId) {
|
||||
const rawId = targetUrl.split('/').filter(Boolean).pop()
|
||||
if (!rawId) {
|
||||
throw new Error(`Could not parse a deployment ID from target_url: ${targetUrl}`)
|
||||
}
|
||||
const deploymentId = rawId.startsWith('dpl_') ? rawId : `dpl_${rawId}`
|
||||
return rawId.startsWith('dpl_') ? rawId : `dpl_${rawId}`
|
||||
}
|
||||
|
||||
const url = teamId
|
||||
? `https://api.vercel.com/v13/deployments/${deploymentId}?teamId=${teamId}`
|
||||
: `https://api.vercel.com/v13/deployments/${deploymentId}`
|
||||
function shortSha(sha) {
|
||||
return sha.slice(0, 10)
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: { Authorization: `Bearer ${vercelToken}` },
|
||||
})
|
||||
function createClients({ fetchImpl = fetch, repository, githubToken, vercelToken, teamId }) {
|
||||
const githubHeaders = {
|
||||
Authorization: `Bearer ${githubToken}`,
|
||||
Accept: 'application/vnd.github+json',
|
||||
}
|
||||
const vercelHeaders = { Authorization: `Bearer ${vercelToken}` }
|
||||
|
||||
if (!response.ok) {
|
||||
async function getJson(url, headers, what) {
|
||||
const response = await fetchImpl(url, { headers })
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to ${what}: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
return response.json()
|
||||
}
|
||||
|
||||
return {
|
||||
// Newest status for the context, or undefined when Vercel has not posted one.
|
||||
async latestStatus(sha, statusContext) {
|
||||
const statuses = await getJson(
|
||||
`https://api.github.com/repos/${repository}/commits/${sha}/statuses`,
|
||||
githubHeaders,
|
||||
`fetch commit statuses for ${shortSha(sha)}`
|
||||
)
|
||||
return statuses
|
||||
.filter((status) => status.context === statusContext)
|
||||
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())[0]
|
||||
},
|
||||
|
||||
// Commit SHAs of the pull request, newest first.
|
||||
async pullRequestShas(prNumber) {
|
||||
const shas = []
|
||||
for (let page = 1; ; page += 1) {
|
||||
const commits = await getJson(
|
||||
`https://api.github.com/repos/${repository}/pulls/${prNumber}/commits?per_page=${GITHUB_PAGE_SIZE}&page=${page}`,
|
||||
githubHeaders,
|
||||
`fetch commits for pull request #${prNumber}`
|
||||
)
|
||||
shas.push(...commits.map((commit) => commit.sha))
|
||||
if (commits.length < GITHUB_PAGE_SIZE) break
|
||||
}
|
||||
return shas.reverse()
|
||||
},
|
||||
|
||||
async deployment(targetUrl) {
|
||||
const deploymentId = deploymentIdFromTargetUrl(targetUrl)
|
||||
const query = teamId ? `?teamId=${teamId}` : ''
|
||||
const deployment = await getJson(
|
||||
`https://api.vercel.com/v13/deployments/${deploymentId}${query}`,
|
||||
vercelHeaders,
|
||||
`resolve Vercel deployment ${deploymentId}`
|
||||
)
|
||||
return { url: `https://${deployment.url}`, state: deployment.readyState ?? deployment.status }
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveFromEarlierCommits(clients, { headSha, prNumber, statusContext, log }) {
|
||||
if (!prNumber) {
|
||||
throw new Error(
|
||||
`Failed to resolve Vercel deployment ${deploymentId}: ${response.status} ${response.statusText}`
|
||||
`The "${statusContext}" build for ${shortSha(headSha)} was skipped, and PR_NUMBER is required to fall back to an earlier commit's preview`
|
||||
)
|
||||
}
|
||||
|
||||
const deployment = await response.json()
|
||||
return `https://${deployment.url}`
|
||||
const shas = await clients.pullRequestShas(prNumber)
|
||||
for (const sha of shas) {
|
||||
if (sha === headSha) continue
|
||||
|
||||
const status = await clients.latestStatus(sha, statusContext)
|
||||
if (status?.state !== 'success' || !status.target_url) continue
|
||||
|
||||
const deployment = await clients.deployment(status.target_url)
|
||||
if (deployment.state === 'READY') {
|
||||
log(`Using the "${statusContext}" preview built for ${shortSha(sha)}: ${deployment.url}`)
|
||||
return deployment.url
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`No READY "${statusContext}" preview among the ${shas.length} commits of pull request #${prNumber}`
|
||||
)
|
||||
}
|
||||
|
||||
async function resolvePreviewUrl(clients, options) {
|
||||
const { headSha, statusContext, timeoutMs, pollIntervalMs, sleep, now, log } = options
|
||||
const start = now()
|
||||
|
||||
for (;;) {
|
||||
const latest = await clients.latestStatus(headSha, statusContext)
|
||||
|
||||
if (latest?.state === 'success') {
|
||||
if (!latest.target_url) {
|
||||
throw new Error(
|
||||
`"${statusContext}" commit status succeeded but had no target_url to resolve a deployment from`
|
||||
)
|
||||
}
|
||||
|
||||
const deployment = await clients.deployment(latest.target_url)
|
||||
if (deployment.state === 'READY') {
|
||||
return deployment.url
|
||||
}
|
||||
if (deployment.state === 'CANCELED') {
|
||||
log(
|
||||
`"${statusContext}" for ${shortSha(headSha)} reports "${latest.description}" and its deployment is CANCELED; looking for the newest READY preview among earlier commits`
|
||||
)
|
||||
return resolveFromEarlierCommits(clients, options)
|
||||
}
|
||||
throw new Error(
|
||||
`"${statusContext}" deployment for ${shortSha(headSha)} is ${deployment.state}, not READY`
|
||||
)
|
||||
}
|
||||
|
||||
if (latest?.state === 'failure' || latest?.state === 'error') {
|
||||
throw new Error(`"${statusContext}" deployment failed (commit status: ${latest.state})`)
|
||||
}
|
||||
|
||||
if (now() - start > timeoutMs) {
|
||||
throw new Error(
|
||||
`Timed out after ${Math.round(timeoutMs / 1000)}s waiting for the "${statusContext}" preview deployment`
|
||||
)
|
||||
}
|
||||
|
||||
await sleep(pollIntervalMs)
|
||||
}
|
||||
}
|
||||
|
||||
function writeOutput(name, value) {
|
||||
@@ -71,6 +177,7 @@ async function main() {
|
||||
const vercelToken = process.env.VERCEL_TOKEN
|
||||
const teamId = process.env.VERCEL_TEAM_ID
|
||||
const statusContext = process.env.VERCEL_STATUS_CONTEXT
|
||||
const prNumber = process.env.PR_NUMBER
|
||||
|
||||
if (!repository) throw new Error('GITHUB_REPOSITORY environment variable is required')
|
||||
if (!sha) throw new Error('HEAD_SHA environment variable is required')
|
||||
@@ -78,35 +185,25 @@ async function main() {
|
||||
if (!vercelToken) throw new Error('VERCEL_TOKEN environment variable is required')
|
||||
if (!statusContext) throw new Error('VERCEL_STATUS_CONTEXT environment variable is required')
|
||||
|
||||
const start = Date.now()
|
||||
|
||||
for (;;) {
|
||||
const latest = await fetchLatestStatus(repository, sha, githubToken, statusContext)
|
||||
|
||||
if (latest?.state === 'success') {
|
||||
if (!latest.target_url) {
|
||||
throw new Error(
|
||||
`"${statusContext}" commit status succeeded but had no target_url to resolve a deployment from`
|
||||
)
|
||||
}
|
||||
const deploymentUrl = await resolveDeploymentUrl(latest.target_url, vercelToken, teamId)
|
||||
writeOutput('deployment-url', deploymentUrl)
|
||||
return
|
||||
}
|
||||
|
||||
if (latest?.state === 'failure' || latest?.state === 'error') {
|
||||
throw new Error(`"${statusContext}" deployment failed (commit status: ${latest.state})`)
|
||||
}
|
||||
|
||||
if (Date.now() - start > TIMEOUT_MS) {
|
||||
throw new Error(`Timed out after 900s waiting for the "${statusContext}" preview deployment`)
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS))
|
||||
}
|
||||
const clients = createClients({ repository, githubToken, vercelToken, teamId })
|
||||
const url = await resolvePreviewUrl(clients, {
|
||||
headSha: sha,
|
||||
prNumber,
|
||||
statusContext,
|
||||
timeoutMs: TIMEOUT_MS,
|
||||
pollIntervalMs: POLL_INTERVAL_MS,
|
||||
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
||||
now: Date.now,
|
||||
log: console.log,
|
||||
})
|
||||
writeOutput('deployment-url', url)
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('Fatal error:', error)
|
||||
process.exit(1)
|
||||
})
|
||||
module.exports = { createClients, deploymentIdFromTargetUrl, resolvePreviewUrl }
|
||||
|
||||
if (require.main === module) {
|
||||
main().catch((error) => {
|
||||
console.error('Fatal error:', error)
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
const assert = require('node:assert/strict')
|
||||
const { test } = require('node:test')
|
||||
|
||||
const { deploymentIdFromTargetUrl, resolvePreviewUrl } = require('./waitForVercelPreview.js')
|
||||
|
||||
const CONTEXT = 'Vercel – docs'
|
||||
const HEAD = 'head000000000000000000000000000000000000'
|
||||
const OLDER = 'older00000000000000000000000000000000000'
|
||||
const OLDEST = 'oldest0000000000000000000000000000000000'
|
||||
|
||||
function success(id, description = 'Deployment has completed') {
|
||||
return { state: 'success', description, target_url: `https://vercel.com/supabase/docs/${id}` }
|
||||
}
|
||||
|
||||
function fakeClients({ statuses = {}, deployments = {}, prShas = [] }) {
|
||||
const calls = { latestStatus: [], deployment: [], pullRequestShas: 0 }
|
||||
return {
|
||||
calls,
|
||||
async latestStatus(sha) {
|
||||
calls.latestStatus.push(sha)
|
||||
const list = statuses[sha]
|
||||
return Array.isArray(list) ? list.shift() : list
|
||||
},
|
||||
async pullRequestShas() {
|
||||
calls.pullRequestShas += 1
|
||||
return prShas
|
||||
},
|
||||
async deployment(targetUrl) {
|
||||
calls.deployment.push(targetUrl)
|
||||
return deployments[deploymentIdFromTargetUrl(targetUrl)]
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function options(overrides = {}) {
|
||||
const log = []
|
||||
return {
|
||||
headSha: HEAD,
|
||||
prNumber: 50119,
|
||||
statusContext: CONTEXT,
|
||||
timeoutMs: 1000,
|
||||
pollIntervalMs: 10,
|
||||
sleep: async () => {},
|
||||
now: () => 0,
|
||||
log: (line) => log.push(line),
|
||||
logLines: log,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
test('deploymentIdFromTargetUrl prefixes bare inspector ids and keeps dpl_ ids', () => {
|
||||
assert.equal(
|
||||
deploymentIdFromTargetUrl('https://vercel.com/supabase/docs/Avoo1vcp'),
|
||||
'dpl_Avoo1vcp'
|
||||
)
|
||||
assert.equal(
|
||||
deploymentIdFromTargetUrl('https://vercel.com/supabase/docs/dpl_Avoo1vcp'),
|
||||
'dpl_Avoo1vcp'
|
||||
)
|
||||
assert.throws(() => deploymentIdFromTargetUrl('https://vercel.com'), /deployment ID/)
|
||||
})
|
||||
|
||||
test('returns the head deployment when it is READY', async () => {
|
||||
const clients = fakeClients({
|
||||
statuses: { [HEAD]: success('dpl_head') },
|
||||
deployments: { dpl_head: { url: 'https://docs-head.vercel.app', state: 'READY' } },
|
||||
})
|
||||
|
||||
const url = await resolvePreviewUrl(clients, options())
|
||||
|
||||
assert.equal(url, 'https://docs-head.vercel.app')
|
||||
assert.equal(clients.calls.pullRequestShas, 0)
|
||||
})
|
||||
|
||||
test('polls while the head status is pending', async () => {
|
||||
let clock = 0
|
||||
const sleeps = []
|
||||
const clients = fakeClients({
|
||||
statuses: { [HEAD]: [undefined, { state: 'pending' }, success('dpl_head')] },
|
||||
deployments: { dpl_head: { url: 'https://docs-head.vercel.app', state: 'READY' } },
|
||||
})
|
||||
|
||||
const url = await resolvePreviewUrl(
|
||||
clients,
|
||||
options({
|
||||
now: () => clock,
|
||||
sleep: async (ms) => {
|
||||
sleeps.push(ms)
|
||||
clock += ms
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
assert.equal(url, 'https://docs-head.vercel.app')
|
||||
assert.deepEqual(sleeps, [10, 10])
|
||||
})
|
||||
|
||||
test('falls back to the newest earlier commit with a READY deployment when the head build was skipped', async () => {
|
||||
const clients = fakeClients({
|
||||
prShas: [HEAD, OLDER, OLDEST],
|
||||
statuses: {
|
||||
[HEAD]: success('dpl_head', 'Skipped - Not affected'),
|
||||
[OLDER]: undefined,
|
||||
[OLDEST]: success('dpl_oldest'),
|
||||
},
|
||||
deployments: {
|
||||
dpl_head: { url: 'https://docs-head.vercel.app', state: 'CANCELED' },
|
||||
dpl_oldest: { url: 'https://docs-oldest.vercel.app', state: 'READY' },
|
||||
},
|
||||
})
|
||||
const opts = options()
|
||||
|
||||
const url = await resolvePreviewUrl(clients, opts)
|
||||
|
||||
assert.equal(url, 'https://docs-oldest.vercel.app')
|
||||
assert.deepEqual(clients.calls.latestStatus, [HEAD, OLDER, OLDEST])
|
||||
assert.ok(opts.logLines.some((line) => line.includes('Skipped - Not affected')))
|
||||
assert.ok(opts.logLines.some((line) => line.includes(OLDEST.slice(0, 10))))
|
||||
})
|
||||
|
||||
test('skips earlier commits whose deployment is not READY', async () => {
|
||||
const clients = fakeClients({
|
||||
prShas: [HEAD, OLDER, OLDEST],
|
||||
statuses: {
|
||||
[HEAD]: success('dpl_head', 'Skipped - Not affected'),
|
||||
[OLDER]: success('dpl_older', 'Skipped - Not affected'),
|
||||
[OLDEST]: success('dpl_oldest'),
|
||||
},
|
||||
deployments: {
|
||||
dpl_head: { url: 'https://docs-head.vercel.app', state: 'CANCELED' },
|
||||
dpl_older: { url: 'https://docs-older.vercel.app', state: 'CANCELED' },
|
||||
dpl_oldest: { url: 'https://docs-oldest.vercel.app', state: 'READY' },
|
||||
},
|
||||
})
|
||||
|
||||
const url = await resolvePreviewUrl(clients, options())
|
||||
|
||||
assert.equal(url, 'https://docs-oldest.vercel.app')
|
||||
})
|
||||
|
||||
test('throws when the head build was skipped and no earlier commit has a READY deployment', async () => {
|
||||
const clients = fakeClients({
|
||||
prShas: [HEAD, OLDER],
|
||||
statuses: {
|
||||
[HEAD]: success('dpl_head', 'Skipped - Not affected'),
|
||||
[OLDER]: { state: 'failure', target_url: 'https://vercel.com/supabase/docs/dpl_older' },
|
||||
},
|
||||
deployments: { dpl_head: { url: 'https://docs-head.vercel.app', state: 'CANCELED' } },
|
||||
})
|
||||
|
||||
await assert.rejects(resolvePreviewUrl(clients, options()), /No READY "Vercel – docs" preview/)
|
||||
})
|
||||
|
||||
test('throws when the head build was skipped and no pull request number is available', async () => {
|
||||
const clients = fakeClients({
|
||||
statuses: { [HEAD]: success('dpl_head', 'Skipped - Not affected') },
|
||||
deployments: { dpl_head: { url: 'https://docs-head.vercel.app', state: 'CANCELED' } },
|
||||
})
|
||||
|
||||
await assert.rejects(resolvePreviewUrl(clients, options({ prNumber: undefined })), /PR_NUMBER/)
|
||||
assert.equal(clients.calls.pullRequestShas, 0)
|
||||
})
|
||||
|
||||
test('throws when the head deployment is in any state other than READY or CANCELED', async () => {
|
||||
const clients = fakeClients({
|
||||
statuses: { [HEAD]: success('dpl_head') },
|
||||
deployments: { dpl_head: { url: 'https://docs-head.vercel.app', state: 'ERROR' } },
|
||||
})
|
||||
|
||||
await assert.rejects(resolvePreviewUrl(clients, options()), /ERROR/)
|
||||
})
|
||||
|
||||
test('throws when the head status reports a failed deployment', async () => {
|
||||
const clients = fakeClients({ statuses: { [HEAD]: { state: 'failure' } } })
|
||||
|
||||
await assert.rejects(resolvePreviewUrl(clients, options()), /deployment failed/)
|
||||
})
|
||||
|
||||
test('throws when the head status never resolves before the timeout', async () => {
|
||||
let clock = 0
|
||||
const clients = fakeClients({ statuses: { [HEAD]: { state: 'pending' } } })
|
||||
|
||||
await assert.rejects(
|
||||
resolvePreviewUrl(
|
||||
clients,
|
||||
options({
|
||||
timeoutMs: 25,
|
||||
now: () => clock,
|
||||
sleep: async (ms) => {
|
||||
clock += ms
|
||||
},
|
||||
})
|
||||
),
|
||||
/Timed out/
|
||||
)
|
||||
})
|
||||
Reference in new issue
Block a user