|
|
|
#!/usr/bin/env node
|
|
|
|
const { promises: fs } = require('fs');
|
|
|
|
const path = require('path');
|
|
|
|
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);
|
|
|
|
|
|
|
|
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://${CIRCLE_BUILD_NUM}-42009758-gh.circle-artifacts.com/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 `<a href="${url}">${platform}</a>`;
|
|
|
|
})
|
|
|
|
.join(', ');
|
|
|
|
const betaBuildLinks = platforms
|
|
|
|
.map((platform) => {
|
|
|
|
const url = `${BUILD_LINK_BASE}/builds-beta/metamask-beta-${platform}-${VERSION}.zip`;
|
|
|
|
return `<a href="${url}">${platform}</a>`;
|
|
|
|
})
|
|
|
|
.join(', ');
|
|
|
|
const flaskBuildLinks = platforms
|
|
|
|
.map((platform) => {
|
|
|
|
const url = `${BUILD_LINK_BASE}/builds-flask/metamask-flask-${platform}-${VERSION}.zip`;
|
|
|
|
return `<a href="${url}">${platform}</a>`;
|
|
|
|
})
|
|
|
|
.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 = `<a href="${url}">${fileIndex || fileRoot}</a>`;
|
|
|
|
|
|
|
|
if (fileRoot in bundles) {
|
|
|
|
bundles[fileRoot].push(link);
|
|
|
|
} else {
|
|
|
|
bundles[fileRoot] = [link];
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
const bundleMarkup = `<ul>${Object.keys(bundles)
|
|
|
|
.map((key) => `<li>${key}: ${bundles[key].join(', ')}</li>`)
|
|
|
|
.join('')}</ul>`;
|
|
|
|
|
|
|
|
const coverageUrl = `${BUILD_LINK_BASE}/coverage/index.html`;
|
|
|
|
const coverageLink = `<a href="${coverageUrl}">Report</a>`;
|
|
|
|
|
|
|
|
const storybookUrl = `${BUILD_LINK_BASE}/storybook/index.html`;
|
|
|
|
const storybookLink = `<a href="${storybookUrl}">Storybook</a>`;
|
|
|
|
|
|
|
|
// links to bundle browser builds
|
|
|
|
const depVizUrl = `${BUILD_LINK_BASE}/build-artifacts/build-viz/index.html`;
|
|
|
|
const depVizLink = `<a href="${depVizUrl}">Build System</a>`;
|
|
|
|
|
|
|
|
// 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}`,
|
|
|
|
`code coverage: ${coverageLink}`,
|
|
|
|
`storybook: ${storybookLink}`,
|
|
|
|
`<a href="${allArtifactsUrl}">all artifacts</a>`,
|
|
|
|
`<details>
|
|
|
|
<summary>bundle viz:</summary>
|
|
|
|
${bundleMarkup}
|
|
|
|
</details>`,
|
|
|
|
];
|
|
|
|
const hiddenContent = `<ul>${contentRows
|
|
|
|
.map((row) => `<li>${row}</li>`)
|
|
|
|
.join('\n')}</ul>`;
|
|
|
|
const exposedContent = `Builds ready [${SHORT_SHA1}]`;
|
|
|
|
const artifactsBody = `<details><summary>${exposedContent}</summary>${hiddenContent}</details>\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 = `<td>${metric}</td>`;
|
|
|
|
for (const measure of allMeasures) {
|
|
|
|
metricData += `<td align="right">${Math.round(
|
|
|
|
parseFloat(benchmarkResults[platform][page][measure][metric]),
|
|
|
|
)}</td>`;
|
|
|
|
}
|
|
|
|
metricRows.push(metricData);
|
|
|
|
}
|
|
|
|
metricRows[0] = `<td rowspan="${
|
|
|
|
allMetrics.size
|
|
|
|
}">${capitalizeFirstLetter(page)}</td>${metricRows[0]}`;
|
|
|
|
pageRows.push(...metricRows);
|
|
|
|
}
|
|
|
|
pageRows[0] = `<td rowspan="${
|
|
|
|
allPages.size * allMetrics.size
|
|
|
|
}">${capitalizeFirstLetter(platform)}</td>${pageRows[0]}`;
|
|
|
|
for (const row of pageRows) {
|
|
|
|
tableRows.push(`<tr>${row}</tr>`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const benchmarkTableHeaders = ['Platform', 'Page', 'Metric'];
|
|
|
|
for (const measure of allMeasures) {
|
|
|
|
benchmarkTableHeaders.push(`${capitalizeFirstLetter(measure)} (ms)`);
|
|
|
|
}
|
|
|
|
const benchmarkTableHeader = `<thead><tr>${benchmarkTableHeaders
|
|
|
|
.map((header) => `<th>${header}</th>`)
|
|
|
|
.join('')}</tr></thead>`;
|
|
|
|
const benchmarkTableBody = `<tbody>${tableRows.join('')}</tbody>`;
|
|
|
|
const benchmarkTable = `<table>${benchmarkTableHeader}${benchmarkTableBody}</table>`;
|
|
|
|
const benchmarkBody = `<details><summary>${benchmarkSummary}</summary>${benchmarkTable}</details>\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 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}'`);
|
|
|
|
}
|
|
|
|
}
|