feat: release notes

Each release can carry notes in Markdown, written before or after it
ships. A draft is generated from its finished tickets grouped by type, to
edit and copy from the releases page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 15:19:23 +02:00
parent 45a3dc1e16
commit 24b37b6628
40 changed files with 1251 additions and 31 deletions

View File

@@ -30,6 +30,7 @@ import { DetachFileCommandHandler } from './detach-file.command-handler';
import { WatchIssueCommandHandler } from './watch-issue.command-handler';
import { SetIssueParentCommandHandler } from './set-issue-parent.command-handler';
import { RankIssueCommandHandler } from './rank-issue.command-handler';
import { WriteReleaseNotesCommandHandler } from './write-release-notes.command-handler';
import { SPRINT_COMMAND_HANDLERS } from './sprint.command-handlers';
import { DetachFromParentCommandHandler } from './detach-from-parent.command-handler';
import {
@@ -72,6 +73,7 @@ export const COMMAND_HANDLERS = [
WatchIssueCommandHandler,
SetIssueParentCommandHandler,
RankIssueCommandHandler,
WriteReleaseNotesCommandHandler,
...SPRINT_COMMAND_HANDLERS,
AddChecklistItemCommandHandler,
UpdateChecklistItemCommandHandler,

View File

@@ -34,3 +34,4 @@ export * from './detach-from-parent.command';
export * from './change-checklist.command';
export * from './rank-issue.command';
export * from './sprint.commands';
export * from './write-release-notes.command';

View File

@@ -0,0 +1,21 @@
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { CommandResult, ack } from './command-result';
import { WriteReleaseNotesCommand } from './write-release-notes.command';
import { WriteAccess } from './write-access';
@CommandHandler(WriteReleaseNotesCommand)
export class WriteReleaseNotesCommandHandler
implements ICommandHandler<WriteReleaseNotesCommand>
{
constructor(private readonly access: WriteAccess) {}
async execute(command: WriteReleaseNotesCommand): Promise<CommandResult> {
const release = await this.access.release(command.releaseId);
(await this.access.project(release.projectId)).assertCanManage(
command.actorId,
);
release.writeNotes(command.actorId, command.notes);
await release.commitAndWait();
return ack(release);
}
}

View File

@@ -0,0 +1,13 @@
import { Command } from '@nestjs/cqrs';
import { CommandResult } from './command-result';
/** Writes (or with null, removes) a release's notes. */
export class WriteReleaseNotesCommand extends Command<CommandResult> {
constructor(
public readonly actorId: string,
public readonly releaseId: string,
public readonly notes: string | null,
) {
super();
}
}