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

@@ -23,6 +23,8 @@ import {
IssueCommentsDocument,
PublishReleaseDocument,
ReleaseBurnupDocument,
ReleaseNotesDraftDocument,
WriteReleaseNotesDocument,
ReleasesDocument,
UpcomingReleasesDocument,
SearchUsersDocument,
@@ -98,6 +100,21 @@ export class GraphqlReleasesGateway extends ReleasesGateway {
.query(ReleaseBurnupDocument, { id: releaseId })
.pipe(map((d) => d.release.burnup));
}
notesDraft(releaseId: string): Observable<string> {
return this.gql
.query(ReleaseNotesDraftDocument, { id: releaseId })
.pipe(map((d) => d.release.notesDraft));
}
writeNotes(
releaseId: string,
notes: string | null,
): Observable<CommandResult> {
return this.gql
.mutate(WriteReleaseNotesDocument, { releaseId, notes })
.pipe(map((d) => toCommandResult(d.writeReleaseNotes)));
}
}
@Injectable()

View File

@@ -140,6 +140,7 @@ fragment ReleaseFields on Release {
targetDate
status
releasedAt
notes
}
fragment ActivityFields on Activity {

View File

@@ -368,6 +368,7 @@ export type ReleaseFieldsFragment = {
targetDate: string | null;
status: ReleaseStatus;
releasedAt: string | null;
notes: string | null;
};
export type ActivityFieldsFragment = {
@@ -1148,6 +1149,7 @@ export type ReleasesQuery = {
targetDate: string | null;
status: ReleaseStatus;
releasedAt: string | null;
notes: string | null;
progress: { total: number; finished: number; percent: number };
}>;
};
@@ -1208,6 +1210,7 @@ export type UpcomingReleasesQuery = {
targetDate: string | null;
status: ReleaseStatus;
releasedAt: string | null;
notes: string | null;
progress: { total: number; finished: number; percent: number };
}>;
};
@@ -1229,6 +1232,23 @@ export type ReleaseBurnupQuery = {
};
};
export type ReleaseNotesDraftQueryVariables = Exact<{
id: string;
}>;
export type ReleaseNotesDraftQuery = {
release: { id: string; notesDraft: string };
};
export type WriteReleaseNotesMutationVariables = Exact<{
releaseId: string;
notes?: string | null | undefined;
}>;
export type WriteReleaseNotesMutation = {
writeReleaseNotes: { id: string; key: string | null; version: number };
};
export type SavedFilterFieldsFragment = {
id: string;
projectId: string;
@@ -1489,6 +1509,7 @@ export type ReleaseChangedSubscription = {
targetDate: string | null;
status: ReleaseStatus;
releasedAt: string | null;
notes: string | null;
};
};
};
@@ -2119,6 +2140,7 @@ export const ReleaseFieldsFragmentDoc = {
{ kind: 'Field', name: { kind: 'Name', value: 'targetDate' } },
{ kind: 'Field', name: { kind: 'Name', value: 'status' } },
{ kind: 'Field', name: { kind: 'Name', value: 'releasedAt' } },
{ kind: 'Field', name: { kind: 'Name', value: 'notes' } },
],
},
},
@@ -7298,6 +7320,7 @@ export const ReleasesDocument = {
{ kind: 'Field', name: { kind: 'Name', value: 'targetDate' } },
{ kind: 'Field', name: { kind: 'Name', value: 'status' } },
{ kind: 'Field', name: { kind: 'Name', value: 'releasedAt' } },
{ kind: 'Field', name: { kind: 'Name', value: 'notes' } },
],
},
},
@@ -7741,6 +7764,7 @@ export const UpcomingReleasesDocument = {
{ kind: 'Field', name: { kind: 'Name', value: 'targetDate' } },
{ kind: 'Field', name: { kind: 'Name', value: 'status' } },
{ kind: 'Field', name: { kind: 'Name', value: 'releasedAt' } },
{ kind: 'Field', name: { kind: 'Name', value: 'notes' } },
],
},
},
@@ -7814,6 +7838,141 @@ export const ReleaseBurnupDocument = {
},
],
} as unknown as DocumentNode<ReleaseBurnupQuery, ReleaseBurnupQueryVariables>;
export const ReleaseNotesDraftDocument = {
kind: 'Document',
definitions: [
{
kind: 'OperationDefinition',
operation: 'query',
name: { kind: 'Name', value: 'ReleaseNotesDraft' },
variableDefinitions: [
{
kind: 'VariableDefinition',
variable: { kind: 'Variable', name: { kind: 'Name', value: 'id' } },
type: {
kind: 'NonNullType',
type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } },
},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: { kind: 'Name', value: 'release' },
arguments: [
{
kind: 'Argument',
name: { kind: 'Name', value: 'id' },
value: {
kind: 'Variable',
name: { kind: 'Name', value: 'id' },
},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
{ kind: 'Field', name: { kind: 'Name', value: 'notesDraft' } },
],
},
},
],
},
},
],
} as unknown as DocumentNode<
ReleaseNotesDraftQuery,
ReleaseNotesDraftQueryVariables
>;
export const WriteReleaseNotesDocument = {
kind: 'Document',
definitions: [
{
kind: 'OperationDefinition',
operation: 'mutation',
name: { kind: 'Name', value: 'WriteReleaseNotes' },
variableDefinitions: [
{
kind: 'VariableDefinition',
variable: {
kind: 'Variable',
name: { kind: 'Name', value: 'releaseId' },
},
type: {
kind: 'NonNullType',
type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } },
},
},
{
kind: 'VariableDefinition',
variable: {
kind: 'Variable',
name: { kind: 'Name', value: 'notes' },
},
type: { kind: 'NamedType', name: { kind: 'Name', value: 'String' } },
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: { kind: 'Name', value: 'writeReleaseNotes' },
arguments: [
{
kind: 'Argument',
name: { kind: 'Name', value: 'releaseId' },
value: {
kind: 'Variable',
name: { kind: 'Name', value: 'releaseId' },
},
},
{
kind: 'Argument',
name: { kind: 'Name', value: 'notes' },
value: {
kind: 'Variable',
name: { kind: 'Name', value: 'notes' },
},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'FragmentSpread',
name: { kind: 'Name', value: 'CommandResultFields' },
},
],
},
},
],
},
},
{
kind: 'FragmentDefinition',
name: { kind: 'Name', value: 'CommandResultFields' },
typeCondition: {
kind: 'NamedType',
name: { kind: 'Name', value: 'CommandResult' },
},
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
{ kind: 'Field', name: { kind: 'Name', value: 'key' } },
{ kind: 'Field', name: { kind: 'Name', value: 'version' } },
],
},
},
],
} as unknown as DocumentNode<
WriteReleaseNotesMutation,
WriteReleaseNotesMutationVariables
>;
export const SavedFiltersDocument = {
kind: 'Document',
definitions: [
@@ -9123,6 +9282,7 @@ export const ReleaseChangedDocument = {
{ kind: 'Field', name: { kind: 'Name', value: 'targetDate' } },
{ kind: 'Field', name: { kind: 'Name', value: 'status' } },
{ kind: 'Field', name: { kind: 'Name', value: 'releasedAt' } },
{ kind: 'Field', name: { kind: 'Name', value: 'notes' } },
],
},
},

View File

@@ -59,3 +59,16 @@ query ReleaseBurnup($id: ID!) {
}
}
}
query ReleaseNotesDraft($id: ID!) {
release(id: $id) {
id
notesDraft
}
}
mutation WriteReleaseNotes($releaseId: ID!, $notes: String) {
writeReleaseNotes(releaseId: $releaseId, notes: $notes) {
...CommandResultFields
}
}