Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/changelog-comment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,21 @@ jobs:

const sections = ['### Added', '', '### Changed', '', '### Deprecated', '', '### Removed', '', '### Fixed', '', '### Security'].join('\n');
const productBlock = (name) => `<details>\n<summary>${name}</summary>\n\n${sections}\n\n</details>`;
const bakeStatus = [
'<!-- changelog:bake-status -->',
'> [!NOTE]',
'> Changelog bake status:',
'> - [ ] App <!-- changelog:product:app -->',
'> - [ ] Website <!-- changelog:product:web -->',
'> - [ ] Hosting <!-- changelog:product:hosting -->',
].join('\n');

const template = [
marker,
'## Pull request changelog',
'',
bakeStatus,
'',
'<!-- Fill in the changelog under each product area this PR affects.',
' Empty sections are ignored. Leave a product collapsed/empty',
' if it doesn\'t apply. -->',
Expand Down
55 changes: 55 additions & 0 deletions apps/frontend/src/helpers/changelog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { getChangelog, type ChangelogEntry, type Product, type VersionEntry } from '@modrinth/blog'
import dayjs from 'dayjs'

export interface AppRelease {
version: string
publishedAt: string
url: string
}

function resolveChangelogEntry(
entry: ChangelogEntry,
appReleaseByVersion: Map<string, AppRelease>,
): VersionEntry | null {
if (entry.date) {
return entry as VersionEntry
}

if (entry.product !== 'app' || !entry.version) {
return null
}

const release = appReleaseByVersion.get(entry.version)
if (!release) {
return null
}

return {
...entry,
date: dayjs(release.publishedAt),
}
}

export function resolveChangelogEntries(appReleases: AppRelease[] = []): VersionEntry[] {
const appReleaseByVersion = new Map(appReleases.map((release) => [release.version, release]))

return getChangelog().flatMap((entry) => {
const resolvedEntry = resolveChangelogEntry(entry, appReleaseByVersion)
return resolvedEntry ? [resolvedEntry] : []
})
}

export function findChangelogEntry(
entries: VersionEntry[],
productParam: string | string[],
dateParam: string | string[],
): VersionEntry | undefined {
const product = (Array.isArray(productParam) ? productParam[0] : productParam) as Product
const date = Array.isArray(dateParam) ? dateParam[0] : dateParam

return entries.find((entry) => {
if (entry.product !== product) return false
if (entry.version && entry.version === date) return true
return entry.date.unix() === Number(date)
})
}
31 changes: 18 additions & 13 deletions apps/frontend/src/pages/news/changelog/[product]/[date].vue
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
<script setup lang="ts">
import { ChevronLeftIcon } from '@modrinth/assets'
import { getChangelog } from '@modrinth/blog'
import { ChangelogEntry, Timeline } from '@modrinth/ui'
import { useQuery } from '@tanstack/vue-query'

import {
findChangelogEntry,
resolveChangelogEntries,
type AppRelease,
} from '~/helpers/changelog'

const route = useRoute()

const { data: appReleases, suspense: appReleasesSuspense } = useQuery({
queryKey: ['changelog', 'app-releases'],
queryFn: () => $fetch<AppRelease[]>('/api/changelog/app-releases'),
})

await appReleasesSuspense().catch(() => {})

const changelogEntries = computed(() => resolveChangelogEntries(appReleases.value ?? []))
const changelogEntry = computed(() =>
route.params.date
? getChangelog().find((x) => {
if (x.product === route.params.product) {
if (x.version && x.version === route.params.date) {
return x
} else if (x.date.unix() === Number(route.params.date as string)) {
return x
}
}
return undefined
})
route.params.date && route.params.product
? findChangelogEntry(changelogEntries.value, route.params.product, route.params.date)
: undefined,
)

const isFirst = computed(() => changelogEntry.value?.date === getChangelog()[0].date)
const isFirst = computed(() => changelogEntry.value === changelogEntries.value[0])

if (!changelogEntry.value) {
throw createError({ statusCode: 404, statusMessage: 'Version not found' })
Expand Down
20 changes: 16 additions & 4 deletions apps/frontend/src/pages/news/changelog/index.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
<script setup lang="ts">
import { getChangelog, type Product } from '@modrinth/blog'
import type { Product } from '@modrinth/blog'
import { ChangelogEntry, NavTabs } from '@modrinth/ui'
import Timeline from '@modrinth/ui/src/components/base/Timeline.vue'
import { useQuery } from '@tanstack/vue-query'

import { resolveChangelogEntries, type AppRelease } from '~/helpers/changelog'

const route = useRoute()

const filter = ref<Product | undefined>(undefined)
const allChangelogEntries = ref(getChangelog())
const { data: appReleases, suspense: appReleasesSuspense } = useQuery({
queryKey: ['changelog', 'app-releases'],
queryFn: () => $fetch<AppRelease[]>('/api/changelog/app-releases'),
})

onServerPrefetch(async () => {
await appReleasesSuspense().catch(() => {})
})

function updateFilter() {
if (route.query.filter) {
Expand All @@ -24,7 +34,9 @@ watch(
)

const changelogEntries = computed(() =>
allChangelogEntries.value.filter((x) => !filter.value || x.product === filter.value),
resolveChangelogEntries(appReleases.value ?? []).filter(
(entry) => !filter.value || entry.product === filter.value,
),
)
</script>

Expand Down Expand Up @@ -54,7 +66,7 @@ const changelogEntries = computed(() =>
<Timeline fade-out-end>
<ChangelogEntry
v-for="(entry, index) in changelogEntries"
:key="entry.date"
:key="`${entry.product}-${entry.version ?? entry.date.unix()}`"
:entry="entry"
:first="index === 0"
:show-type="filter === undefined"
Expand Down
65 changes: 65 additions & 0 deletions apps/frontend/src/server/routes/api/changelog/app-releases.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
const CACHE_MAX_AGE = 60 * 30
const GITHUB_RELEASES_URL = 'https://api.github.com/repos/modrinth/code/releases'
const PAGE_SIZE = 100

interface GitHubRelease {
tag_name: string
html_url: string
published_at: string | null
created_at: string
}

export interface AppRelease {
version: string
publishedAt: string
url: string
}

export default defineCachedEventHandler(
async (): Promise<AppRelease[]> => {
const releases: GitHubRelease[] = []
let page = 1

while (true) {
const response = await fetch(`${GITHUB_RELEASES_URL}?per_page=${PAGE_SIZE}&page=${page}`, {
headers: {
Accept: 'application/vnd.github+json',
'User-Agent': 'modrinth-changelog',
},
})

if (!response.ok) {
throw createError({
statusCode: 502,
message: `GitHub releases request failed with ${response.status}`,
})
}

const pageReleases = (await response.json()) as GitHubRelease[]
if (!Array.isArray(pageReleases)) {
throw createError({ statusCode: 502, message: 'Invalid GitHub releases response' })
}

releases.push(...pageReleases)

if (pageReleases.length < PAGE_SIZE) {
break
}

page++
}

return releases
.filter((release) => release.tag_name.startsWith('v'))
.map((release) => ({
version: release.tag_name.replace(/^v/, ''),
publishedAt: release.published_at ?? release.created_at,
url: release.html_url,
}))
},
{
maxAge: CACHE_MAX_AGE,
name: 'changelog-app-releases',
getKey: () => 'changelog-app-releases',
},
)
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
"icons:add": "pnpm --filter @modrinth/assets icons:add",
"changelog:collect": "node scripts/run.mjs collect-changelog",
"changelog:combine-for-app": "node scripts/run.mjs build-theseus-release-notes",
"release:prepare": "node scripts/run.mjs release-prepare",
"release:push": "node scripts/run.mjs release-push",
"scripts": "node scripts/run.mjs"
},
"devDependencies": {
Expand Down
18 changes: 14 additions & 4 deletions packages/blog/changelog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,22 @@ import dayjs from 'dayjs'

export type Product = 'web' | 'hosting' | 'app'

export type VersionEntry = {
date: dayjs.Dayjs
export type ChangelogEntry = {
date?: dayjs.Dayjs
product: Product
version?: string
body: string
}

const VERSIONS: VersionEntry[] = [
export type VersionEntry = ChangelogEntry & {
date: dayjs.Dayjs
}

type RawChangelogEntry = Omit<ChangelogEntry, 'date'> & {
date?: string
}

const VERSIONS: ChangelogEntry[] = ([
{
date: `2026-04-29T17:19:44+00:00`,
product: 'app',
Expand Down Expand Up @@ -2038,7 +2046,9 @@ Contributed by [IMB11](https://github.com/modrinth/code/pull/1301).`,
### Known Issues
- Backups may occasionally take longer than expected or become stuck. If a backup is unresponsive, please submit a support inquiry, and we'll investigate further.`,
},
].map((x) => ({ ...x, date: dayjs(x.date) }) as VersionEntry)
] satisfies RawChangelogEntry[]).map(
(x) => ({ ...x, date: x.date ? dayjs(x.date) : undefined }) as ChangelogEntry,
)

export function getChangelog() {
return VERSIONS
Expand Down
4 changes: 2 additions & 2 deletions scripts/build-theseus-release-notes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const REPO_ROOT = join(__dirname, '..')
type Product = 'web' | 'app' | 'hosting'

interface ChangelogEntry {
date: string
date?: string
product: Product
version: string | undefined
body: string
Expand Down Expand Up @@ -78,7 +78,7 @@ function parseArgs(argv: string[]): { dryRun: boolean; version: string; outFile:
*/
function parseChangelogEntries(src: string): ChangelogEntry[] {
const entryRe =
/\{\s*date:\s*`([^`]+)`,\s*product:\s*'(\w+)',(?:\s*version:\s*[`']([^`']+)[`'],)?\s*body:\s*`([\s\S]*?)`,\s*\}/g
/\{\s*(?:date:\s*`((?:\\`|[^`])*)`,\s*)?product:\s*'(\w+)',(?:\s*version:\s*[`']([^`']+)[`'],)?\s*body:\s*`((?:\\`|[^`])*)`,\s*\}/g
const entries: ChangelogEntry[] = []
let match: RegExpExecArray | null
while ((match = entryRe.exec(src)) !== null) {
Expand Down
Loading
Loading