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:
Ady Beraud 2024-05-01 09:35:11 +03:00 committed by GitHub
parent bf7c4a5a89
commit df5cb9a904
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 232 additions and 11 deletions

View File

@ -63,8 +63,10 @@ const gabarito = Gabarito({
export const Release = ({
release,
mdxReleaseContent,
githubPublishedAt,
}: {
release: ReleaseNote;
githubPublishedAt: string;
mdxReleaseContent: ReactElement<any, string | JSXElementConstructor<any>>;
}) => {
return (
@ -73,9 +75,9 @@ export const Release = ({
<StyledVersion>
<StyledRelease>{release.release}</StyledRelease>
<StyledDate>
{release.date.endsWith(new Date().getFullYear().toString())
? release.date.slice(0, -5)
: release.date}
{githubPublishedAt.endsWith(new Date().getFullYear().toString())
? githubPublishedAt.slice(0, -5)
: githubPublishedAt}
</StyledDate>
</StyledVersion>
<ArticleContent>{mdxReleaseContent}</ArticleContent>

View File

@ -0,0 +1,41 @@
import { desc } from 'drizzle-orm';
import { getGithubReleaseDateFromReleaseNote } from '@/app/releases/utils/get-github-release-date-from-release-note';
import { getReleases } from '@/app/releases/utils/get-releases';
import { getVisibleReleases } from '@/app/releases/utils/get-visible-releases';
import { findAll } from '@/database/database';
import { GithubReleases, githubReleasesModel } from '@/database/model';
export const dynamic = 'force-dynamic';
export async function GET() {
try {
const githubReleases = (await findAll(
githubReleasesModel,
desc(githubReleasesModel.publishedAt),
)) as GithubReleases[];
const latestGithubRelease = githubReleases[0];
const releaseNotes = await getReleases();
const visibleReleasesNotes = getVisibleReleases(
releaseNotes,
latestGithubRelease.tagName,
);
const formattedReleasesNotes = visibleReleasesNotes.map((releaseNote) => ({
...releaseNote,
publishedAt: getGithubReleaseDateFromReleaseNote(
githubReleases,
releaseNote.release,
releaseNote.date,
),
}));
return Response.json(formattedReleasesNotes);
} catch (error: any) {
return new Response(`Github releases error: ${error?.message}`, {
status: 500,
});
}
}

View File

@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server';
import { getReleases } from '@/app/releases/get-releases';
import { getReleases } from '@/app/releases/utils/get-releases';
export interface ReleaseNote {
slug: string;

View File

@ -1,14 +1,20 @@
import React from 'react';
import { desc } from 'drizzle-orm';
import { Metadata } from 'next';
import { Line } from '@/app/_components/releases/Line';
import { Release } from '@/app/_components/releases/Release';
import { Title } from '@/app/_components/releases/StyledTitle';
import { ContentContainer } from '@/app/_components/ui/layout/ContentContainer';
import { getGithubReleaseDateFromReleaseNote } from '@/app/releases/utils/get-github-release-date-from-release-note';
import {
getMdxReleasesContent,
getReleases,
} from '@/app/releases/get-releases';
} from '@/app/releases/utils/get-releases';
import { getVisibleReleases } from '@/app/releases/utils/get-visible-releases';
import { findAll } from '@/database/database';
import { GithubReleases, githubReleasesModel } from '@/database/model';
import { pgGithubReleasesModel } from '@/database/schema-postgres';
export const metadata: Metadata = {
title: 'Twenty - Releases',
@ -19,20 +25,37 @@ export const metadata: Metadata = {
export const dynamic = 'force-dynamic';
const Home = async () => {
const releases = await getReleases();
const mdxReleasesContent = await getMdxReleasesContent(releases);
const githubReleases = (await findAll(
githubReleasesModel,
desc(pgGithubReleasesModel.publishedAt),
)) as GithubReleases[];
const latestGithubRelease = githubReleases[0];
const releaseNotes = await getReleases();
const visibleReleasesNotes = getVisibleReleases(
releaseNotes,
latestGithubRelease.tagName,
);
const mdxReleasesContent = await getMdxReleasesContent(releaseNotes);
return (
<ContentContainer>
<Title />
{releases.map((note, index) => (
{visibleReleasesNotes.map((note, index) => (
<React.Fragment key={note.slug}>
<Release
githubPublishedAt={getGithubReleaseDateFromReleaseNote(
githubReleases,
note.release,
note.date,
)}
release={note}
mdxReleaseContent={mdxReleasesContent[index]}
/>
{index != releases.length - 1 && <Line />}
{index != releaseNotes.length - 1 && <Line />}
</React.Fragment>
))}
</ContentContainer>

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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,
);
};

View File

@ -22,7 +22,10 @@ const findOne = (model: any, orderBy: any) => {
return pgDb.select().from(model).orderBy(orderBy).limit(1).execute();
};
const findAll = (model: any) => {
const findAll = (model: any, orderBy?: any) => {
if (orderBy) {
return pgDb.select().from(model).orderBy(orderBy).execute();
}
return pgDb.select().from(model).execute();
};

View File

@ -0,0 +1,8 @@
import { migrate } from '@/database/database';
export const initDatabase = async () => {
await migrate();
process.exit(0);
};
initDatabase();

View File

@ -0,0 +1,9 @@
CREATE TABLE IF NOT EXISTS "githubReleases" (
"tagName" text PRIMARY KEY NOT NULL,
"publishedAt" date NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "githubStars" (
"timestamp" timestamp DEFAULT now() NOT NULL,
"numberOfStars" integer
);

View File

@ -15,6 +15,13 @@
"when": 1713792223113,
"tag": "0001_marvelous_eddie_brock",
"breakpoints": true
},
{
"idx": 2,
"version": "5",
"when": 1714375499735,
"tag": "0002_demonic_matthew_murdock",
"breakpoints": true
}
]
}

View File

@ -1,4 +1,5 @@
import {
pgGithubReleasesModel,
pgGithubStars,
pgIssueLabels,
pgIssues,
@ -16,6 +17,7 @@ export const pullRequestLabelModel = pgPullRequestLabels;
export const issueLabelModel = pgIssueLabels;
export const githubStarsModel = pgGithubStars;
export const githubReleasesModel = pgGithubReleasesModel;
export type User = typeof pgUsers.$inferSelect;
export type PullRequest = typeof pgPullRequests.$inferSelect;
@ -31,3 +33,4 @@ export type LabelInsert = typeof pgLabels.$inferInsert;
export type PullRequestLabelInsert = typeof pgPullRequestLabels.$inferInsert;
export type IssueLabelInsert = typeof pgIssueLabels.$inferInsert;
export type GithubStars = typeof pgGithubStars.$inferInsert;
export type GithubReleases = typeof pgGithubReleasesModel.$inferInsert;

View File

@ -1,4 +1,4 @@
import { integer, pgTable, text, timestamp } from 'drizzle-orm/pg-core';
import { date, integer, pgTable, text, timestamp } from 'drizzle-orm/pg-core';
export const pgUsers = pgTable('users', {
id: text('id').primaryKey(),
@ -55,3 +55,8 @@ export const pgGithubStars = pgTable('githubStars', {
timestamp: timestamp('timestamp').notNull().defaultNow(),
numberOfStars: integer('numberOfStars'),
});
export const pgGithubReleasesModel = pgTable('githubReleases', {
tagName: text('tagName').primaryKey(),
publishedAt: date('publishedAt', { mode: 'string' }).notNull(),
});

View File

@ -68,12 +68,24 @@ export interface Stargazers {
totalCount: number;
}
export interface Releases {
nodes: ReleaseNode[];
}
export interface ReleaseNode {
tagName: string;
name: string;
description: string;
publishedAt: string;
}
export interface Repository {
repository: {
pullRequests: PullRequests;
issues: Issues;
assignableUsers: AssignableUsers;
stargazers: Stargazers;
releases: Releases;
};
}

View File

@ -6,6 +6,7 @@ import { fetchIssuesPRs } from '@/github-sync/contributors/fetch-issues-prs';
import { saveIssuesToDB } from '@/github-sync/contributors/save-issues-to-db';
import { savePRsToDB } from '@/github-sync/contributors/save-prs-to-db';
import { IssueNode, PullRequestNode } from '@/github-sync/contributors/types';
import { fetchAndSaveGithubReleases } from '@/github-sync/github-releases/fetch-and-save-github-releases';
import { fetchAndSaveGithubStars } from '@/github-sync/github-stars/fetch-and-save-github-stars';
export const fetchAndSaveGithubData = async () => {
@ -22,6 +23,7 @@ export const fetchAndSaveGithubData = async () => {
});
await fetchAndSaveGithubStars(query);
await fetchAndSaveGithubReleases(query);
const assignableUsers = await fetchAssignableUsers(query);
const fetchedPRs = (await fetchIssuesPRs(

View File

@ -0,0 +1,26 @@
import { graphql } from '@octokit/graphql';
import { insertMany } from '@/database/database';
import { githubReleasesModel } from '@/database/model';
import { Repository } from '@/github-sync/contributors/types';
export const fetchAndSaveGithubReleases = async (
query: typeof graphql,
): Promise<void> => {
const { repository } = await query<Repository>(`
query {
repository(owner: "twentyhq", name: "twenty") {
releases(first: 100) {
nodes {
tagName
publishedAt
}
}
}
}
`);
await insertMany(githubReleasesModel, repository.releases.nodes, {
onConflictKey: 'tagName',
});
};