From cdbc0780ac8fa5281eb4d88f22d9bb5589a9b3a9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Jun 2026 21:10:56 +0000 Subject: [PATCH 01/12] Add Node 26-safe changeset changelog generator Co-authored-by: joshblack <3901764+joshblack@users.noreply.github.com> --- .changeset/config.json | 2 +- script/changeset-changelog.cjs | 325 +++++++++++++++++++++++++++++++++ 2 files changed, 326 insertions(+), 1 deletion(-) create mode 100644 script/changeset-changelog.cjs diff --git a/.changeset/config.json b/.changeset/config.json index c7ceaac47f7..d76dda5d6b3 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -1,6 +1,6 @@ { "$schema": "https://unpkg.com/@changesets/config@1.4.0/schema.json", - "changelog": ["@changesets/changelog-github", {"repo": "primer/react"}], + "changelog": ["../script/changeset-changelog.cjs", {"repo": "primer/react"}], "commit": false, "linked": [], "access": "public", diff --git a/script/changeset-changelog.cjs b/script/changeset-changelog.cjs new file mode 100644 index 00000000000..388e835032d --- /dev/null +++ b/script/changeset-changelog.cjs @@ -0,0 +1,325 @@ +const validRepoNameRegex = /^[\w.-]+\/[\w.-]+$/ + +function readEnv() { + return { + GITHUB_GRAPHQL_URL: process.env.GITHUB_GRAPHQL_URL || 'https://api.github.com/graphql', + GITHUB_SERVER_URL: (process.env.GITHUB_SERVER_URL || 'https://github.com').replace(/\/$/, ''), + GITHUB_TOKEN: process.env.GITHUB_TOKEN, + } +} + +function makeQuery(repos) { + return ` + query { + ${Object.keys(repos) + .map( + (repo, index) => + `a${index}: repository( + owner: ${JSON.stringify(repo.split('/')[0])} + name: ${JSON.stringify(repo.split('/')[1])} + ) { + ${repos[repo] + .map(data => + data.kind === 'commit' + ? `a${data.commit}: object(expression: ${JSON.stringify(data.commit)}) { + ... on Commit { + commitUrl + associatedPullRequests(first: 50) { + nodes { + number + url + mergedAt + author { + login + url + } + } + } + author { + user { + login + url + } + } + }}` + : `pr__${data.pull}: pullRequest(number: ${data.pull}) { + url + author { + login + url + } + mergeCommit { + commitUrl + abbreviatedOid + } + }`, + ) + .join('\n')} + }`, + ) + .join('\n')} + } + ` +} + +function getFetchImplementation() { + if (typeof globalThis.fetch === 'function') { + return globalThis.fetch + } + + return require('node-fetch') +} + +function isRetryableError(error) { + return ( + error?.code === 'ERR_STREAM_PREMATURE_CLOSE' || + error?.cause?.code === 'ERR_STREAM_PREMATURE_CLOSE' || + /premature close|terminated|socket hang up|network timeout/i.test(error?.message || '') + ) +} + +async function fetchGitHubData(query) { + const {GITHUB_GRAPHQL_URL, GITHUB_TOKEN} = readEnv() + const fetch = getFetchImplementation() + const headers = { + Authorization: `Token ${GITHUB_TOKEN}`, + 'Accept-Encoding': 'identity', + 'Content-Type': 'application/json', + } + const body = JSON.stringify({query}) + let lastError + + for (let attempt = 0; attempt < 3; attempt++) { + try { + const response = await fetch(GITHUB_GRAPHQL_URL, { + method: 'POST', + headers, + body, + compress: false, + }) + + if (!response.ok && (response.status === 429 || response.status >= 500)) { + throw new Error(`GitHub GraphQL request failed with status ${response.status}`) + } + + const data = await response.json() + + if (data.errors) { + throw new Error(`Fetched data from GitHub returned errors\n${JSON.stringify(data.errors, null, 2)}`) + } + + if (!data.data) { + throw new Error(`Fetched data from GitHub has missing data\n${JSON.stringify(data)}`) + } + + return data + } catch (error) { + lastError = error + if (!isRetryableError(error) && !/status (?:429|5\d\d)/.test(error?.message || '')) { + break + } + } + } + + throw new Error(`An error occurred when fetching data from GitHub\n${lastError.message}`) +} + +async function loadGitHubInfo(request) { + const {GITHUB_SERVER_URL, GITHUB_TOKEN} = readEnv() + + if (!GITHUB_TOKEN) { + throw new Error( + `Please create a GitHub personal access token at ${GITHUB_SERVER_URL}/settings/tokens/new with \`read:user\` and \`repo:status\` permissions and add it as the GITHUB_TOKEN environment variable`, + ) + } + + if (!request.repo) { + throw new Error('Please pass a GitHub repository in the form of userOrOrg/repoName to getInfo') + } + + if (!validRepoNameRegex.test(request.repo)) { + throw new Error( + `Please pass a valid GitHub repository in the form of userOrOrg/repoName to getInfo (it has to match the "${validRepoNameRegex.source}" pattern)`, + ) + } + + const repos = { + [request.repo]: [request.kind === 'commit' ? {kind: 'commit', commit: request.commit} : {kind: 'pull', pull: request.pull}], + } + const data = await fetchGitHubData(makeQuery(repos)) + return data.data.a0[request.kind === 'commit' ? `a${request.commit}` : `pr__${request.pull}`] +} + +async function getInfo(request) { + if (!request.commit) { + throw new Error('Please pass a commit SHA to getInfo') + } + + const data = await loadGitHubInfo({ + kind: 'commit', + repo: request.repo, + commit: request.commit, + }) + let user = data.author?.user || null + const associatedPullRequest = + data.associatedPullRequests?.nodes?.length > 0 + ? data.associatedPullRequests.nodes.sort((a, b) => { + if (a.mergedAt === null && b.mergedAt === null) { + return 0 + } + + if (a.mergedAt === null) { + return 1 + } + + if (b.mergedAt === null) { + return -1 + } + + a = new Date(a.mergedAt) + b = new Date(b.mergedAt) + return a > b ? 1 : a < b ? -1 : 0 + })[0] + : null + + if (associatedPullRequest) { + user = associatedPullRequest.author + } + + return { + user: user ? user.login : null, + pull: associatedPullRequest ? associatedPullRequest.number : null, + links: { + commit: `[\`${request.commit.slice(0, 7)}\`](${data.commitUrl})`, + pull: associatedPullRequest ? `[#${associatedPullRequest.number}](${associatedPullRequest.url})` : null, + user: user ? `[@${user.login}](${user.url})` : null, + }, + } +} + +async function getInfoFromPullRequest(request) { + if (request.pull === undefined) { + throw new Error('Please pass a pull request number') + } + + const {GITHUB_SERVER_URL} = readEnv() + const data = await loadGitHubInfo({ + kind: 'pull', + repo: request.repo, + pull: request.pull, + }) + const user = data?.author + const commit = data?.mergeCommit + + return { + user: user ? user.login : null, + commit: commit ? commit.abbreviatedOid : null, + links: { + commit: commit ? `[\`${commit.abbreviatedOid.slice(0, 7)}\`](${commit.commitUrl})` : null, + pull: `[#${request.pull}](${GITHUB_SERVER_URL}/${request.repo}/pull/${request.pull})`, + user: user ? `[@${user.login}](${user.url})` : null, + }, + } +} + +const changelogFunctions = { + getDependencyReleaseLine: async (changesets, dependenciesUpdated, options) => { + if (!options.repo) { + throw new Error( + 'Please provide a repo to this changelog generator like this:\n"changelog": ["../script/changeset-changelog.cjs", { "repo": "org/repo" }]', + ) + } + + if (dependenciesUpdated.length === 0) { + return '' + } + + const changesetLink = `- Updated dependencies [${( + await Promise.all( + changesets.map(async changeset => { + if (changeset.commit) { + const {links} = await getInfo({ + repo: options.repo, + commit: changeset.commit, + }) + return links.commit + } + }), + ) + ) + .filter(Boolean) + .join(', ')}]:` + const updatedDependenciesList = dependenciesUpdated.map(dependency => ` - ${dependency.name}@${dependency.newVersion}`) + return [changesetLink, ...updatedDependenciesList].join('\n') + }, + getReleaseLine: async (changeset, _type, options) => { + if (!options?.repo) { + throw new Error( + 'Please provide a repo to this changelog generator like this:\n"changelog": ["../script/changeset-changelog.cjs", { "repo": "org/repo" }]', + ) + } + + let prFromSummary + let commitFromSummary + const usersFromSummary = [] + const replacedChangelog = changeset.summary + .replace(/^\s*(?:pr|pull|pull\s+request):\s*#?(\d+)/im, (_, pr) => { + const num = Number(pr) + if (!Number.isNaN(num)) { + prFromSummary = num + } + return '' + }) + .replace(/^\s*commit:\s*([^\s]+)/im, (_, commit) => { + commitFromSummary = commit + return '' + }) + .replace(/^\s*(?:author|user):\s*@?([^\s]+)/gim, (_, user) => { + usersFromSummary.push(user) + return '' + }) + .trim() + const [firstLine, ...futureLines] = replacedChangelog.split('\n').map(line => line.trimEnd()) + const links = await (async () => { + if (prFromSummary !== undefined) { + let {links} = await getInfoFromPullRequest({ + repo: options.repo, + pull: prFromSummary, + }) + + if (commitFromSummary) { + const shortCommitId = commitFromSummary.slice(0, 7) + links = { + ...links, + commit: `[\`${shortCommitId}\`](https://github.com/${options.repo}/commit/${commitFromSummary})`, + } + } + + return links + } + + const commitToFetchFrom = commitFromSummary || changeset.commit + + if (commitToFetchFrom) { + const {links} = await getInfo({ + repo: options.repo, + commit: commitToFetchFrom, + }) + return links + } + + return { + commit: null, + pull: null, + user: null, + } + })() + const users = usersFromSummary.length + ? usersFromSummary.map(userFromSummary => `[@${userFromSummary}](https://github.com/${userFromSummary})`).join(', ') + : links.user + const prefix = [links.pull === null ? '' : ` ${links.pull}`, links.commit === null ? '' : ` ${links.commit}`, users === null ? '' : ` Thanks ${users}!`].join('') + return `\n\n-${prefix ? `${prefix} -` : ''} ${firstLine}\n${futureLines.map(line => ` ${line}`).join('\n')}` + }, +} + +module.exports = changelogFunctions From e151ee3b3163fa65392f521c6ca997cbab64519a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Jun 2026 21:11:14 +0000 Subject: [PATCH 02/12] Format changeset changelog generator Co-authored-by: joshblack <3901764+joshblack@users.noreply.github.com> --- script/changeset-changelog.cjs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/script/changeset-changelog.cjs b/script/changeset-changelog.cjs index 388e835032d..5d22e1605a2 100644 --- a/script/changeset-changelog.cjs +++ b/script/changeset-changelog.cjs @@ -144,7 +144,9 @@ async function loadGitHubInfo(request) { } const repos = { - [request.repo]: [request.kind === 'commit' ? {kind: 'commit', commit: request.commit} : {kind: 'pull', pull: request.pull}], + [request.repo]: [ + request.kind === 'commit' ? {kind: 'commit', commit: request.commit} : {kind: 'pull', pull: request.pull}, + ], } const data = await fetchGitHubData(makeQuery(repos)) return data.data.a0[request.kind === 'commit' ? `a${request.commit}` : `pr__${request.pull}`] @@ -249,7 +251,9 @@ const changelogFunctions = { ) .filter(Boolean) .join(', ')}]:` - const updatedDependenciesList = dependenciesUpdated.map(dependency => ` - ${dependency.name}@${dependency.newVersion}`) + const updatedDependenciesList = dependenciesUpdated.map( + dependency => ` - ${dependency.name}@${dependency.newVersion}`, + ) return [changesetLink, ...updatedDependenciesList].join('\n') }, getReleaseLine: async (changeset, _type, options) => { @@ -315,9 +319,15 @@ const changelogFunctions = { } })() const users = usersFromSummary.length - ? usersFromSummary.map(userFromSummary => `[@${userFromSummary}](https://github.com/${userFromSummary})`).join(', ') + ? usersFromSummary + .map(userFromSummary => `[@${userFromSummary}](https://github.com/${userFromSummary})`) + .join(', ') : links.user - const prefix = [links.pull === null ? '' : ` ${links.pull}`, links.commit === null ? '' : ` ${links.commit}`, users === null ? '' : ` Thanks ${users}!`].join('') + const prefix = [ + links.pull === null ? '' : ` ${links.pull}`, + links.commit === null ? '' : ` ${links.commit}`, + users === null ? '' : ` Thanks ${users}!`, + ].join('') return `\n\n-${prefix ? `${prefix} -` : ''} ${firstLine}\n${futureLines.map(line => ` ${line}`).join('\n')}` }, } From 72456305558c70d92b382fa2c06b002f6ef52f09 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Jun 2026 21:16:06 +0000 Subject: [PATCH 03/12] Address release changelog review feedback Co-authored-by: joshblack <3901764+joshblack@users.noreply.github.com> --- script/changeset-changelog.cjs | 42 ++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/script/changeset-changelog.cjs b/script/changeset-changelog.cjs index 5d22e1605a2..b7baa3223b4 100644 --- a/script/changeset-changelog.cjs +++ b/script/changeset-changelog.cjs @@ -64,14 +64,25 @@ function makeQuery(repos) { function getFetchImplementation() { if (typeof globalThis.fetch === 'function') { - return globalThis.fetch + return { + fetch: globalThis.fetch, + useNodeFetchOptions: false, + } } - return require('node-fetch') + return { + fetch: require('node-fetch'), + useNodeFetchOptions: true, + } +} + +function isRetryableStatus(status) { + return status === 429 || status >= 500 } function isRetryableError(error) { return ( + error?.retryable === true || error?.code === 'ERR_STREAM_PREMATURE_CLOSE' || error?.cause?.code === 'ERR_STREAM_PREMATURE_CLOSE' || /premature close|terminated|socket hang up|network timeout/i.test(error?.message || '') @@ -80,7 +91,7 @@ function isRetryableError(error) { async function fetchGitHubData(query) { const {GITHUB_GRAPHQL_URL, GITHUB_TOKEN} = readEnv() - const fetch = getFetchImplementation() + const {fetch, useNodeFetchOptions} = getFetchImplementation() const headers = { Authorization: `Token ${GITHUB_TOKEN}`, 'Accept-Encoding': 'identity', @@ -91,15 +102,22 @@ async function fetchGitHubData(query) { for (let attempt = 0; attempt < 3; attempt++) { try { - const response = await fetch(GITHUB_GRAPHQL_URL, { + const requestOptions = { method: 'POST', headers, body, - compress: false, - }) + } + + if (useNodeFetchOptions) { + requestOptions.compress = false + } + + const response = await fetch(GITHUB_GRAPHQL_URL, requestOptions) - if (!response.ok && (response.status === 429 || response.status >= 500)) { - throw new Error(`GitHub GraphQL request failed with status ${response.status}`) + if (!response.ok && isRetryableStatus(response.status)) { + const error = new Error(`GitHub GraphQL request failed with status ${response.status}`) + error.retryable = true + throw error } const data = await response.json() @@ -115,7 +133,7 @@ async function fetchGitHubData(query) { return data } catch (error) { lastError = error - if (!isRetryableError(error) && !/status (?:429|5\d\d)/.test(error?.message || '')) { + if (!isRetryableError(error)) { break } } @@ -178,9 +196,9 @@ async function getInfo(request) { return -1 } - a = new Date(a.mergedAt) - b = new Date(b.mergedAt) - return a > b ? 1 : a < b ? -1 : 0 + const dateA = new Date(a.mergedAt) + const dateB = new Date(b.mergedAt) + return dateA > dateB ? 1 : dateA < dateB ? -1 : 0 })[0] : null From 070cbedfe5f02333708865c544a2e494bb09c10a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Jun 2026 21:18:55 +0000 Subject: [PATCH 04/12] Refine release changelog helper Co-authored-by: joshblack <3901764+joshblack@users.noreply.github.com> --- script/changeset-changelog.cjs | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/script/changeset-changelog.cjs b/script/changeset-changelog.cjs index b7baa3223b4..1643081a157 100644 --- a/script/changeset-changelog.cjs +++ b/script/changeset-changelog.cjs @@ -1,4 +1,5 @@ const validRepoNameRegex = /^[\w.-]+\/[\w.-]+$/ +const MAX_RETRY_ATTEMPTS = 3 function readEnv() { return { @@ -89,6 +90,15 @@ function isRetryableError(error) { ) } +function getCommitLink(commit, url, options = {}) { + if (typeof commit !== 'string' || commit.length === 0) { + throw new Error('Expected a commit SHA when generating changelog links') + } + + const label = options.alreadyAbbreviated ? commit : commit.slice(0, 7) + return `[\`${label}\`](${url})` +} + async function fetchGitHubData(query) { const {GITHUB_GRAPHQL_URL, GITHUB_TOKEN} = readEnv() const {fetch, useNodeFetchOptions} = getFetchImplementation() @@ -100,7 +110,7 @@ async function fetchGitHubData(query) { const body = JSON.stringify({query}) let lastError - for (let attempt = 0; attempt < 3; attempt++) { + for (let attempt = 0; attempt < MAX_RETRY_ATTEMPTS; attempt++) { try { const requestOptions = { method: 'POST', @@ -139,7 +149,11 @@ async function fetchGitHubData(query) { } } - throw new Error(`An error occurred when fetching data from GitHub\n${lastError.message}`) + throw new Error( + `An error occurred when fetching data from GitHub after ${MAX_RETRY_ATTEMPTS} attempts\n${ + lastError.stack || lastError.message + }`, + ) } async function loadGitHubInfo(request) { @@ -210,7 +224,7 @@ async function getInfo(request) { user: user ? user.login : null, pull: associatedPullRequest ? associatedPullRequest.number : null, links: { - commit: `[\`${request.commit.slice(0, 7)}\`](${data.commitUrl})`, + commit: getCommitLink(request.commit, data.commitUrl), pull: associatedPullRequest ? `[#${associatedPullRequest.number}](${associatedPullRequest.url})` : null, user: user ? `[@${user.login}](${user.url})` : null, }, @@ -235,7 +249,7 @@ async function getInfoFromPullRequest(request) { user: user ? user.login : null, commit: commit ? commit.abbreviatedOid : null, links: { - commit: commit ? `[\`${commit.abbreviatedOid.slice(0, 7)}\`](${commit.commitUrl})` : null, + commit: commit ? getCommitLink(commit.abbreviatedOid, commit.commitUrl, {alreadyAbbreviated: true}) : null, pull: `[#${request.pull}](${GITHUB_SERVER_URL}/${request.repo}/pull/${request.pull})`, user: user ? `[@${user.login}](${user.url})` : null, }, @@ -310,10 +324,9 @@ const changelogFunctions = { }) if (commitFromSummary) { - const shortCommitId = commitFromSummary.slice(0, 7) links = { ...links, - commit: `[\`${shortCommitId}\`](https://github.com/${options.repo}/commit/${commitFromSummary})`, + commit: getCommitLink(commitFromSummary, `https://github.com/${options.repo}/commit/${commitFromSummary}`), } } From 9d8eca0337331b5cc9a9379a26495d63098ee36f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Jun 2026 21:21:54 +0000 Subject: [PATCH 05/12] Polish release changelog helper Co-authored-by: joshblack <3901764+joshblack@users.noreply.github.com> --- script/changeset-changelog.cjs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/script/changeset-changelog.cjs b/script/changeset-changelog.cjs index 1643081a157..e82a21d1582 100644 --- a/script/changeset-changelog.cjs +++ b/script/changeset-changelog.cjs @@ -103,7 +103,7 @@ async function fetchGitHubData(query) { const {GITHUB_GRAPHQL_URL, GITHUB_TOKEN} = readEnv() const {fetch, useNodeFetchOptions} = getFetchImplementation() const headers = { - Authorization: `Token ${GITHUB_TOKEN}`, + Authorization: 'Bearer ' + GITHUB_TOKEN, 'Accept-Encoding': 'identity', 'Content-Type': 'application/json', } @@ -212,7 +212,7 @@ async function getInfo(request) { const dateA = new Date(a.mergedAt) const dateB = new Date(b.mergedAt) - return dateA > dateB ? 1 : dateA < dateB ? -1 : 0 + return dateA - dateB })[0] : null @@ -295,6 +295,7 @@ const changelogFunctions = { ) } + const {GITHUB_SERVER_URL} = readEnv() let prFromSummary let commitFromSummary const usersFromSummary = [] @@ -326,7 +327,10 @@ const changelogFunctions = { if (commitFromSummary) { links = { ...links, - commit: getCommitLink(commitFromSummary, `https://github.com/${options.repo}/commit/${commitFromSummary}`), + commit: getCommitLink( + commitFromSummary, + `${GITHUB_SERVER_URL}/${options.repo}/commit/${commitFromSummary}`, + ), } } From b3f2d8d68b0d27665959726e28be06012570a63c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Jun 2026 21:24:27 +0000 Subject: [PATCH 06/12] Document Node 26 release workaround Co-authored-by: joshblack <3901764+joshblack@users.noreply.github.com> --- script/changeset-changelog.cjs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/script/changeset-changelog.cjs b/script/changeset-changelog.cjs index e82a21d1582..41cd89380ba 100644 --- a/script/changeset-changelog.cjs +++ b/script/changeset-changelog.cjs @@ -82,6 +82,7 @@ function isRetryableStatus(status) { } function isRetryableError(error) { + // Node.js v26 can surface GitHub gzip stream failures as premature close errors. return ( error?.retryable === true || error?.code === 'ERR_STREAM_PREMATURE_CLOSE' || @@ -104,6 +105,7 @@ async function fetchGitHubData(query) { const {fetch, useNodeFetchOptions} = getFetchImplementation() const headers = { Authorization: 'Bearer ' + GITHUB_TOKEN, + // Avoid node-fetch gzip handling failures in the release workflow on Node.js v26. 'Accept-Encoding': 'identity', 'Content-Type': 'application/json', } @@ -119,6 +121,7 @@ async function fetchGitHubData(query) { } if (useNodeFetchOptions) { + // Complement the identity encoding request for node-fetch v2, which is used on older Node.js versions. requestOptions.compress = false } From ff0e55bddc6ddd1d2da3a1f3f957c7a631e0c53c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Jun 2026 21:27:01 +0000 Subject: [PATCH 07/12] Harden changelog helper errors Co-authored-by: joshblack <3901764+joshblack@users.noreply.github.com> --- script/changeset-changelog.cjs | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/script/changeset-changelog.cjs b/script/changeset-changelog.cjs index 41cd89380ba..8596d7c273d 100644 --- a/script/changeset-changelog.cjs +++ b/script/changeset-changelog.cjs @@ -13,11 +13,11 @@ function makeQuery(repos) { return ` query { ${Object.keys(repos) - .map( - (repo, index) => - `a${index}: repository( - owner: ${JSON.stringify(repo.split('/')[0])} - name: ${JSON.stringify(repo.split('/')[1])} + .map((repo, index) => { + const [owner, name] = repo.split('/') + return `a${index}: repository( + owner: ${JSON.stringify(owner)} + name: ${JSON.stringify(name)} ) { ${repos[repo] .map(data => @@ -56,8 +56,8 @@ function makeQuery(repos) { }`, ) .join('\n')} - }`, - ) + }` + }) .join('\n')} } ` @@ -102,6 +102,10 @@ function getCommitLink(commit, url, options = {}) { async function fetchGitHubData(query) { const {GITHUB_GRAPHQL_URL, GITHUB_TOKEN} = readEnv() + if (!GITHUB_TOKEN) { + throw new Error('GITHUB_TOKEN is required to fetch changelog data from GitHub') + } + const {fetch, useNodeFetchOptions} = getFetchImplementation() const headers = { Authorization: 'Bearer ' + GITHUB_TOKEN, @@ -153,9 +157,7 @@ async function fetchGitHubData(query) { } throw new Error( - `An error occurred when fetching data from GitHub after ${MAX_RETRY_ATTEMPTS} attempts\n${ - lastError.stack || lastError.message - }`, + `An error occurred when fetching data from GitHub after ${MAX_RETRY_ATTEMPTS} attempts\n${lastError.message}`, ) } From cbce1967b01d66ddeeaa9030ef9457461c583aea Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Jun 2026 21:30:00 +0000 Subject: [PATCH 08/12] Polish changelog helper implementation Co-authored-by: joshblack <3901764+joshblack@users.noreply.github.com> --- script/changeset-changelog.cjs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/script/changeset-changelog.cjs b/script/changeset-changelog.cjs index 8596d7c273d..088fdb0e067 100644 --- a/script/changeset-changelog.cjs +++ b/script/changeset-changelog.cjs @@ -4,7 +4,7 @@ const MAX_RETRY_ATTEMPTS = 3 function readEnv() { return { GITHUB_GRAPHQL_URL: process.env.GITHUB_GRAPHQL_URL || 'https://api.github.com/graphql', - GITHUB_SERVER_URL: (process.env.GITHUB_SERVER_URL || 'https://github.com').replace(/\/$/, ''), + GITHUB_SERVER_URL: (process.env.GITHUB_SERVER_URL || 'https://github.com').replace(/\/+$/, ''), GITHUB_TOKEN: process.env.GITHUB_TOKEN, } } @@ -156,9 +156,11 @@ async function fetchGitHubData(query) { } } - throw new Error( + const error = new Error( `An error occurred when fetching data from GitHub after ${MAX_RETRY_ATTEMPTS} attempts\n${lastError.message}`, ) + error.cause = lastError + throw error } async function loadGitHubInfo(request) { @@ -217,7 +219,7 @@ async function getInfo(request) { const dateA = new Date(a.mergedAt) const dateB = new Date(b.mergedAt) - return dateA - dateB + return dateA.getTime() - dateB.getTime() })[0] : null @@ -321,7 +323,7 @@ const changelogFunctions = { return '' }) .trim() - const [firstLine, ...futureLines] = replacedChangelog.split('\n').map(line => line.trimEnd()) + const [firstLine, ...remainingLines] = replacedChangelog.split('\n').map(line => line.trimEnd()) const links = await (async () => { if (prFromSummary !== undefined) { let {links} = await getInfoFromPullRequest({ @@ -368,7 +370,7 @@ const changelogFunctions = { links.commit === null ? '' : ` ${links.commit}`, users === null ? '' : ` Thanks ${users}!`, ].join('') - return `\n\n-${prefix ? `${prefix} -` : ''} ${firstLine}\n${futureLines.map(line => ` ${line}`).join('\n')}` + return `\n\n-${prefix ? `${prefix} -` : ''} ${firstLine}\n${remainingLines.map(line => ` ${line}`).join('\n')}` }, } From e4c634fbbcc5b072daf7f02e4d74fa43c5f8774b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Jun 2026 21:32:45 +0000 Subject: [PATCH 09/12] Harden changelog helper logging Co-authored-by: joshblack <3901764+joshblack@users.noreply.github.com> --- script/changeset-changelog.cjs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/script/changeset-changelog.cjs b/script/changeset-changelog.cjs index 088fdb0e067..07d634336df 100644 --- a/script/changeset-changelog.cjs +++ b/script/changeset-changelog.cjs @@ -3,7 +3,6 @@ const MAX_RETRY_ATTEMPTS = 3 function readEnv() { return { - GITHUB_GRAPHQL_URL: process.env.GITHUB_GRAPHQL_URL || 'https://api.github.com/graphql', GITHUB_SERVER_URL: (process.env.GITHUB_SERVER_URL || 'https://github.com').replace(/\/+$/, ''), GITHUB_TOKEN: process.env.GITHUB_TOKEN, } @@ -100,8 +99,16 @@ function getCommitLink(commit, url, options = {}) { return `[\`${label}\`](${url})` } +function getErrorSummary(error) { + if (error?.status) { + return `status ${error.status}` + } + + return error?.code || error?.name || 'unknown error' +} + async function fetchGitHubData(query) { - const {GITHUB_GRAPHQL_URL, GITHUB_TOKEN} = readEnv() + const {GITHUB_TOKEN} = readEnv() if (!GITHUB_TOKEN) { throw new Error('GITHUB_TOKEN is required to fetch changelog data from GitHub') } @@ -129,10 +136,11 @@ async function fetchGitHubData(query) { requestOptions.compress = false } - const response = await fetch(GITHUB_GRAPHQL_URL, requestOptions) + const response = await fetch('https://api.github.com/graphql', requestOptions) if (!response.ok && isRetryableStatus(response.status)) { const error = new Error(`GitHub GraphQL request failed with status ${response.status}`) + error.status = response.status error.retryable = true throw error } @@ -140,11 +148,11 @@ async function fetchGitHubData(query) { const data = await response.json() if (data.errors) { - throw new Error(`Fetched data from GitHub returned errors\n${JSON.stringify(data.errors, null, 2)}`) + throw new Error(`Fetched data from GitHub returned ${data.errors.length} error(s)`) } if (!data.data) { - throw new Error(`Fetched data from GitHub has missing data\n${JSON.stringify(data)}`) + throw new Error('Fetched data from GitHub has missing data') } return data @@ -157,7 +165,9 @@ async function fetchGitHubData(query) { } const error = new Error( - `An error occurred when fetching data from GitHub after ${MAX_RETRY_ATTEMPTS} attempts\n${lastError.message}`, + `An error occurred when fetching data from GitHub after ${MAX_RETRY_ATTEMPTS} attempts: ${getErrorSummary( + lastError, + )}`, ) error.cause = lastError throw error From 6fa65c429a6dd92c6668b10b9d7ef7b21789a5b5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Jun 2026 21:35:46 +0000 Subject: [PATCH 10/12] Improve changelog helper readability Co-authored-by: joshblack <3901764+joshblack@users.noreply.github.com> --- script/changeset-changelog.cjs | 90 +++++++++++++++++----------------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/script/changeset-changelog.cjs b/script/changeset-changelog.cjs index 07d634336df..11d91ffdaad 100644 --- a/script/changeset-changelog.cjs +++ b/script/changeset-changelog.cjs @@ -1,12 +1,6 @@ const validRepoNameRegex = /^[\w.-]+\/[\w.-]+$/ const MAX_RETRY_ATTEMPTS = 3 - -function readEnv() { - return { - GITHUB_SERVER_URL: (process.env.GITHUB_SERVER_URL || 'https://github.com').replace(/\/+$/, ''), - GITHUB_TOKEN: process.env.GITHUB_TOKEN, - } -} +const GITHUB_SERVER_URL = (process.env.GITHUB_SERVER_URL || 'https://github.com').replace(/\/+$/, '') function makeQuery(repos) { return ` @@ -14,7 +8,7 @@ function makeQuery(repos) { ${Object.keys(repos) .map((repo, index) => { const [owner, name] = repo.split('/') - return `a${index}: repository( + return `repo${index}: repository( owner: ${JSON.stringify(owner)} name: ${JSON.stringify(name)} ) { @@ -107,8 +101,33 @@ function getErrorSummary(error) { return error?.code || error?.name || 'unknown error' } +function summarizeGitHubErrors(errors) { + return errors + .slice(0, 3) + .map(error => error.type || error.message || 'unknown error') + .join('; ') +} + +function sortPullRequestsByMergeDate(a, b) { + if (a.mergedAt === null && b.mergedAt === null) { + return 0 + } + + if (a.mergedAt === null) { + return 1 + } + + if (b.mergedAt === null) { + return -1 + } + + const dateA = new Date(a.mergedAt) + const dateB = new Date(b.mergedAt) + return dateA.getTime() - dateB.getTime() +} + async function fetchGitHubData(query) { - const {GITHUB_TOKEN} = readEnv() + const GITHUB_TOKEN = process.env.GITHUB_TOKEN if (!GITHUB_TOKEN) { throw new Error('GITHUB_TOKEN is required to fetch changelog data from GitHub') } @@ -148,7 +167,9 @@ async function fetchGitHubData(query) { const data = await response.json() if (data.errors) { - throw new Error(`Fetched data from GitHub returned ${data.errors.length} error(s)`) + throw new Error( + `Fetched data from GitHub returned ${data.errors.length} error(s): ${summarizeGitHubErrors(data.errors)}`, + ) } if (!data.data) { @@ -174,7 +195,7 @@ async function fetchGitHubData(query) { } async function loadGitHubInfo(request) { - const {GITHUB_SERVER_URL, GITHUB_TOKEN} = readEnv() + const GITHUB_TOKEN = process.env.GITHUB_TOKEN if (!GITHUB_TOKEN) { throw new Error( @@ -198,7 +219,7 @@ async function loadGitHubInfo(request) { ], } const data = await fetchGitHubData(makeQuery(repos)) - return data.data.a0[request.kind === 'commit' ? `a${request.commit}` : `pr__${request.pull}`] + return data.data.repo0[request.kind === 'commit' ? `a${request.commit}` : `pr__${request.pull}`] } async function getInfo(request) { @@ -214,23 +235,7 @@ async function getInfo(request) { let user = data.author?.user || null const associatedPullRequest = data.associatedPullRequests?.nodes?.length > 0 - ? data.associatedPullRequests.nodes.sort((a, b) => { - if (a.mergedAt === null && b.mergedAt === null) { - return 0 - } - - if (a.mergedAt === null) { - return 1 - } - - if (b.mergedAt === null) { - return -1 - } - - const dateA = new Date(a.mergedAt) - const dateB = new Date(b.mergedAt) - return dateA.getTime() - dateB.getTime() - })[0] + ? data.associatedPullRequests.nodes.sort(sortPullRequestsByMergeDate)[0] : null if (associatedPullRequest) { @@ -253,7 +258,6 @@ async function getInfoFromPullRequest(request) { throw new Error('Please pass a pull request number') } - const {GITHUB_SERVER_URL} = readEnv() const data = await loadGitHubInfo({ kind: 'pull', repo: request.repo, @@ -285,21 +289,18 @@ const changelogFunctions = { return '' } - const changesetLink = `- Updated dependencies [${( - await Promise.all( - changesets.map(async changeset => { - if (changeset.commit) { - const {links} = await getInfo({ - repo: options.repo, - commit: changeset.commit, - }) - return links.commit - } - }), - ) + const dependencyCommitLinks = await Promise.all( + changesets.map(async changeset => { + if (changeset.commit) { + const {links} = await getInfo({ + repo: options.repo, + commit: changeset.commit, + }) + return links.commit + } + }), ) - .filter(Boolean) - .join(', ')}]:` + const changesetLink = `- Updated dependencies [${dependencyCommitLinks.filter(Boolean).join(', ')}]:` const updatedDependenciesList = dependenciesUpdated.map( dependency => ` - ${dependency.name}@${dependency.newVersion}`, ) @@ -312,7 +313,6 @@ const changelogFunctions = { ) } - const {GITHUB_SERVER_URL} = readEnv() let prFromSummary let commitFromSummary const usersFromSummary = [] From 792e4b704fdc4323e27ffd1d30835f1bc2e2279a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Jun 2026 21:38:28 +0000 Subject: [PATCH 11/12] Clarify changelog helper constants Co-authored-by: joshblack <3901764+joshblack@users.noreply.github.com> --- script/changeset-changelog.cjs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/script/changeset-changelog.cjs b/script/changeset-changelog.cjs index 11d91ffdaad..5a8faf016c4 100644 --- a/script/changeset-changelog.cjs +++ b/script/changeset-changelog.cjs @@ -1,5 +1,7 @@ const validRepoNameRegex = /^[\w.-]+\/[\w.-]+$/ const MAX_RETRY_ATTEMPTS = 3 +const COMMIT_ABBREVIATION_LENGTH = 7 +const MAX_GITHUB_ERRORS_TO_DISPLAY = 3 const GITHUB_SERVER_URL = (process.env.GITHUB_SERVER_URL || 'https://github.com').replace(/\/+$/, '') function makeQuery(repos) { @@ -89,7 +91,7 @@ function getCommitLink(commit, url, options = {}) { throw new Error('Expected a commit SHA when generating changelog links') } - const label = options.alreadyAbbreviated ? commit : commit.slice(0, 7) + const label = options.alreadyAbbreviated ? commit : commit.slice(0, COMMIT_ABBREVIATION_LENGTH) return `[\`${label}\`](${url})` } @@ -103,7 +105,7 @@ function getErrorSummary(error) { function summarizeGitHubErrors(errors) { return errors - .slice(0, 3) + .slice(0, MAX_GITHUB_ERRORS_TO_DISPLAY) .map(error => error.type || error.message || 'unknown error') .join('; ') } @@ -235,7 +237,7 @@ async function getInfo(request) { let user = data.author?.user || null const associatedPullRequest = data.associatedPullRequests?.nodes?.length > 0 - ? data.associatedPullRequests.nodes.sort(sortPullRequestsByMergeDate)[0] + ? [...data.associatedPullRequests.nodes].sort(sortPullRequestsByMergeDate)[0] : null if (associatedPullRequest) { @@ -333,7 +335,9 @@ const changelogFunctions = { return '' }) .trim() - const [firstLine, ...remainingLines] = replacedChangelog.split('\n').map(line => line.trimEnd()) + const [rawFirstLine, ...rawRemainingLines] = replacedChangelog.split('\n') + const firstLine = rawFirstLine.trimEnd() + const remainingLines = rawRemainingLines.map(line => line.trimEnd()) const links = await (async () => { if (prFromSummary !== undefined) { let {links} = await getInfoFromPullRequest({ From cbd0d907bbf595d40d3e7367e86e042b35d7d253 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Jun 2026 21:51:46 +0000 Subject: [PATCH 12/12] Use built-in Changesets changelog Co-authored-by: joshblack <3901764+joshblack@users.noreply.github.com> --- .changeset/config.json | 2 +- package-lock.json | 77 ------- package.json | 1 - script/changeset-changelog.cjs | 391 --------------------------------- 4 files changed, 1 insertion(+), 470 deletions(-) delete mode 100644 script/changeset-changelog.cjs diff --git a/.changeset/config.json b/.changeset/config.json index d76dda5d6b3..c8615396de0 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -1,6 +1,6 @@ { "$schema": "https://unpkg.com/@changesets/config@1.4.0/schema.json", - "changelog": ["../script/changeset-changelog.cjs", {"repo": "primer/react"}], + "changelog": "@changesets/cli/changelog", "commit": false, "linked": [], "access": "public", diff --git a/package-lock.json b/package-lock.json index 13a61236ac9..caf3b437546 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,6 @@ "devDependencies": { "@babel/core": "^7.29.0", "@babel/eslint-parser": "7.28.6", - "@changesets/changelog-github": "0.5.1", "@changesets/cli": "^2.29.6", "@eslint-react/eslint-plugin": "^1.52.6", "@eslint/compat": "^2.0.2", @@ -2320,16 +2319,6 @@ "@changesets/types": "^6.1.0" } }, - "node_modules/@changesets/changelog-github": { - "version": "0.5.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@changesets/get-github-info": "^0.6.0", - "@changesets/types": "^6.1.0", - "dotenv": "^8.1.0" - } - }, "node_modules/@changesets/cli": { "version": "2.29.6", "resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-2.29.6.tgz", @@ -2403,15 +2392,6 @@ "semver": "^7.5.3" } }, - "node_modules/@changesets/get-github-info": { - "version": "0.6.0", - "dev": true, - "license": "MIT", - "dependencies": { - "dataloader": "^1.4.0", - "node-fetch": "^2.5.0" - } - }, "node_modules/@changesets/get-release-plan": { "version": "4.0.13", "resolved": "https://registry.npmjs.org/@changesets/get-release-plan/-/get-release-plan-4.0.13.tgz", @@ -14190,11 +14170,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/dataloader": { - "version": "1.4.0", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -14511,14 +14486,6 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "node_modules/dotenv": { - "version": "8.6.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=10" - } - }, "node_modules/dunder-proto": { "version": "1.0.1", "license": "MIT", @@ -20838,25 +20805,6 @@ "semver": "bin/semver.js" } }, - "node_modules/node-fetch": { - "version": "2.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -26860,13 +26808,6 @@ "node": ">= 4.0.0" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true, - "license": "MIT" - }, "node_modules/tree-kill": { "version": "1.2.2", "dev": true, @@ -29064,24 +29005,6 @@ "node": ">=18" } }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/whatwg-url/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true, - "license": "BSD-2-Clause" - }, "node_modules/which": { "version": "1.3.1", "dev": true, diff --git a/package.json b/package.json index a05901677fb..a182b180178 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,6 @@ "devDependencies": { "@babel/core": "^7.29.0", "@babel/eslint-parser": "7.28.6", - "@changesets/changelog-github": "0.5.1", "@changesets/cli": "^2.29.6", "@eslint-react/eslint-plugin": "^1.52.6", "@eslint/compat": "^2.0.2", diff --git a/script/changeset-changelog.cjs b/script/changeset-changelog.cjs deleted file mode 100644 index 5a8faf016c4..00000000000 --- a/script/changeset-changelog.cjs +++ /dev/null @@ -1,391 +0,0 @@ -const validRepoNameRegex = /^[\w.-]+\/[\w.-]+$/ -const MAX_RETRY_ATTEMPTS = 3 -const COMMIT_ABBREVIATION_LENGTH = 7 -const MAX_GITHUB_ERRORS_TO_DISPLAY = 3 -const GITHUB_SERVER_URL = (process.env.GITHUB_SERVER_URL || 'https://github.com').replace(/\/+$/, '') - -function makeQuery(repos) { - return ` - query { - ${Object.keys(repos) - .map((repo, index) => { - const [owner, name] = repo.split('/') - return `repo${index}: repository( - owner: ${JSON.stringify(owner)} - name: ${JSON.stringify(name)} - ) { - ${repos[repo] - .map(data => - data.kind === 'commit' - ? `a${data.commit}: object(expression: ${JSON.stringify(data.commit)}) { - ... on Commit { - commitUrl - associatedPullRequests(first: 50) { - nodes { - number - url - mergedAt - author { - login - url - } - } - } - author { - user { - login - url - } - } - }}` - : `pr__${data.pull}: pullRequest(number: ${data.pull}) { - url - author { - login - url - } - mergeCommit { - commitUrl - abbreviatedOid - } - }`, - ) - .join('\n')} - }` - }) - .join('\n')} - } - ` -} - -function getFetchImplementation() { - if (typeof globalThis.fetch === 'function') { - return { - fetch: globalThis.fetch, - useNodeFetchOptions: false, - } - } - - return { - fetch: require('node-fetch'), - useNodeFetchOptions: true, - } -} - -function isRetryableStatus(status) { - return status === 429 || status >= 500 -} - -function isRetryableError(error) { - // Node.js v26 can surface GitHub gzip stream failures as premature close errors. - return ( - error?.retryable === true || - error?.code === 'ERR_STREAM_PREMATURE_CLOSE' || - error?.cause?.code === 'ERR_STREAM_PREMATURE_CLOSE' || - /premature close|terminated|socket hang up|network timeout/i.test(error?.message || '') - ) -} - -function getCommitLink(commit, url, options = {}) { - if (typeof commit !== 'string' || commit.length === 0) { - throw new Error('Expected a commit SHA when generating changelog links') - } - - const label = options.alreadyAbbreviated ? commit : commit.slice(0, COMMIT_ABBREVIATION_LENGTH) - return `[\`${label}\`](${url})` -} - -function getErrorSummary(error) { - if (error?.status) { - return `status ${error.status}` - } - - return error?.code || error?.name || 'unknown error' -} - -function summarizeGitHubErrors(errors) { - return errors - .slice(0, MAX_GITHUB_ERRORS_TO_DISPLAY) - .map(error => error.type || error.message || 'unknown error') - .join('; ') -} - -function sortPullRequestsByMergeDate(a, b) { - if (a.mergedAt === null && b.mergedAt === null) { - return 0 - } - - if (a.mergedAt === null) { - return 1 - } - - if (b.mergedAt === null) { - return -1 - } - - const dateA = new Date(a.mergedAt) - const dateB = new Date(b.mergedAt) - return dateA.getTime() - dateB.getTime() -} - -async function fetchGitHubData(query) { - const GITHUB_TOKEN = process.env.GITHUB_TOKEN - if (!GITHUB_TOKEN) { - throw new Error('GITHUB_TOKEN is required to fetch changelog data from GitHub') - } - - const {fetch, useNodeFetchOptions} = getFetchImplementation() - const headers = { - Authorization: 'Bearer ' + GITHUB_TOKEN, - // Avoid node-fetch gzip handling failures in the release workflow on Node.js v26. - 'Accept-Encoding': 'identity', - 'Content-Type': 'application/json', - } - const body = JSON.stringify({query}) - let lastError - - for (let attempt = 0; attempt < MAX_RETRY_ATTEMPTS; attempt++) { - try { - const requestOptions = { - method: 'POST', - headers, - body, - } - - if (useNodeFetchOptions) { - // Complement the identity encoding request for node-fetch v2, which is used on older Node.js versions. - requestOptions.compress = false - } - - const response = await fetch('https://api.github.com/graphql', requestOptions) - - if (!response.ok && isRetryableStatus(response.status)) { - const error = new Error(`GitHub GraphQL request failed with status ${response.status}`) - error.status = response.status - error.retryable = true - throw error - } - - const data = await response.json() - - if (data.errors) { - throw new Error( - `Fetched data from GitHub returned ${data.errors.length} error(s): ${summarizeGitHubErrors(data.errors)}`, - ) - } - - if (!data.data) { - throw new Error('Fetched data from GitHub has missing data') - } - - return data - } catch (error) { - lastError = error - if (!isRetryableError(error)) { - break - } - } - } - - const error = new Error( - `An error occurred when fetching data from GitHub after ${MAX_RETRY_ATTEMPTS} attempts: ${getErrorSummary( - lastError, - )}`, - ) - error.cause = lastError - throw error -} - -async function loadGitHubInfo(request) { - const GITHUB_TOKEN = process.env.GITHUB_TOKEN - - if (!GITHUB_TOKEN) { - throw new Error( - `Please create a GitHub personal access token at ${GITHUB_SERVER_URL}/settings/tokens/new with \`read:user\` and \`repo:status\` permissions and add it as the GITHUB_TOKEN environment variable`, - ) - } - - if (!request.repo) { - throw new Error('Please pass a GitHub repository in the form of userOrOrg/repoName to getInfo') - } - - if (!validRepoNameRegex.test(request.repo)) { - throw new Error( - `Please pass a valid GitHub repository in the form of userOrOrg/repoName to getInfo (it has to match the "${validRepoNameRegex.source}" pattern)`, - ) - } - - const repos = { - [request.repo]: [ - request.kind === 'commit' ? {kind: 'commit', commit: request.commit} : {kind: 'pull', pull: request.pull}, - ], - } - const data = await fetchGitHubData(makeQuery(repos)) - return data.data.repo0[request.kind === 'commit' ? `a${request.commit}` : `pr__${request.pull}`] -} - -async function getInfo(request) { - if (!request.commit) { - throw new Error('Please pass a commit SHA to getInfo') - } - - const data = await loadGitHubInfo({ - kind: 'commit', - repo: request.repo, - commit: request.commit, - }) - let user = data.author?.user || null - const associatedPullRequest = - data.associatedPullRequests?.nodes?.length > 0 - ? [...data.associatedPullRequests.nodes].sort(sortPullRequestsByMergeDate)[0] - : null - - if (associatedPullRequest) { - user = associatedPullRequest.author - } - - return { - user: user ? user.login : null, - pull: associatedPullRequest ? associatedPullRequest.number : null, - links: { - commit: getCommitLink(request.commit, data.commitUrl), - pull: associatedPullRequest ? `[#${associatedPullRequest.number}](${associatedPullRequest.url})` : null, - user: user ? `[@${user.login}](${user.url})` : null, - }, - } -} - -async function getInfoFromPullRequest(request) { - if (request.pull === undefined) { - throw new Error('Please pass a pull request number') - } - - const data = await loadGitHubInfo({ - kind: 'pull', - repo: request.repo, - pull: request.pull, - }) - const user = data?.author - const commit = data?.mergeCommit - - return { - user: user ? user.login : null, - commit: commit ? commit.abbreviatedOid : null, - links: { - commit: commit ? getCommitLink(commit.abbreviatedOid, commit.commitUrl, {alreadyAbbreviated: true}) : null, - pull: `[#${request.pull}](${GITHUB_SERVER_URL}/${request.repo}/pull/${request.pull})`, - user: user ? `[@${user.login}](${user.url})` : null, - }, - } -} - -const changelogFunctions = { - getDependencyReleaseLine: async (changesets, dependenciesUpdated, options) => { - if (!options.repo) { - throw new Error( - 'Please provide a repo to this changelog generator like this:\n"changelog": ["../script/changeset-changelog.cjs", { "repo": "org/repo" }]', - ) - } - - if (dependenciesUpdated.length === 0) { - return '' - } - - const dependencyCommitLinks = await Promise.all( - changesets.map(async changeset => { - if (changeset.commit) { - const {links} = await getInfo({ - repo: options.repo, - commit: changeset.commit, - }) - return links.commit - } - }), - ) - const changesetLink = `- Updated dependencies [${dependencyCommitLinks.filter(Boolean).join(', ')}]:` - const updatedDependenciesList = dependenciesUpdated.map( - dependency => ` - ${dependency.name}@${dependency.newVersion}`, - ) - return [changesetLink, ...updatedDependenciesList].join('\n') - }, - getReleaseLine: async (changeset, _type, options) => { - if (!options?.repo) { - throw new Error( - 'Please provide a repo to this changelog generator like this:\n"changelog": ["../script/changeset-changelog.cjs", { "repo": "org/repo" }]', - ) - } - - let prFromSummary - let commitFromSummary - const usersFromSummary = [] - const replacedChangelog = changeset.summary - .replace(/^\s*(?:pr|pull|pull\s+request):\s*#?(\d+)/im, (_, pr) => { - const num = Number(pr) - if (!Number.isNaN(num)) { - prFromSummary = num - } - return '' - }) - .replace(/^\s*commit:\s*([^\s]+)/im, (_, commit) => { - commitFromSummary = commit - return '' - }) - .replace(/^\s*(?:author|user):\s*@?([^\s]+)/gim, (_, user) => { - usersFromSummary.push(user) - return '' - }) - .trim() - const [rawFirstLine, ...rawRemainingLines] = replacedChangelog.split('\n') - const firstLine = rawFirstLine.trimEnd() - const remainingLines = rawRemainingLines.map(line => line.trimEnd()) - const links = await (async () => { - if (prFromSummary !== undefined) { - let {links} = await getInfoFromPullRequest({ - repo: options.repo, - pull: prFromSummary, - }) - - if (commitFromSummary) { - links = { - ...links, - commit: getCommitLink( - commitFromSummary, - `${GITHUB_SERVER_URL}/${options.repo}/commit/${commitFromSummary}`, - ), - } - } - - return links - } - - const commitToFetchFrom = commitFromSummary || changeset.commit - - if (commitToFetchFrom) { - const {links} = await getInfo({ - repo: options.repo, - commit: commitToFetchFrom, - }) - return links - } - - return { - commit: null, - pull: null, - user: null, - } - })() - const users = usersFromSummary.length - ? usersFromSummary - .map(userFromSummary => `[@${userFromSummary}](https://github.com/${userFromSummary})`) - .join(', ') - : links.user - const prefix = [ - links.pull === null ? '' : ` ${links.pull}`, - links.commit === null ? '' : ` ${links.commit}`, - users === null ? '' : ` Thanks ${users}!`, - ].join('') - return `\n\n-${prefix ? `${prefix} -` : ''} ${firstLine}\n${remainingLines.map(line => ` ${line}`).join('\n')}` - }, -} - -module.exports = changelogFunctions