diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 379de4f086..95252572bd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,6 +39,8 @@ jobs: token: ${{ steps.app-token.outputs.token }} fetch-depth: 0 # otherwise, there would be errors pushing refs to the destination repository. path: ./cloud-sdk + - name: Fetch full commit history (blobless) + run: git fetch --filter=blob:none --unshallow || git fetch --filter=blob:none - name: Update release notes run: | npx ts-node -e "import { addCurrentChangelog } from './scripts/add-changelog'; addCurrentChangelog()" diff --git a/scripts/add-changelog.ts b/scripts/add-changelog.ts index 83d7bf68d8..30de708dc0 100644 --- a/scripts/add-changelog.ts +++ b/scripts/add-changelog.ts @@ -1,9 +1,66 @@ +import { execFile } from 'node:child_process'; +import { promisify } from 'node:util'; import { getPackageVersion } from './get-package-version'; import { readdir, readFile, writeFile } from 'node:fs/promises'; +const execFileAsync = promisify(execFile); + +const SHORT_SHA_AT_LINE_END_RE = /\(([0-9a-f]{7,12})\)$/; + +async function getRepo(): Promise { + const { stdout } = await execFileAsync('git', [ + 'remote', + 'get-url', + 'origin' + ]); + // Handles both https://github.com/owner/repo(.git) and git@github.com:owner/repo(.git) + const match = stdout.trim().match(/github\.com[:/](.+?)(?:\.git)?$/); + if (!match) + throw new Error(`Cannot parse repo from remote URL: ${stdout.trim()}`); + return match[1]; +} + +async function expandSha(shortSha: string): Promise { + try { + const { stdout } = await execFileAsync('git', ['rev-parse', shortSha]); + return stdout.trim(); + } catch { + return shortSha; + } +} + +export async function embedCommitLinks( + markdown: string, + repo: string +): Promise { + const lines = markdown.split('\n'); + return ( + await Promise.all( + lines.map(async line => { + const match = line.match(SHORT_SHA_AT_LINE_END_RE); + if (!match || !line.trimStart().startsWith('- [')) { + return line; + } + + const sha = match[1]; + const full = await expandSha(sha); + const replacement = + full === sha + ? `(${sha})` + : `([${sha}](https://github.com/${repo}/commit/${full}))`; + + return line.replace(SHORT_SHA_AT_LINE_END_RE, replacement); + }) + ) + ).join('\n'); +} + async function getChangelogWithVersion(v?: string): Promise { v = v || (await getPackageVersion()); - const changelog = await readFile('CHANGELOG.md', { encoding: 'utf8' }); + const [changelog, repo] = await Promise.all([ + readFile('CHANGELOG.md', { encoding: 'utf8' }), + getRepo() + ]); const [, olderLogs] = changelog.split(`\n# ${v}`); if (!olderLogs) { throw new Error(`Can not find version ${v} in CHANGELOG.md`); @@ -19,7 +76,7 @@ async function getChangelogWithVersion(v?: string): Promise { const headerWithVersion = `\n## ${v} [Core Modules] - ${month} ${day}, ${year}`; - return [headerWithVersion, logs].join('\n'); + return await embedCommitLinks([headerWithVersion, logs].join('\n'), repo); } async function getReleaseNotesFilePath(): Promise {