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:
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
defaultProjectSettings,
|
||||
IssueReadModel,
|
||||
ProjectWorkflow,
|
||||
ReleaseReadModel,
|
||||
StatusCategory,
|
||||
} from '@task-stream/api/tracker/domain';
|
||||
import { draftReleaseNotes } from './release-notes';
|
||||
|
||||
const issue = (
|
||||
number: number,
|
||||
type: string,
|
||||
title: string,
|
||||
statusCategory = StatusCategory.DONE,
|
||||
) =>
|
||||
({
|
||||
number,
|
||||
key: `WEB-${number}`,
|
||||
type,
|
||||
title,
|
||||
statusCategory,
|
||||
}) as IssueReadModel;
|
||||
|
||||
describe('release notes draft', () => {
|
||||
it('lists finished tickets by type, news first, QA left out', () => {
|
||||
const notes = draftReleaseNotes({
|
||||
release: {
|
||||
name: 'v1.4.0',
|
||||
description: 'Pagos.',
|
||||
targetDate: '2026-11-14',
|
||||
releasedAt: null,
|
||||
} as ReleaseReadModel,
|
||||
issues: [
|
||||
issue(3, 'BUG', 'El botón de pagar no responde en Safari'),
|
||||
issue(1, 'FEATURE', 'Pago con tarjeta'),
|
||||
issue(2, 'QA', 'Plan de pruebas de pagos'),
|
||||
issue(4, 'FEATURE', 'Recibo por correo', StatusCategory.IN_PROGRESS),
|
||||
],
|
||||
workflow: new ProjectWorkflow(defaultProjectSettings()),
|
||||
});
|
||||
expect(notes).toBe(
|
||||
[
|
||||
'## v1.4.0 — 14 de noviembre de 2026',
|
||||
'',
|
||||
'Pagos.',
|
||||
'',
|
||||
'### Correcciones',
|
||||
'',
|
||||
'- El botón de pagar no responde en Safari (WEB-3)',
|
||||
'',
|
||||
'### Novedades',
|
||||
'',
|
||||
'- Pago con tarjeta (WEB-1)',
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
IssueReadModel,
|
||||
ProjectWorkflow,
|
||||
ReleaseReadModel,
|
||||
StatusCategory,
|
||||
} from '@task-stream/api/tracker/domain';
|
||||
|
||||
/** How the default types are told in release notes; others use their name. */
|
||||
const HEADING: Record<string, string> = {
|
||||
FEATURE: 'Novedades',
|
||||
UI_CHANGE: 'Cambios en la interfaz',
|
||||
BUG: 'Correcciones',
|
||||
TASK: 'Mejoras internas',
|
||||
};
|
||||
/** Verification work is not news for whoever reads the notes. */
|
||||
const LEFT_OUT = new Set(['QA']);
|
||||
|
||||
/**
|
||||
* A first draft of a release's notes, in Markdown: its finished tickets
|
||||
* grouped by type (in the project's order of types), newest version header
|
||||
* first. Someone edits it before publishing it; it is never saved by itself.
|
||||
*/
|
||||
export function draftReleaseNotes(input: {
|
||||
release: ReleaseReadModel;
|
||||
issues: IssueReadModel[];
|
||||
workflow: ProjectWorkflow;
|
||||
}): string {
|
||||
const { release, issues, workflow } = input;
|
||||
const done = issues.filter(
|
||||
(i) => i.statusCategory === StatusCategory.DONE && !LEFT_OUT.has(i.type),
|
||||
);
|
||||
const day = release.releasedAt ?? release.targetDate;
|
||||
const date = day
|
||||
? ` — ${new Date(day).toLocaleDateString('es', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
timeZone: 'UTC',
|
||||
})}`
|
||||
: '';
|
||||
const lines = [`## ${release.name}${date}`];
|
||||
if (release.description) lines.push('', release.description);
|
||||
const order = workflow.settings.issueTypes.map((t) => t.id);
|
||||
const types = [...new Set(done.map((i) => i.type))].sort(
|
||||
(a, b) =>
|
||||
(order.indexOf(a) + 1 || order.length + 1) -
|
||||
(order.indexOf(b) + 1 || order.length + 1),
|
||||
);
|
||||
for (const type of types) {
|
||||
lines.push('', `### ${HEADING[type] ?? workflow.typeName(type)}`, '');
|
||||
for (const issue of done
|
||||
.filter((i) => i.type === type)
|
||||
.sort((a, b) => a.number - b.number))
|
||||
lines.push(`- ${issue.title} (${issue.key})`);
|
||||
}
|
||||
if (!done.length)
|
||||
lines.push('', '_Aún no hay tickets terminados en esta release._');
|
||||
return lines.join('\n') + '\n';
|
||||
}
|
||||
Reference in New Issue
Block a user