diff --git a/.github/scripts/add-preview-links.mjs b/.github/scripts/add-preview-links.mjs index d67e59b105..dca2777425 100644 --- a/.github/scripts/add-preview-links.mjs +++ b/.github/scripts/add-preview-links.mjs @@ -6,10 +6,21 @@ export const END_MARKER = ''; const GITHUB_API_URL = 'https://api.github.com'; const GITHUB_API_VERSION = '2026-03-10'; -const MAIN_BASE_URL = 'https://main--perf-html.netlify.app'; +const NETLIFY_HOST_SUFFIX = '--perf-html.netlify.app'; +const MAIN_BASE_URL = `https://main${NETLIFY_HOST_SUFFIX}`; const PROFILE_HOST = 'profiler.firefox.com'; const SHARE_HOST = 'share.firefox.dev'; +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +const PROFILE_URL_HOSTS_REGEXP = [ + 'share\\.firefox\\.dev', + 'profiler\\.firefox\\.com', + `[a-z0-9-]+${escapeRegExp(NETLIFY_HOST_SUFFIX)}`, +].join('|'); + export function hasDeployPreviewLink(body) { return ( body.includes(START_MARKER) || @@ -17,11 +28,19 @@ export function hasDeployPreviewLink(body) { ); } -export function extractIssueNumbers(text) { +export function extractIssueNumbers( + text, + owner = 'firefox-devtools', + repo = 'profiler' +) { const issueNumbers = new Set(); const markdownIssueRegExp = /(?:^|[^\w/-])#(\d+)\b/g; - const issueUrlRegExp = - /https:\/\/github\.com\/firefox-devtools\/profiler\/issues\/(\d+)\b/g; + const issueUrlRegExp = new RegExp( + `https://github\\.com/${escapeRegExp(owner)}/${escapeRegExp( + repo + )}/(?:issues|pull)/(\\d+)\\b`, + 'g' + ); for (const match of text.matchAll(markdownIssueRegExp)) { issueNumbers.add(Number(match[1])); @@ -36,8 +55,10 @@ export function extractIssueNumbers(text) { export function extractProfileUrls(text) { const urls = []; - const profileUrlRegExp = - /https:\/\/(?:share\.firefox\.dev|profiler\.firefox\.com)\/[^\s<>)\]]+/g; + const profileUrlRegExp = new RegExp( + `https://(?:${PROFILE_URL_HOSTS_REGEXP})/[^\\s<>)\\]]+`, + 'g' + ); for (const match of text.matchAll(profileUrlRegExp)) { // Profile links are often followed by punctuation in prose, for example @@ -51,7 +72,10 @@ export function extractProfileUrls(text) { export function profileUrlToPath(profileUrl) { const url = new URL(profileUrl); - if (url.hostname !== PROFILE_HOST) { + if ( + url.hostname !== PROFILE_HOST && + !url.hostname.endsWith(NETLIFY_HOST_SUFFIX) + ) { return null; } @@ -59,21 +83,27 @@ export function profileUrlToPath(profileUrl) { return path === '/' ? null : path; } -// Returns the path for a profiler.firefox.com URL, and follows share.firefox.dev -// redirects to resolve short links to their full profiler.firefox.com URL. +// Returns the profile path from profiler.firefox.com, main, or deploy preview +// URLs, and follows share.firefox.dev redirects to resolve short links. export async function resolveProfileUrlToPath(profileUrl, fetchImpl = fetch) { const url = new URL(profileUrl); + const path = profileUrlToPath(profileUrl); - if (url.hostname === PROFILE_HOST) { - return profileUrlToPath(profileUrl); + if (path) { + return path; } if (url.hostname !== SHARE_HOST) { return null; } - const response = await fetchImpl(profileUrl, { redirect: 'follow' }); - return profileUrlToPath(response.url); + try { + const response = await fetchImpl(profileUrl, { redirect: 'follow' }); + return profileUrlToPath(response.url); + } catch (error) { + console.warn(`Could not resolve ${profileUrl}: ${error.message}`); + return null; + } } export function normalizePath(path) { @@ -159,7 +189,7 @@ async function findProfilePath({ owner, repo, pullRequest, token }) { } } - for (const issueNumber of extractIssueNumbers(pullRequestText)) { + for (const issueNumber of extractIssueNumbers(pullRequestText, owner, repo)) { let issueTexts; try { diff --git a/.github/scripts/add-preview-links.test.mjs b/.github/scripts/add-preview-links.test.mjs index 57c7482f8e..521ee17149 100644 --- a/.github/scripts/add-preview-links.test.mjs +++ b/.github/scripts/add-preview-links.test.mjs @@ -29,15 +29,17 @@ test('hasDeployPreviewLink detects generated and manual preview links', () => { }); test('extractIssueNumbers finds markdown references and issue URLs', () => { - assert.deepEqual( - extractIssueNumbers( - 'Fixes #5598 and see https://github.com/firefox-devtools/profiler/issues/6083. Duplicate #5598.' - ), - [5598, 6083] - ); + const text = [ + 'Fixes #5598.', + 'See https://github.com/firefox-devtools/profiler/issues/6083.', + 'Follow-up to https://github.com/firefox-devtools/profiler/pull/6017.', + 'Duplicate #5598.', + ].join(' '); + + assert.deepEqual(extractIssueNumbers(text), [5598, 6083, 6017]); }); -test('extractProfileUrls finds profiler and share URLs', () => { +test('extractProfileUrls finds profiler, share, and preview URLs', () => { assert.deepEqual( extractProfileUrls('Profile: https://share.firefox.dev/466MJwC.'), ['https://share.firefox.dev/466MJwC'] @@ -48,6 +50,15 @@ test('extractProfileUrls finds profiler and share URLs', () => { ), ['https://profiler.firefox.com/public/abc/calltree/?thread=1&v=16'] ); + const previewLinks = [ + '[Main](https://main--perf-html.netlify.app/public/abc/)', + '[Deploy preview](https://deploy-preview-6017--perf-html.netlify.app/public/abc/)', + ].join(' | '); + + assert.deepEqual(extractProfileUrls(previewLinks), [ + 'https://main--perf-html.netlify.app/public/abc/', + 'https://deploy-preview-6017--perf-html.netlify.app/public/abc/', + ]); }); test('profileUrlToPath keeps the profiler path, query, and hash', () => { @@ -58,6 +69,16 @@ test('profileUrlToPath keeps the profiler path, query, and hash', () => { '/public/abc/flame-graph/?thread=1&v=16#hash' ); assert.equal(profileUrlToPath('https://profiler.firefox.com/'), null); + assert.equal( + profileUrlToPath('https://main--perf-html.netlify.app/public/abc/'), + '/public/abc/' + ); + assert.equal( + profileUrlToPath( + 'https://deploy-preview-6017--perf-html.netlify.app/public/abc/' + ), + '/public/abc/' + ); }); test('resolveProfileUrlToPath follows share.firefox.dev redirects', async () => { @@ -74,11 +95,62 @@ test('resolveProfileUrlToPath follows share.firefox.dev redirects', async () => ); }); +test('resolveProfileUrlToPath handles main and deploy preview links', async () => { + assert.equal( + await resolveProfileUrlToPath( + 'https://main--perf-html.netlify.app/public/abc/?v=16' + ), + '/public/abc/?v=16' + ); + assert.equal( + await resolveProfileUrlToPath( + 'https://deploy-preview-6017--perf-html.netlify.app/public/abc/?v=16' + ), + '/public/abc/?v=16' + ); +}); + +test('resolveProfileUrlToPath ignores broken share.firefox.dev links', async () => { + const unresolvedFetchImpl = async () => ({ + url: 'https://share.firefox.dev/nonsense', + }); + const failingFetchImpl = async () => { + throw new Error('Network error'); + }; + + assert.equal( + await resolveProfileUrlToPath( + 'https://share.firefox.dev/nonsense', + unresolvedFetchImpl + ), + null + ); + + const originalWarn = console.warn; + + try { + console.warn = () => {}; + assert.equal( + await resolveProfileUrlToPath( + 'https://share.firefox.dev/nonsense', + failingFetchImpl + ), + null + ); + } finally { + console.warn = originalWarn; + } +}); + test('buildPreviewLinks uses the main branch and deploy preview hosts', () => { + const path = '/public/abc/marker-table/?thread=0&v=16'; + const mainUrl = `https://main--perf-html.netlify.app${path}`; + const previewUrl = `https://deploy-preview-6083--perf-html.netlify.app${path}`; + assert.equal(normalizePath('public/abc'), '/public/abc'); assert.equal( - buildPreviewLinks(6083, '/public/abc/marker-table/?thread=0&v=16'), - '[Main](https://main--perf-html.netlify.app/public/abc/marker-table/?thread=0&v=16) | [Deploy preview](https://deploy-preview-6083--perf-html.netlify.app/public/abc/marker-table/?thread=0&v=16)' + buildPreviewLinks(6083, path), + `[Main](${mainUrl}) | [Deploy preview](${previewUrl})` ); }); @@ -88,6 +160,12 @@ test('addPreviewLinksToBody prepends a marked block', () => { 'Fixes #5598.', '[Main](main) | [Deploy preview](preview)' ), - `${START_MARKER}\n[Main](main) | [Deploy preview](preview)\n${END_MARKER}\n\nFixes #5598.` + [ + START_MARKER, + '[Main](main) | [Deploy preview](preview)', + END_MARKER, + '', + 'Fixes #5598.', + ].join('\n') ); });