next.js/.github/actions/next-stats-action/src/aggregate-results.js
aggregate-results.js128 lines3.6 KB
#!/usr/bin/env node
/**
 * Aggregates results from sharded stats runs and posts combined comment
 *
 * Usage: node aggregate-results.js <results-dir>
 *
 * Expects JSON files named pr-stats-*.json in the results directory
 */

const path = require('path')
const fs = require('fs/promises')
const { existsSync } = require('fs')
const addComment = require('./add-comment')
const logger = require('./util/logger')

async function main() {
  const resultsDir = process.argv[2] || process.cwd()

  logger(`Aggregating results from: ${resultsDir}`)

  // Find all pr-stats-*.json files
  const files = await fs.readdir(resultsDir)
  const statsFiles = files.filter(
    (f) => f.startsWith('pr-stats-') && f.endsWith('.json')
  )

  if (statsFiles.length === 0) {
    // This can happen for docs-only changes where stats jobs are skipped
    logger('No pr-stats-*.json files found - this may be a docs-only change')
    process.exit(0)
  }

  logger(`Found ${statsFiles.length} results files: ${statsFiles.join(', ')}`)

  // Load all results
  const allData = []
  for (const file of statsFiles) {
    const filePath = path.join(resultsDir, file)
    try {
      const content = await fs.readFile(filePath, 'utf8')
      const data = JSON.parse(content)
      allData.push(data)
      logger(`Loaded ${file} successfully`)
    } catch (err) {
      logger(`Warning: Failed to load ${file}: ${err.message}`)
    }
  }

  if (allData.length === 0) {
    logger('No valid results files could be loaded')
    process.exit(1)
  }

  // Use the first file's actionInfo and statsConfig
  const { actionInfo, statsConfig } = allData[0]

  // Re-inject the GitHub token from env (it's excluded from JSON serialization for security)
  actionInfo.githubToken = process.env.PR_STATS_COMMENT_TOKEN

  // Merge results from all files
  // Each file has results array with {title, mainRepoStats, diffRepoStats, diffs}
  // We need to merge stats objects by combining their keys
  const mergedResults = []

  // Assume all files have the same number of configs with same titles
  const numConfigs = allData[0].results.length

  for (let i = 0; i < numConfigs; i++) {
    const title = allData[0].results[i].title
    const mergedMainRepoStats = {}
    const mergedDiffRepoStats = {}
    let mergedDiffs = null

    for (const data of allData) {
      const result = data.results[i]

      // Merge mainRepoStats
      if (result.mainRepoStats) {
        for (const [key, value] of Object.entries(result.mainRepoStats)) {
          if (!mergedMainRepoStats[key]) {
            mergedMainRepoStats[key] = {}
          }
          Object.assign(mergedMainRepoStats[key], value)
        }
      }

      // Merge diffRepoStats
      if (result.diffRepoStats) {
        for (const [key, value] of Object.entries(result.diffRepoStats)) {
          if (!mergedDiffRepoStats[key]) {
            mergedDiffRepoStats[key] = {}
          }
          Object.assign(mergedDiffRepoStats[key], value)
        }
      }

      // Merge diffs (just combine all diff objects)
      if (result.diffs) {
        if (!mergedDiffs) {
          mergedDiffs = {}
        }
        Object.assign(mergedDiffs, result.diffs)
      }
    }

    mergedResults.push({
      title,
      mainRepoStats: mergedMainRepoStats,
      diffRepoStats: mergedDiffRepoStats,
      diffs: mergedDiffs,
    })
  }

  logger(
    `Merged ${allData.length} result sets into ${mergedResults.length} configs`
  )

  // Post the combined comment
  await addComment(mergedResults, actionInfo, statsConfig)

  logger('Aggregation complete')
}

main().catch((err) => {
  console.error('Error aggregating results:', err)
  process.exit(1)
})
Quest for Codev2.0.0
/
SIGN IN