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:
@@ -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',
|
||||
})
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -857,6 +857,19 @@ export class CreateReleaseDto {
|
||||
|
||||
export class UpdateReleaseDto extends PartialType(CreateReleaseDto) {}
|
||||
|
||||
export class ReleaseNotesDto {
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
nullable: true,
|
||||
maxLength: 20000,
|
||||
description: 'Markdown; null removes the notes',
|
||||
})
|
||||
@ValidateIf((dto: ReleaseNotesDto) => dto.notes !== null)
|
||||
@IsString()
|
||||
@MaxLength(20000)
|
||||
notes!: string | null;
|
||||
}
|
||||
|
||||
const DAY = /^\d{4}-\d{2}-\d{2}$/;
|
||||
|
||||
export class CreateSprintDto {
|
||||
|
||||
@@ -331,6 +331,12 @@ export class ReleaseDto {
|
||||
status!: ReleaseStatus;
|
||||
@ApiProperty({ type: String, nullable: true, format: 'date-time' })
|
||||
releasedAt!: string | null;
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
nullable: true,
|
||||
description: 'What shipped, told for people (Markdown)',
|
||||
})
|
||||
notes!: string | null;
|
||||
@ApiProperty({ format: 'date-time' }) createdAt!: string;
|
||||
@ApiProperty() version!: number;
|
||||
}
|
||||
@@ -370,6 +376,13 @@ export class EpicDto {
|
||||
@ApiProperty({ type: ChildProgressDto }) progress!: ChildProgressDto;
|
||||
}
|
||||
|
||||
export class ReleaseNotesDraftDto {
|
||||
@ApiProperty({
|
||||
example: '## v1.4.0\n\n### Novedades\n\n- Pago con tarjeta (WEB-1)\n',
|
||||
})
|
||||
markdown!: string;
|
||||
}
|
||||
|
||||
export class ReleaseProgressDto {
|
||||
@ApiProperty() total!: number;
|
||||
@ApiProperty() finished!: number;
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
@@ -34,6 +35,7 @@ import {
|
||||
CreateReleaseDto,
|
||||
PageQueryDto,
|
||||
PublishReleaseDto,
|
||||
ReleaseNotesDto,
|
||||
UpcomingReleasesQueryDto,
|
||||
UpdateReleaseDto,
|
||||
} from './dto/requests.dto';
|
||||
@@ -41,6 +43,7 @@ import {
|
||||
CommandResultDto,
|
||||
IssuePageDto,
|
||||
ReleaseDto,
|
||||
ReleaseNotesDraftDto,
|
||||
ReleaseBurnupDto,
|
||||
ReleaseProgressDto,
|
||||
} from './dto/responses.dto';
|
||||
@@ -194,6 +197,38 @@ export class ReleasesController {
|
||||
return this.releases.publish(me.id, releaseId, body.force);
|
||||
}
|
||||
|
||||
@Put('releases/:releaseId/notes')
|
||||
@ApiOperation({
|
||||
summary: 'Write the release notes (Markdown)',
|
||||
description: 'Also after the release ships; not once archived.',
|
||||
})
|
||||
@ApiOkResponse({ type: CommandResultDto })
|
||||
@ApiCommandErrors()
|
||||
writeNotes(
|
||||
@CurrentUser() me: User,
|
||||
@Param('releaseId') releaseId: string,
|
||||
@Body() body: ReleaseNotesDto,
|
||||
) {
|
||||
return this.releases.writeNotes(me.id, releaseId, body.notes);
|
||||
}
|
||||
|
||||
@Get('releases/:releaseId/notes/draft')
|
||||
@ApiOptionalPireonAuth()
|
||||
@ApiOperation({
|
||||
summary: 'A Markdown draft of the notes from the finished tickets',
|
||||
description: 'Grouped by type; nothing is saved.',
|
||||
})
|
||||
@ApiOkResponse({ type: ReleaseNotesDraftDto })
|
||||
@ApiReadErrors()
|
||||
async notesDraft(
|
||||
@CurrentUser() me: User | undefined,
|
||||
@Param('releaseId') releaseId: string,
|
||||
) {
|
||||
return {
|
||||
markdown: await this.releases.notesDraft(viewerOf(me), releaseId),
|
||||
};
|
||||
}
|
||||
|
||||
@Post('releases/:releaseId/archive')
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ summary: 'Archive a planned release (its tickets leave it)' })
|
||||
|
||||
Reference in New Issue
Block a user