next.js/.github/actions/next-stats-action/src/index.js
index.js245 lines8.3 KB
const path = require('path')
const fs = require('fs/promises')
const { existsSync } = require('fs')
const exec = require('./util/exec')
const logger = require('./util/logger')
const runConfigs = require('./run')
const addComment = require('./add-comment')
const actionInfo = require('./prepare/action-info')()
const { mainRepoDir, diffRepoDir, pnpmStoreDir } = require('./constants')
const loadStatsConfig = require('./prepare/load-stats-config')
const { cloneRepo, mergeBranch, getCommitId, linkPackages, getLastStable } =
  require('./prepare/repo-setup')(actionInfo)

const allowedActions = new Set(['synchronize', 'opened'])

// Get bundler filter from action input (set by GitHub Actions as INPUT_BUNDLER)
const bundlerInput = (process.env.INPUT_BUNDLER || 'both').toLowerCase()
const isShardedRun = bundlerInput !== 'both'

if (isShardedRun) {
  logger(`Running in sharded mode for bundler: ${bundlerInput}`)
}

if (!allowedActions.has(actionInfo.actionName) && !actionInfo.isRelease) {
  logger(
    `Not running for ${actionInfo.actionName} event action on repo: ${actionInfo.prRepo} and ref ${actionInfo.prRef}`
  )
  process.exit(0)
}

;(async () => {
  try {
    if (existsSync(path.join(__dirname, '../SKIP_NEXT_STATS.txt'))) {
      console.log(
        'SKIP_NEXT_STATS.txt file present, exiting stats generation..'
      )
      process.exit(0)
    }

    const { stdout: gitName } = await exec(
      'git config user.name && git config user.email'
    )
    console.log('git author result:', gitName)

    // clone PR/newer repository/ref first to get settings
    if (!actionInfo.skipClone) {
      await cloneRepo(actionInfo.prRepo, diffRepoDir, actionInfo.prRef)
    }

    if (actionInfo.isRelease) {
      process.env.STATS_IS_RELEASE = 'true'
    }

    // load stats config from allowed locations
    const { statsConfig, relativeStatsAppDir } = loadStatsConfig()

    if (actionInfo.isLocal && actionInfo.prRef === statsConfig.mainBranch) {
      throw new Error(
        `'GITHUB_REF' can not be the same as mainBranch in 'stats-config.js'.\n` +
          `This will result in comparing against the same branch`
      )
    }

    if (actionInfo.isLocal) {
      // make sure to use local repo location instead of the
      // one provided in statsConfig
      statsConfig.mainRepo = actionInfo.prRepo
    }

    /* eslint-disable-next-line */
    actionInfo.commitId = await getCommitId(diffRepoDir)

    if (!actionInfo.skipClone) {
      let mainRef = statsConfig.mainBranch

      if (actionInfo.isRelease) {
        logger(`Release detected, using last stable tag: "${actionInfo.prRef}"`)
        const lastStableTag = await getLastStable(diffRepoDir, actionInfo.prRef)
        mainRef = lastStableTag
        if (!lastStableTag) throw new Error('failed to get last stable tag')
        logger(`using latestStable: "${lastStableTag}"`)

        /* eslint-disable-next-line */
        actionInfo.lastStableTag = lastStableTag
        /* eslint-disable-next-line */
        actionInfo.commitId = await getCommitId(diffRepoDir)

        if (!actionInfo.customCommentEndpoint) {
          /* eslint-disable-next-line */
          actionInfo.commentEndpoint = `https://api.github.com/repos/${statsConfig.mainRepo}/commits/${actionInfo.commitId}/comments`
        }
      }

      await cloneRepo(statsConfig.mainRepo, mainRepoDir, mainRef)

      if (!actionInfo.isRelease && statsConfig.autoMergeMain) {
        logger('Attempting auto merge of main branch')
        await mergeBranch(statsConfig.mainBranch, mainRepoDir, diffRepoDir)
      }
    }
    let mainRepoPkgPaths
    let diffRepoPkgPaths

    // run install/initialBuildCommand
    const repoDirs = [mainRepoDir, diffRepoDir]

    for (const dir of repoDirs) {
      logger(`Running initial build for ${dir}`)
      if (!actionInfo.skipClone) {
        // TODO: we can remove this `packageManager` modification once Next.js
        // 16.3 is released, but we must override it for now because 16.2 uses
        // pnpm 9.6.0, which supports different arguments. `diffRepoDir`
        // points to the most recent stable tag.
        const packageJson = path.join(dir, 'package.json')
        const packageJsonContents = JSON.parse(
          await fs.readFile(packageJson, { encoding: 'utf8' })
        )
        packageJsonContents.packageManager = 'pnpm@10.33.0'
        if (packageJsonContents.engines != null) {
          delete packageJsonContents.engines.pnpm
        }
        await fs.writeFile(
          packageJson,
          JSON.stringify(packageJsonContents, null, '  ')
        )

        if (!statsConfig.skipInitialInstall) {
          const command =
            'pnpm install ' +
            // tolerate lockfile changes from merging latest changes
            '--no-frozen-lockfile ' +
            // avoid hardlink issues on self-hosted runners,
            '--package-import-method=clone-or-copy ' +
            // the store is colocated with the workdir to avoid EXDEV copy
            // failures on overlayfs runners.
            `--store-dir=${pnpmStoreDir}`
          await exec.spawnPromise(command, {
            env: { PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1' },
            cwd: dir,
          })

          await exec.spawnPromise(
            statsConfig.initialBuildCommand || 'pnpm build',
            { cwd: dir }
          )
        }
      }

      const nativeDir = path.join(dir, 'packages/next-swc/native')
      await fs
        .cp(path.join(__dirname, '../native'), nativeDir, {
          recursive: true,
          force: true,
        })
        .catch(console.error)

      process.env.NEXT_TEST_NATIVE_DIR = nativeDir

      logger(`Packing packages in ${dir}`)
      const turboJsonPath = path.join(dir, 'turbo.json')
      let hasTurboPackTask = false
      try {
        const turboJson = JSON.parse(
          await fs.readFile(turboJsonPath, { encoding: 'utf8' })
        )
        hasTurboPackTask =
          turboJson.tasks?.['pack-for-isolated-tests'] !== undefined
      } catch {}

      if (hasTurboPackTask) {
        await exec.spawnPromise('pnpm turbo run pack-for-isolated-tests', {
          cwd: dir,
        })
      } else {
        // Temporary fallback because stats action run on the canary branch which does not have the pack-for-isolated-tests task yet.
        logger(
          'turbo task pack-for-isolated-tests not found, falling back to pnpm pack per package'
        )
        const packagesDir = path.join(dir, 'packages')
        const packageFolders = await fs.readdir(packagesDir)
        await Promise.all(
          packageFolders.map(async (folder) => {
            const pkgDir = path.join(packagesDir, folder)
            const pkgJsonPath = path.join(pkgDir, 'package.json')
            if (!existsSync(pkgJsonPath)) return
            try {
              await exec.spawnPromise('pnpm pack --out packed.tgz', {
                cwd: pkgDir,
              })
            } catch (err) {
              logger(`Failed to pack ${folder}: ${err.message}`)
            }
          })
        )
      }

      logger(`Linking packages in ${dir}`)
      const pkgPaths = await linkPackages({
        repoDir: dir,
      })

      if (dir === mainRepoDir) mainRepoPkgPaths = pkgPaths
      else diffRepoPkgPaths = pkgPaths
    }

    // run the configs and collect results
    const results = await runConfigs(statsConfig.configs, {
      statsConfig,
      mainRepoPkgPaths,
      diffRepoPkgPaths,
      relativeStatsAppDir,
      bundlerFilter: isShardedRun ? bundlerInput : null,
    })

    if (isShardedRun) {
      // In sharded mode, save results to JSON for later aggregation
      const resultsPath = path.join(
        process.env.GITHUB_WORKSPACE || process.cwd(),
        `pr-stats-${bundlerInput}.json`
      )
      // Exclude sensitive fields (githubToken) before serializing to JSON
      const { githubToken, ...safeActionInfo } = actionInfo
      await fs.writeFile(
        resultsPath,
        JSON.stringify(
          { results, actionInfo: safeActionInfo, statsConfig },
          null,
          2
        )
      )
      logger(`Saved results to ${resultsPath}`)
    } else {
      // In non-sharded mode, post comment directly
      await addComment(results, actionInfo, statsConfig)
    }

    logger('finished')
    process.exit(0)
  } catch (err) {
    console.error('Error occurred generating stats:')
    console.error(err)
    process.exit(1)
  }
})()
Quest for Codev2.0.0
/
SIGN IN