Ctrl K
ComunidadGuías…

scripts/warn-stale-pr-migrations.mjs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { readdirSync, readFileSync } from 'node:fs'
import { join } from 'node:path'
import process from 'node:process'

export const COMMENT_MARKER = '<!-- capgo-stale-migration-order -->'

const MIGRATIONS_PREFIX = 'supabase/migrations/'
const MIGRATION_FILE_PATTERN = /\/(\d{14})_[^/]+\.sql$/
const RECENT_ACTIVITY_HOURS = 72

export function migrationTimestamp(path) {
  return path.match(MIGRATION_FILE_PATTERN)?.[1] ?? null
}

export function latestMigrationPath(paths) {
  return paths
    .filter(path => migrationTimestamp(path))
    .toSorted((left, right) => {
      const timestampOrder = migrationTimestamp(left).localeCompare(migrationTimestamp(right))
      return timestampOrder || left.localeCompare(right)
    })
    .at(-1) ?? null
}

function migrationPathMarkup(path) {
  const escaped = path
    .replaceAll('&', '&amp;')
    .replaceAll('<', '&lt;')
    .replaceAll('>', '&gt;')
    .replaceAll('"', '&quot;')
    .replaceAll('\'', '&#39;')
  return `<code>${escaped}</code>`
}

export function stalePullRequestMigrations(files, latestMainMigrationPath, mainMigrationPaths = [latestMainMigrationPath]) {
  const latestMainTimestamp = migrationTimestamp(latestMainMigrationPath)
  if (!latestMainTimestamp)
    return []

  const migrationFiles = files
    .filter(file => file.status === 'added' || file.status === 'renamed')
    .filter(file => file.filename.startsWith(MIGRATIONS_PREFIX) && file.filename.endsWith('.sql'))
  const addedBaselines = migrationFiles.filter(file =>
    file.status === 'added' && file.filename.endsWith('_baseline.sql'),
  )
  const removedMigrationCount = files.filter(file =>
    file.status === 'removed'
    && file.filename.startsWith(MIGRATIONS_PREFIX)
    && file.filename.endsWith('.sql'),
  ).length
  const squashBaselinePath = addedBaselines.length === 1 && removedMigrationCount >= 50
    ? addedBaselines[0].filename
    : null
  const mainTimestamps = new Set(mainMigrationPaths.map(migrationTimestamp).filter(Boolean))

  return [...new Set(migrationFiles
    .map(file => file.filename)
    .filter((path) => {
      const timestamp = migrationTimestamp(path)
      if (!timestamp || timestamp > latestMainTimestamp)
        return false

      const isAllowedSquashBaseline = path === squashBaselinePath
        && timestamp < latestMainTimestamp
        && !mainTimestamps.has(timestamp)
      return !isAllowedSquashBaseline
    }))].toSorted()
}

export function buildWarningBody({
  mainMigrationPaths,
  latestMainMigrationPath,
  staleMigrationPaths,
  commitUrl,
}) {
  const mainList = mainMigrationPaths.map(path => `- ${migrationPathMarkup(path)}`).join('\n')
  const staleList = staleMigrationPaths.map(path => `- ${migrationPathMarkup(path)}`).join('\n')

  return `${COMMENT_MARKER}
## ⚠️ Supabase migration order is now stale

[A push to \`main\`](${commitUrl}) added or restamped these migrations:

${mainList}

This PR still adds migrations that sort at or before the latest migration on \`main\` (${migrationPathMarkup(latestMainMigrationPath)}):

${staleList}

The migration-order check will reject this ordering, and Supabase will not deploy this PR correctly as-is. Update the branch from \`main\`, then recreate the pending migration with a timestamp newer than the latest migration on \`main\`. If the migration has never been applied and its SQL must stay identical, use a content-preserving restamp.`
}

function localMainMigrationPaths(workspace) {
  return readdirSync(join(workspace, 'supabase', 'migrations'))
    .filter(file => file.endsWith('.sql'))
    .map(file => `${MIGRATIONS_PREFIX}${file}`)
}

function loadCurrentMainMigrationPaths(workspace, providedPaths) {
  if (providedPaths)
    return providedPaths

  const snapshotPath = process.env.CURRENT_MAIN_MIGRATION_PATHS_FILE
  if (snapshotPath) {
    return readFileSync(snapshotPath, 'utf8')
      .split('\0')
      .map(path => path.trim())
      .filter(Boolean)
  }

  return localMainMigrationPaths(workspace)
}

export async function warnStalePrMigrations({
  github,
  context,
  core,
  currentMainMigrationPaths,
  mainMigrationPaths,
  now = new Date(),
}) {
  const changedMainMigrations = mainMigrationPaths
    .map(path => path.trim())
    .filter(path => migrationTimestamp(path))
    .toSorted()

  if (changedMainMigrations.length === 0) {
    core.info('No valid Supabase migration additions were found in the main push.')
    return 0
  }

  const workspace = process.env.GITHUB_WORKSPACE ?? process.cwd()
  const currentMainPaths = loadCurrentMainMigrationPaths(workspace, currentMainMigrationPaths)
  const latestMain = latestMigrationPath(currentMainPaths)
  if (!latestMain)
    throw new Error('Could not determine the latest Supabase migration on main.')

  const { owner, repo } = context.repo
  const base = context.payload.repository?.default_branch ?? 'main'
  const cutoff = new Date(now.getTime() - RECENT_ACTIVITY_HOURS * 60 * 60 * 1000)
  const pullRequests = await github.paginate(github.rest.pulls.list, {
    owner,
    repo,
    base,
    state: 'open',
    sort: 'updated',
    direction: 'desc',
    per_page: 100,
  })
  const recentPullRequests = pullRequests.filter((pullRequest) => {
    const updatedAt = new Date(pullRequest.updated_at)
    return !Number.isNaN(updatedAt.getTime()) && updatedAt >= cutoff
  })

  let warned = 0
  for (const pullRequest of recentPullRequests) {
    try {
      const files = await github.paginate(github.rest.pulls.listFiles, {
        owner,
        repo,
        pull_number: pullRequest.number,
        per_page: 100,
      })
      const staleMigrationPaths = stalePullRequestMigrations(files, latestMain, currentMainPaths)
      const comments = await github.paginate(github.rest.issues.listComments, {
        owner,
        repo,
        issue_number: pullRequest.number,
        per_page: 100,
      })
      const existing = comments.find(comment =>
        comment.user?.login === 'github-actions[bot]'
        && comment.body?.startsWith(COMMENT_MARKER),
      )
      if (staleMigrationPaths.length === 0) {
        if (existing) {
          await github.rest.issues.deleteComment({
            owner,
            repo,
            comment_id: existing.id,
          })
          core.notice(`Removed resolved migration-order warning from PR #${pullRequest.number}.`)
        }
        core.info(`PR #${pullRequest.number} has no stale Supabase migrations.`)
        continue
      }

      const body = buildWarningBody({
        mainMigrationPaths: changedMainMigrations,
        latestMainMigrationPath: latestMain,
        staleMigrationPaths,
        commitUrl: `https://github.com/${owner}/${repo}/commit/${context.sha}`,
      })
      if (existing) {
        await github.rest.issues.updateComment({
          owner,
          repo,
          comment_id: existing.id,
          body,
        })
        core.notice(`Updated migration-order warning on PR #${pullRequest.number}.`)
      }
      else {
        await github.rest.issues.createComment({
          owner,
          repo,
          issue_number: pullRequest.number,
          body,
        })
        core.notice(`Posted migration-order warning on PR #${pullRequest.number}.`)
      }
      warned += 1
    }
    catch (error) {
      const message = error instanceof Error ? error.message : String(error)
      core.warning(`Could not process migration warning for PR #${pullRequest.number}: ${message}`)
    }
  }

  core.info(`Checked ${recentPullRequests.length} recently active PRs and warned ${warned}.`)
  return warned
}