From e15bdf9af3c1c94c5c7c2ca5d9e072d9979232d0 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Fri, 30 Oct 2026 12:09:14 -0400 Subject: [PATCH] fix(ci): release notes script (#32827) (cherry picked from commit f47b02f91d6d55e0b0281609c37e6b1676626115) --- .github/workflows/draft-release.yml | 6 +++--- .gitignore | 1 + packages/scripts/src/cli.ts | 4 +++- packages/scripts/src/commands/notes.spec.ts | 22 +++++++++++++++++++-- packages/scripts/src/commands/notes.ts | 13 ++++++++++-- 5 files changed, 38 insertions(+), 8 deletions(-) diff --git a/.github/workflows/draft-release.yml b/.github/workflows/draft-release.yml index 574b88646..fc6c0b700 100644 --- a/.github/workflows/draft-release.yml +++ b/.github/workflows/draft-release.yml @@ -154,8 +154,8 @@ jobs: previous=$(grep -A1 -xF "${TAG}" <<< "${tags}" | tail -1) echo "generating notes from ${previous}" - mise //:notes --tag "${TAG}" --previous "${previous}" > "${RUNNER_TEMP}/notes.md" - cat "${RUNNER_TEMP}/notes.md" + mise //:notes --tag "${TAG}" --previous "${previous}" + cat misc/release/notes.md - name: Create draft release env: @@ -171,7 +171,7 @@ jobs: --draft \ --verify-tag \ --prerelease="${PRERELEASE}" \ - --notes-file "${RUNNER_TEMP}/notes.md" \ + --notes-file misc/release/notes.md \ docker/docker-compose.yml \ docker/docker-compose.rootless.yml \ docker/example.env \ diff --git a/.gitignore b/.gitignore index e1a1554b7..b78a5a1b7 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ vite.config.js.timestamp-* .devcontainer/.env* *.tsbuildinfo *.tsbuildInfo +misc/release/notes.md diff --git a/packages/scripts/src/cli.ts b/packages/scripts/src/cli.ts index b8e71d7f9..b878a868f 100644 --- a/packages/scripts/src/cli.ts +++ b/packages/scripts/src/cli.ts @@ -1,5 +1,6 @@ import { Command, Option } from 'commander'; import { + FileSystem, GitClient, GitHubClient, type NotesOptions, @@ -63,10 +64,11 @@ export const cli = (argv: string[]) => { .action(async ({ token, ...options }: NotesOptions & { token: string }) => { try { const service = new NotesService( + new FileSystem(), new GitClient(), new GitHubClient(token), ); - process.stdout.write(await service.handleNotes(options)); + await service.handleNotes(options); } catch (error) { console.error(error instanceof Error ? error.message : error); process.exit(1); diff --git a/packages/scripts/src/commands/notes.spec.ts b/packages/scripts/src/commands/notes.spec.ts index fdc9b2c1d..93a32f7db 100644 --- a/packages/scripts/src/commands/notes.spec.ts +++ b/packages/scripts/src/commands/notes.spec.ts @@ -2,6 +2,7 @@ import type { Public } from 'src/types.js'; import { beforeEach, describe, expect, it, type Mocked, vi } from 'vitest'; import { type Contributor, + type FileSystem, type GitClient, type GitHubClient, NotesService, @@ -37,6 +38,7 @@ const changelog = (...lines: string[]) => const body = (notes: string) => notes.slice(notes.indexOf("## What's Changed")); describe(NotesService.name, () => { + let fs: Mocked>; let git: Mocked>; let github: Mocked>; let sut: NotesService; @@ -88,12 +90,16 @@ describe(NotesService.name, () => { github.getFirstTimers.mockResolvedValue(firstTimers); }; - const run = () => sut.handleNotes({ tag: 'v3.3.0', previous: 'v3.2.4' }); + const run = async () => { + await sut.handleNotes({ tag: 'v3.3.0', previous: 'v3.2.4' }); + return fs.writeFileSync.mock.lastCall?.[1] as string; + }; beforeEach(() => { + fs = { writeFileSync: vi.fn() }; git = { log: vi.fn() }; github = { getPullRequests: vi.fn(), getFirstTimers: vi.fn() }; - sut = new NotesService(git, github); + sut = new NotesService(fs, git, github); }); it('should compare the previous tag with the new tag', async () => { @@ -104,6 +110,18 @@ describe(NotesService.name, () => { expect(git.log).toHaveBeenCalledWith('v3.2.4', 'v3.3.0'); }); + it('should write the notes to misc/release/notes.md', async () => { + setup([{ number: 1 }]); + + await run(); + + expect(fs.writeFileSync).toHaveBeenCalledOnce(); + expect(fs.writeFileSync).toHaveBeenCalledWith( + expect.stringMatching(/\/misc\/release\/notes\.md$/), + expect.any(String), + ); + }); + it('should start with the template and end with the full changelog', async () => { setup([{ number: 1 }]); diff --git a/packages/scripts/src/commands/notes.ts b/packages/scripts/src/commands/notes.ts index dfc7e6fd7..4b6b362ca 100644 --- a/packages/scripts/src/commands/notes.ts +++ b/packages/scripts/src/commands/notes.ts @@ -1,5 +1,5 @@ import { execFileSync } from 'node:child_process'; -import { existsSync, readFileSync } from 'node:fs'; +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import type { Public } from 'src/types.js'; @@ -15,6 +15,7 @@ const REPO = 'immich-app/immich'; const Files = { Config: join(root, '.github/release.yml'), Template: join(root, 'misc/release/notes.tmpl'), + Output: join(root, 'misc/release/notes.md'), }; export type NotesOptions = { @@ -55,6 +56,12 @@ type PullRequestNode = { const PULL_REQUEST_BATCH = 50; const SEARCH_BATCH = 20; +export class FileSystem { + writeFileSync(path: string, data: string) { + writeFileSync(path, data); + } +} + export class GitClient { log(previous: string, tag: string) { return execFileSync( @@ -189,6 +196,7 @@ export class GitHubClient { export class NotesService { constructor( + private fs: Public, private git: Public, private github: Public, ) {} @@ -198,8 +206,9 @@ export class NotesService { const pulls = await this.getPullRequests(commits); const changes = this.selectChanges(commits, pulls); const firstTimers = await this.getFirstTimers(changes); + const notes = this.render({ tag, previous, changes, firstTimers }); - return this.render({ tag, previous, changes, firstTimers }); + this.fs.writeFileSync(Files.Output, notes); } private parseCommits(log: string): Commit[] {