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

@@ -63,6 +63,29 @@ export class ReleaseResolver {
return this.releases.progress(viewerOf(me), release.id);
}
@ResolveField(() => String, {
description:
'A Markdown draft of the notes from its finished tickets, by type (not saved)',
})
notesDraft(
@CurrentUser() me: User | undefined,
@Parent() release: ReleaseReadModel,
) {
return this.releases.notesDraft(viewerOf(me), release.id);
}
@Mutation(() => CommandResultGql, {
description:
'Write the release notes (Markdown; null removes them). Also after shipping.',
})
writeReleaseNotes(
@CurrentUser() me: User,
@Args('releaseId', { type: () => ID }) releaseId: string,
@Args('notes', { type: () => String, nullable: true }) notes: string | null,
) {
return this.releases.writeNotes(me.id, releaseId, notes ?? null);
}
@ResolveField(() => ReleaseBurnupGql, {
description: 'Scope vs finished tickets, day by day',
})

View File

@@ -285,6 +285,11 @@ export class ReleaseGql {
@Field(() => String, { nullable: true }) targetDate!: string | null;
@Field(() => ReleaseStatus) status!: ReleaseStatus;
@Field(() => String, { nullable: true }) releasedAt!: string | null;
@Field(() => String, {
nullable: true,
description: 'What shipped, told for people (Markdown)',
})
notes!: string | null;
@Field() createdAt!: string;
@Field(() => Int) version!: number;
}