From df5cb9a904e7606d524634514b2cf9a1b9475dd2 Mon Sep 17 00:00:00 2001 From: Ady Beraud <102751374+ady-beraud@users.noreply.github.com> Date: Wed, 1 May 2024 09:35:11 +0300 Subject: [PATCH] Smart changelog (#5205) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Co-authored-by: Félix Malfait --- .../src/app/_components/releases/Release.tsx | 8 ++-- .../src/app/api/releases/route.tsx | 41 ++++++++++++++++++ .../src/app/releases/api/route.tsx | 2 +- .../twenty-website/src/app/releases/page.tsx | 33 +++++++++++--- .../utils/get-formatted-release-number.ts | 19 ++++++++ ...t-github-release-date-from-release-note.ts | 43 +++++++++++++++++++ .../app/releases/{ => utils}/get-releases.tsx | 0 .../releases/utils/get-visible-releases.ts | 18 ++++++++ .../twenty-website/src/database/database.ts | 5 ++- .../src/database/init-database.ts | 8 ++++ .../0002_demonic_matthew_murdock.sql | 9 ++++ .../database/migrations/meta/_journal.json | 7 +++ packages/twenty-website/src/database/model.ts | 3 ++ .../src/database/schema-postgres.ts | 7 ++- .../src/github-sync/contributors/types.tsx | 12 ++++++ .../github-sync/fetch-and-save-github-data.ts | 2 + .../fetch-and-save-github-releases.tsx | 26 +++++++++++ 17 files changed, 232 insertions(+), 11 deletions(-) create mode 100644 packages/twenty-website/src/app/api/releases/route.tsx create mode 100644 packages/twenty-website/src/app/releases/utils/get-formatted-release-number.ts create mode 100644 packages/twenty-website/src/app/releases/utils/get-github-release-date-from-release-note.ts rename packages/twenty-website/src/app/releases/{ => utils}/get-releases.tsx (100%) create mode 100644 packages/twenty-website/src/app/releases/utils/get-visible-releases.ts create mode 100644 packages/twenty-website/src/database/init-database.ts create mode 100644 packages/twenty-website/src/database/migrations/0002_demonic_matthew_murdock.sql create mode 100644 packages/twenty-website/src/github-sync/github-releases/fetch-and-save-github-releases.tsx diff --git a/packages/twenty-website/src/app/_components/releases/Release.tsx b/packages/twenty-website/src/app/_components/releases/Release.tsx index 0b5a28cec..f0eddc523 100644 --- a/packages/twenty-website/src/app/_components/releases/Release.tsx +++ b/packages/twenty-website/src/app/_components/releases/Release.tsx @@ -63,8 +63,10 @@ const gabarito = Gabarito({ export const Release = ({ release, mdxReleaseContent, + githubPublishedAt, }: { release: ReleaseNote; + githubPublishedAt: string; mdxReleaseContent: ReactElement>; }) => { return ( @@ -73,9 +75,9 @@ export const Release = ({ {release.release} - {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} {mdxReleaseContent} diff --git a/packages/twenty-website/src/app/api/releases/route.tsx b/packages/twenty-website/src/app/api/releases/route.tsx new file mode 100644 index 000000000..ae5b8441c --- /dev/null +++ b/packages/twenty-website/src/app/api/releases/route.tsx @@ -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, + }); + } +} diff --git a/packages/twenty-website/src/app/releases/api/route.tsx b/packages/twenty-website/src/app/releases/api/route.tsx index afd9dfe8c..849c8a4e6 100644 --- a/packages/twenty-website/src/app/releases/api/route.tsx +++ b/packages/twenty-website/src/app/releases/api/route.tsx @@ -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; diff --git a/packages/twenty-website/src/app/releases/page.tsx b/packages/twenty-website/src/app/releases/page.tsx index 3b57f09c7..40a2fe7da 100644 --- a/packages/twenty-website/src/app/releases/page.tsx +++ b/packages/twenty-website/src/app/releases/page.tsx @@ -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 ( - {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> diff --git a/packages/twenty-website/src/app/releases/utils/get-formatted-release-number.ts b/packages/twenty-website/src/app/releases/utils/get-formatted-release-number.ts new file mode 100644 index 000000000..ebdb33cc7 --- /dev/null +++ b/packages/twenty-website/src/app/releases/utils/get-formatted-release-number.ts @@ -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; +}; diff --git a/packages/twenty-website/src/app/releases/utils/get-github-release-date-from-release-note.ts b/packages/twenty-website/src/app/releases/utils/get-github-release-date-from-release-note.ts new file mode 100644 index 000000000..8e7116bd5 --- /dev/null +++ b/packages/twenty-website/src/app/releases/utils/get-github-release-date-from-release-note.ts @@ -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; +}; diff --git a/packages/twenty-website/src/app/releases/get-releases.tsx b/packages/twenty-website/src/app/releases/utils/get-releases.tsx similarity index 100% rename from packages/twenty-website/src/app/releases/get-releases.tsx rename to packages/twenty-website/src/app/releases/utils/get-releases.tsx diff --git a/packages/twenty-website/src/app/releases/utils/get-visible-releases.ts b/packages/twenty-website/src/app/releases/utils/get-visible-releases.ts new file mode 100644 index 000000000..2a5bc5a53 --- /dev/null +++ b/packages/twenty-website/src/app/releases/utils/get-visible-releases.ts @@ -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, + ); +}; diff --git a/packages/twenty-website/src/database/database.ts b/packages/twenty-website/src/database/database.ts index b940ace7c..69b30d2eb 100644 --- a/packages/twenty-website/src/database/database.ts +++ b/packages/twenty-website/src/database/database.ts @@ -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(); }; diff --git a/packages/twenty-website/src/database/init-database.ts b/packages/twenty-website/src/database/init-database.ts new file mode 100644 index 000000000..440f1b39e --- /dev/null +++ b/packages/twenty-website/src/database/init-database.ts @@ -0,0 +1,8 @@ +import { migrate } from '@/database/database'; + +export const initDatabase = async () => { + await migrate(); + process.exit(0); +}; + +initDatabase(); diff --git a/packages/twenty-website/src/database/migrations/0002_demonic_matthew_murdock.sql b/packages/twenty-website/src/database/migrations/0002_demonic_matthew_murdock.sql new file mode 100644 index 000000000..c4b1eceb6 --- /dev/null +++ b/packages/twenty-website/src/database/migrations/0002_demonic_matthew_murdock.sql @@ -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 +); diff --git a/packages/twenty-website/src/database/migrations/meta/_journal.json b/packages/twenty-website/src/database/migrations/meta/_journal.json index 816086f87..2f4446e5f 100644 --- a/packages/twenty-website/src/database/migrations/meta/_journal.json +++ b/packages/twenty-website/src/database/migrations/meta/_journal.json @@ -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 } ] } \ No newline at end of file diff --git a/packages/twenty-website/src/database/model.ts b/packages/twenty-website/src/database/model.ts index 84613004d..7344f7be8 100644 --- a/packages/twenty-website/src/database/model.ts +++ b/packages/twenty-website/src/database/model.ts @@ -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; diff --git a/packages/twenty-website/src/database/schema-postgres.ts b/packages/twenty-website/src/database/schema-postgres.ts index d65aca0c5..b9c50c99b 100644 --- a/packages/twenty-website/src/database/schema-postgres.ts +++ b/packages/twenty-website/src/database/schema-postgres.ts @@ -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(), +}); diff --git a/packages/twenty-website/src/github-sync/contributors/types.tsx b/packages/twenty-website/src/github-sync/contributors/types.tsx index 3b78b4df1..0290e62b4 100644 --- a/packages/twenty-website/src/github-sync/contributors/types.tsx +++ b/packages/twenty-website/src/github-sync/contributors/types.tsx @@ -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; }; } diff --git a/packages/twenty-website/src/github-sync/fetch-and-save-github-data.ts b/packages/twenty-website/src/github-sync/fetch-and-save-github-data.ts index a3e7cb4c9..48616efc5 100644 --- a/packages/twenty-website/src/github-sync/fetch-and-save-github-data.ts +++ b/packages/twenty-website/src/github-sync/fetch-and-save-github-data.ts @@ -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( diff --git a/packages/twenty-website/src/github-sync/github-releases/fetch-and-save-github-releases.tsx b/packages/twenty-website/src/github-sync/github-releases/fetch-and-save-github-releases.tsx new file mode 100644 index 000000000..7237e912b --- /dev/null +++ b/packages/twenty-website/src/github-sync/github-releases/fetch-and-save-github-releases.tsx @@ -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', + }); +};