#!/usr/bin/env node const { promises: fs } = require('fs'); const path = require('path'); // Fetch is part of node js in future versions, thus triggering no-shadow // eslint-disable-next-line no-shadow const fetch = require('node-fetch'); const glob = require('fast-glob'); const VERSION = require('../package.json').version; const { getHighlights } = require('./highlights'); start().catch(console.error); function capitalizeFirstLetter(string) { return string.charAt(0).toUpperCase() + string.slice(1); } async function start() { const { GITHUB_COMMENT_TOKEN, CIRCLE_PULL_REQUEST } = process.env; console.log('CIRCLE_PULL_REQUEST', CIRCLE_PULL_REQUEST); const { CIRCLE_SHA1 } = process.env; console.log('CIRCLE_SHA1', CIRCLE_SHA1); const { CIRCLE_BUILD_NUM } = process.env; console.log('CIRCLE_BUILD_NUM', CIRCLE_BUILD_NUM); const { CIRCLE_WORKFLOW_JOB_ID } = process.env; console.log('CIRCLE_WORKFLOW_JOB_ID', CIRCLE_WORKFLOW_JOB_ID); const { PARENT_COMMIT } = process.env; console.log('PARENT_COMMIT', PARENT_COMMIT); if (!CIRCLE_PULL_REQUEST) { console.warn(`No pull request detected for commit "${CIRCLE_SHA1}"`); return; } const CIRCLE_PR_NUMBER = CIRCLE_PULL_REQUEST.split('/').pop(); const SHORT_SHA1 = CIRCLE_SHA1.slice(0, 7); const BUILD_LINK_BASE = `https://output.circle-artifacts.com/output/job/${CIRCLE_WORKFLOW_JOB_ID}/artifacts/0`; // build the github comment content // links to extension builds const platforms = ['chrome', 'firefox', 'opera']; const buildLinks = platforms .map((platform) => { const url = `${BUILD_LINK_BASE}/builds/metamask-${platform}-${VERSION}.zip`; return `${platform}`; }) .join(', '); const betaBuildLinks = platforms .map((platform) => { const url = `${BUILD_LINK_BASE}/builds-beta/metamask-beta-${platform}-${VERSION}.zip`; return `${platform}`; }) .join(', '); const flaskBuildLinks = platforms .map((platform) => { const url = `${BUILD_LINK_BASE}/builds-flask/metamask-flask-${platform}-${VERSION}.zip`; return `${platform}`; }) .join(', '); // links to bundle browser builds const bundles = {}; const fileType = '.html'; const sourceMapRoot = '/build-artifacts/source-map-explorer/'; const bundleFiles = await glob(`.${sourceMapRoot}*${fileType}`); bundleFiles.forEach((bundleFile) => { const fileName = bundleFile.split(sourceMapRoot)[1]; const bundleName = fileName.split(fileType)[0]; const url = `${BUILD_LINK_BASE}${sourceMapRoot}${fileName}`; let fileRoot = bundleName; let fileIndex = bundleName.match(/-[0-9]{1,}$/u)?.index; if (fileIndex) { fileRoot = bundleName.slice(0, fileIndex); fileIndex = bundleName.slice(fileIndex + 1, bundleName.length); } const link = `${fileIndex || fileRoot}`; if (fileRoot in bundles) { bundles[fileRoot].push(link); } else { bundles[fileRoot] = [link]; } }); const bundleMarkup = ``; const bundleSizeDataUrl = 'https://raw.githubusercontent.com/MetaMask/extension_bundlesize_stats/main/stats/bundle_size_data.json'; const coverageUrl = `${BUILD_LINK_BASE}/coverage/index.html`; const coverageLink = `Report`; const storybookUrl = `${BUILD_LINK_BASE}/storybook/index.html`; const storybookLink = `Storybook`; const tsMigrationDashboardUrl = `${BUILD_LINK_BASE}/ts-migration-dashboard/index.html`; const tsMigrationDashboardLink = `Dashboard`; // links to bundle browser builds const depVizUrl = `${BUILD_LINK_BASE}/build-artifacts/build-viz/index.html`; const depVizLink = `Build System`; const moduleInitStatsBackgroundUrl = `${BUILD_LINK_BASE}/test-artifacts/chrome/mv3/initialisation/background/index.html`; const moduleInitStatsBackgroundLink = `Background Module Init Stats`; const moduleInitStatsUIUrl = `${BUILD_LINK_BASE}/test-artifacts/chrome/mv3/initialisation/ui/index.html`; const moduleInitStatsUILink = `UI Init Stats`; const moduleLoadStatsUrl = `${BUILD_LINK_BASE}/test-artifacts/chrome/mv3/load_time/index.html`; const moduleLoadStatsLink = `Module Load Stats`; const bundleSizeStatsUrl = `${BUILD_LINK_BASE}/test-artifacts/chrome/mv3/bundle_size.json`; const bundleSizeStatsLink = `Bundle Size Stats`; const userActionsStatsUrl = `${BUILD_LINK_BASE}/test-artifacts/chrome/benchmark/user_actions.json`; const userActionsStatsLink = `E2e Actions Stats`; // link to artifacts const allArtifactsUrl = `https://circleci.com/gh/MetaMask/metamask-extension/${CIRCLE_BUILD_NUM}#artifacts/containers/0`; const contentRows = [ `builds: ${buildLinks}`, `builds (beta): ${betaBuildLinks}`, `builds (flask): ${flaskBuildLinks}`, `build viz: ${depVizLink}`, `mv3: ${moduleInitStatsBackgroundLink}`, `mv3: ${moduleInitStatsUILink}`, `mv3: ${moduleLoadStatsLink}`, `mv3: ${bundleSizeStatsLink}`, `mv2: ${userActionsStatsLink}`, `code coverage: ${coverageLink}`, `storybook: ${storybookLink}`, `typescript migration: ${tsMigrationDashboardLink}`, `all artifacts`, `
bundle viz: ${bundleMarkup}
`, ]; const hiddenContent = ``; const exposedContent = `Builds ready [${SHORT_SHA1}]`; const artifactsBody = `
${exposedContent}${hiddenContent}
\n\n`; const benchmarkResults = {}; for (const platform of platforms) { const benchmarkPath = path.resolve( __dirname, '..', path.join('test-artifacts', platform, 'benchmark', 'pageload.json'), ); try { const data = await fs.readFile(benchmarkPath, 'utf8'); const benchmark = JSON.parse(data); benchmarkResults[platform] = benchmark; } catch (error) { if (error.code === 'ENOENT') { console.log(`No benchmark data found for ${platform}; skipping`); } else { console.error( `Error encountered processing benchmark data for '${platform}': '${error}'`, ); } } } const summaryPlatform = 'chrome'; const summaryPage = 'home'; let commentBody = artifactsBody; if (benchmarkResults[summaryPlatform]) { try { const summaryPageLoad = Math.round( parseFloat(benchmarkResults[summaryPlatform][summaryPage].average.load), ); const summaryPageLoadMarginOfError = Math.round( parseFloat( benchmarkResults[summaryPlatform][summaryPage].marginOfError.load, ), ); const benchmarkSummary = `Page Load Metrics (${summaryPageLoad} ± ${summaryPageLoadMarginOfError} ms)`; const allPlatforms = new Set(); const allPages = new Set(); const allMetrics = new Set(); const allMeasures = new Set(); for (const platform of Object.keys(benchmarkResults)) { allPlatforms.add(platform); const platformBenchmark = benchmarkResults[platform]; const pages = Object.keys(platformBenchmark); for (const page of pages) { allPages.add(page); const pageBenchmark = platformBenchmark[page]; const measures = Object.keys(pageBenchmark); for (const measure of measures) { allMeasures.add(measure); const measureBenchmark = pageBenchmark[measure]; const metrics = Object.keys(measureBenchmark); for (const metric of metrics) { allMetrics.add(metric); } } } } const tableRows = []; for (const platform of allPlatforms) { const pageRows = []; for (const page of allPages) { const metricRows = []; for (const metric of allMetrics) { let metricData = `${metric}`; for (const measure of allMeasures) { metricData += `${Math.round( parseFloat(benchmarkResults[platform][page][measure][metric]), )}`; } metricRows.push(metricData); } metricRows[0] = `${capitalizeFirstLetter(page)}${metricRows[0]}`; pageRows.push(...metricRows); } pageRows[0] = `${capitalizeFirstLetter(platform)}${pageRows[0]}`; for (const row of pageRows) { tableRows.push(`${row}`); } } const benchmarkTableHeaders = ['Platform', 'Page', 'Metric']; for (const measure of allMeasures) { benchmarkTableHeaders.push(`${capitalizeFirstLetter(measure)} (ms)`); } const benchmarkTableHeader = `${benchmarkTableHeaders .map((header) => `${header}`) .join('')}`; const benchmarkTableBody = `${tableRows.join('')}`; const benchmarkTable = `${benchmarkTableHeader}${benchmarkTableBody}
`; const benchmarkBody = `
${benchmarkSummary}${benchmarkTable}
\n\n`; commentBody += `${benchmarkBody}`; } catch (error) { console.error(`Error constructing benchmark results: '${error}'`); } } else { console.log(`No results for ${summaryPlatform} found; skipping benchmark`); } try { const prBundleSizeStats = JSON.parse( await fs.readFile( path.resolve( __dirname, '..', path.join('test-artifacts', 'chrome', 'mv3', 'bundle_size.json'), ), 'utf-8', ), ); const devBundleSizeStats = await ( await fetch(bundleSizeDataUrl, { method: 'GET', }) ).json(); const prSizes = { background: prBundleSizeStats.background.size, ui: prBundleSizeStats.ui.size, common: prBundleSizeStats.common.size, }; const devSizes = Object.keys(prSizes).reduce((sizes, part) => { sizes[part] = devBundleSizeStats[PARENT_COMMIT][part] || 0; return sizes; }, {}); const diffs = Object.keys(prSizes).reduce((output, part) => { output[part] = prSizes[part] - devSizes[part]; return output; }, {}); const sizeDiffRows = Object.keys(diffs).map( (part) => `${part}: ${diffs[part]} bytes`, ); const sizeDiffHiddenContent = ``; const sizeDiff = diffs.background + diffs.common; const sizeDiffWarning = sizeDiff > 0 ? `🚨 Warning! Bundle size has increased!` : `🚀 Bundle size reduced!`; const sizeDiffExposedContent = sizeDiff === 0 ? `Bundle size diffs` : `Bundle size diffs [${sizeDiffWarning}]`; const sizeDiffBody = `
${sizeDiffExposedContent}${sizeDiffHiddenContent}
\n\n`; commentBody += sizeDiffBody; } catch (error) { console.error(`Error constructing bundle size diffs results: '${error}'`); } try { const highlights = await getHighlights({ artifactBase: BUILD_LINK_BASE }); if (highlights) { const highlightsBody = `### highlights:\n${highlights}\n`; commentBody += highlightsBody; } } catch (error) { console.error(`Error constructing highlight results: '${error}'`); } const JSON_PAYLOAD = JSON.stringify({ body: commentBody }); const POST_COMMENT_URI = `https://api.github.com/repos/metamask/metamask-extension/issues/${CIRCLE_PR_NUMBER}/comments`; console.log(`Announcement:\n${commentBody}`); console.log(`Posting to: ${POST_COMMENT_URI}`); const response = await fetch(POST_COMMENT_URI, { method: 'POST', body: JSON_PAYLOAD, headers: { 'User-Agent': 'metamaskbot', Authorization: `token ${GITHUB_COMMENT_TOKEN}`, }, }); if (!response.ok) { throw new Error(`Post comment failed with status '${response.statusText}'`); } }