Smart changelog (#5205)
Added a smart Changelog : - Publish the Changelog before the app release. If the release has not yet been pushed to production, do not display it. - When the app release is done, make the Changelog available with the correct date. - If the Changelog writing is delayed because the release has already been made, publish it immediately. - Display everything locally to be able to iterate on the changelog and have a preview Added an endpoint for the Changelog --------- Co-authored-by: Ady Beraud <a.beraud96@gmail.com> Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
@ -0,0 +1,19 @@
|
||||
export const getFormattedReleaseNumber = (versionNumber: string) => {
|
||||
const formattedVersion = versionNumber.replace('v', '');
|
||||
|
||||
const parts = formattedVersion.split('.').map(Number);
|
||||
|
||||
if (parts.length !== 3) {
|
||||
throw new Error('Version must be in the format major.minor.patch');
|
||||
}
|
||||
|
||||
// Assign weights. Adjust these based on your needs.
|
||||
const majorWeight = 10000;
|
||||
const minorWeight = 100;
|
||||
const patchWeight = 1;
|
||||
|
||||
const numericVersion =
|
||||
parts[0] * majorWeight + parts[1] * minorWeight + parts[2] * patchWeight;
|
||||
|
||||
return numericVersion;
|
||||
};
|
||||
@ -0,0 +1,43 @@
|
||||
import { GithubReleases } from '@/database/model';
|
||||
|
||||
function formatDate(dateString: string) {
|
||||
const date = new Date(dateString);
|
||||
|
||||
const formatter = new Intl.DateTimeFormat('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
|
||||
return formatter.format(date) + getOrdinal(date.getDate());
|
||||
}
|
||||
|
||||
function getOrdinal(day: number) {
|
||||
if (day > 3 && day < 21) return 'th';
|
||||
switch (day % 10) {
|
||||
case 1:
|
||||
return 'st';
|
||||
case 2:
|
||||
return 'nd';
|
||||
case 3:
|
||||
return 'rd';
|
||||
default:
|
||||
return 'th';
|
||||
}
|
||||
}
|
||||
|
||||
export const getGithubReleaseDateFromReleaseNote = (
|
||||
githubReleases: GithubReleases[],
|
||||
noteTagName: string,
|
||||
noteDate: string,
|
||||
) => {
|
||||
const formattedNoteTagName = `v${noteTagName}`;
|
||||
const date = githubReleases?.find?.(
|
||||
(githubRelease) => githubRelease?.tagName === formattedNoteTagName,
|
||||
)?.publishedAt;
|
||||
|
||||
if (date) {
|
||||
return formatDate(date);
|
||||
}
|
||||
|
||||
return noteDate;
|
||||
};
|
||||
@ -0,0 +1,70 @@
|
||||
import { JSXElementConstructor, ReactElement } from 'react';
|
||||
import fs from 'fs';
|
||||
import matter from 'gray-matter';
|
||||
import { compileMDX } from 'next-mdx-remote/rsc';
|
||||
import gfm from 'remark-gfm';
|
||||
|
||||
import { ReleaseNote } from '@/app/releases/api/route';
|
||||
import { compareSemanticVersions } from '@/shared-utils/compareSemanticVersions';
|
||||
|
||||
// WARNING: This API is used by twenty-front, not just by twenty-website
|
||||
// Make sure you don't change it without updating twenty-front at the same time
|
||||
export async function getReleases(baseUrl?: string): Promise<ReleaseNote[]> {
|
||||
const files = fs.readdirSync('src/content/releases');
|
||||
const releasenotes: ReleaseNote[] = [];
|
||||
|
||||
for (const fileName of files) {
|
||||
if (!fileName.endsWith('.md') && !fileName.endsWith('.mdx')) {
|
||||
continue;
|
||||
}
|
||||
const file = fs.readFileSync(`src/content/releases/${fileName}`, 'utf-8');
|
||||
const { data: frontmatter, content } = matter(file);
|
||||
|
||||
let updatedContent;
|
||||
if (baseUrl) {
|
||||
updatedContent = content.replace(
|
||||
/!\[(.*?)\]\((?!http)(.*?)\)/g,
|
||||
(match: string, alt: string, src: string) => {
|
||||
// Check if src is a relative path (not starting with http:// or https://)
|
||||
if (!src.startsWith('/')) {
|
||||
src = `${baseUrl}/${src}`;
|
||||
} else {
|
||||
src = `${baseUrl}${src}`;
|
||||
}
|
||||
return ``;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
releasenotes.push({
|
||||
slug: fileName.slice(0, -4),
|
||||
date: frontmatter.Date,
|
||||
release: frontmatter.release,
|
||||
content: updatedContent ?? content,
|
||||
});
|
||||
}
|
||||
|
||||
releasenotes.sort((a, b) => compareSemanticVersions(b.release, a.release));
|
||||
|
||||
return releasenotes;
|
||||
}
|
||||
|
||||
export async function getMdxReleasesContent(
|
||||
releases: ReleaseNote[],
|
||||
): Promise<ReactElement<any, string | JSXElementConstructor<any>>[]> {
|
||||
const mdxSourcesPromises = releases.map(async (release) => {
|
||||
const mdxSource = await compileMDX<{ title: string; position?: number }>({
|
||||
source: release.content,
|
||||
options: {
|
||||
parseFrontmatter: true,
|
||||
mdxOptions: {
|
||||
development: process.env.NODE_ENV === 'development',
|
||||
remarkPlugins: [gfm],
|
||||
},
|
||||
},
|
||||
});
|
||||
return mdxSource.content;
|
||||
});
|
||||
|
||||
return await Promise.all(mdxSourcesPromises);
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
import { ReleaseNote } from '@/app/releases/api/route';
|
||||
import { getFormattedReleaseNumber } from '@/app/releases/utils/get-formatted-release-number';
|
||||
|
||||
export const getVisibleReleases = (
|
||||
releaseNotes: ReleaseNote[],
|
||||
publishedReleaseVersion: string,
|
||||
) => {
|
||||
if (process.env.NODE_ENV !== 'production') return releaseNotes;
|
||||
|
||||
const publishedVersionNumber = getFormattedReleaseNumber(
|
||||
publishedReleaseVersion,
|
||||
);
|
||||
|
||||
return releaseNotes.filter(
|
||||
(releaseNote) =>
|
||||
getFormattedReleaseNumber(releaseNote.release) <= publishedVersionNumber,
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user