Skip to content
Open
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
26 changes: 26 additions & 0 deletions src/pages/docs/changelog.md.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { APIRoute } from "astro"
import { renderChangelogIndexMarkdown } from "~/utils/changelogMarkdown"
import { fetchMajorReleases } from "~/utils/fetchChangelogVersions"

// Same limit as the getStaticPaths in src/pages/docs/changelog/[tag].astro, so
// the index lists exactly the release pages that get built.
const CHANGELOG_RELEASES = 150

/**
* Serves the changelog index page (`/docs/changelog`) as raw Markdown at
* `/docs/changelog.md`.
*
* The release notes themselves live behind `/docs/changelog/{tag}.md`; this
* index gives AI agents and other tools a single entry point to discover them
* instead of returning a 404.
*/
export const GET: APIRoute = async () => {
const releases = await fetchMajorReleases(CHANGELOG_RELEASES)

return new Response(renderChangelogIndexMarkdown(releases), {
status: 200,
headers: {
"Content-Type": "text/markdown; charset=utf-8",
},
})
}
42 changes: 42 additions & 0 deletions src/pages/docs/changelog/[tag].md.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { APIRoute, GetStaticPaths } from "astro"
import { renderReleaseMarkdown } from "~/utils/changelogMarkdown"
import {
fetchMajorReleases,
type GitHubRelease,
} from "~/utils/fetchChangelogVersions"

// Same limit as the getStaticPaths in src/pages/docs/changelog/[tag].astro, so
// every release page that gets built also gets a Markdown endpoint.
const CHANGELOG_RELEASES = 150

export const getStaticPaths = (async () => {
const releases = await fetchMajorReleases(CHANGELOG_RELEASES)
return releases.map((release: GitHubRelease) => ({
params: { tag: release.tag_name },
props: { release },
}))
}) satisfies GetStaticPaths

/**
* Serves a release notes page (`/docs/changelog/{tag}`) as raw Markdown at
* `/docs/changelog/{tag}.md`.
*
* The catch-all route `docs/[...docsPath].md.ts` only emits paths for the
* `docs` content collection, and changelog pages are built from the GitHub
* releases API instead of Markdown files in this repo. Without this endpoint
* every `/docs/changelog/*.md` URL 404s, which breaks the "append .md to any
* kestra.io/docs/* URL" contract advertised in DocsLayout and llms-full.txt.
*/
export const GET: APIRoute = ({ props }) => {
const release = (props as { release?: GitHubRelease }).release
if (!release) {
return new Response("Not found", { status: 404 })
}

return new Response(renderReleaseMarkdown(release), {
status: 200,
headers: {
"Content-Type": "text/markdown; charset=utf-8",
},
})
}
72 changes: 72 additions & 0 deletions src/utils/changelogMarkdown.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { describe, expect, it } from "vitest"
import type { GitHubRelease } from "~/utils/fetchChangelogVersions"
import {
renderChangelogIndexMarkdown,
renderReleaseMarkdown,
} from "~/utils/changelogMarkdown"

// Shaped like the GitHub releases payload after fetchMajorReleases(): the body is
// already Markdown with commit links rewritten.
const release = (over: Partial<GitHubRelease> = {}): GitHubRelease => ({
tag_name: "v1.3.35",
name: "v1.3.35",
body: "## Changelog\n\n### 🐛 Bug Fixes",
published_at: "2026-08-25T09:12:33Z",
draft: false,
prerelease: false,
...over,
})

describe("renderReleaseMarkdown", () => {
it("prefixes the release body with the release name as an h1", () => {
expect(renderReleaseMarkdown(release())).toBe(
"# v1.3.35\n\n## Changelog\n\n### 🐛 Bug Fixes",
)
})

it("falls back to the tag when the release has no name", () => {
const md = renderReleaseMarkdown(release({ name: undefined }))
expect(md.startsWith("# v1.3.35\n\n")).toBe(true)
})

it("does not emit 'undefined' when the body is missing", () => {
const md = renderReleaseMarkdown(
release({ body: undefined as unknown as string }),
)
expect(md).toBe("# v1.3.35\n\n")
})
})

describe("renderChangelogIndexMarkdown", () => {
it("links every release to its page with the publication date", () => {
const md = renderChangelogIndexMarkdown([
release(),
release({ tag_name: "v1.0.57", name: "v1.0.57" }),
])

expect(md).toContain(
"- [v1.3.35](https://kestra.io/docs/changelog/v1.3.35) — 2026-08-25",
)
expect(md).toContain(
"- [v1.0.57](https://kestra.io/docs/changelog/v1.0.57) — 2026-08-25",
)
expect(md.startsWith("# Release Notes\n")).toBe(true)
})

it("omits the separator when a release has no publication date", () => {
const md = renderChangelogIndexMarkdown([
release({ published_at: undefined as unknown as string }),
])

expect(md).toContain(
"- [v1.3.35](https://kestra.io/docs/changelog/v1.3.35)\n",
)
expect(md).not.toContain("—")
})

it("still renders a usable page when the release list is empty", () => {
const md = renderChangelogIndexMarkdown([])
expect(md).toContain("# Release Notes")
expect(md).not.toContain("- [")
})
})
31 changes: 31 additions & 0 deletions src/utils/changelogMarkdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { GitHubRelease } from "~/utils/fetchChangelogVersions"

export const CHANGELOG_BASE_URL = "https://kestra.io/docs/changelog"

/** Renders a single release as the Markdown served at `/docs/changelog/{tag}.md`. */
export function renderReleaseMarkdown(release: GitHubRelease): string {
const title = release.name || release.tag_name
return `# ${title}\n\n${release.body ?? ""}`
}

/** Renders the release index served at `/docs/changelog.md`. */
export function renderChangelogIndexMarkdown(
releases: GitHubRelease[],
): string {
const lines = releases.map((release) => {
const title = release.name || release.tag_name
const url = `${CHANGELOG_BASE_URL}/${release.tag_name}`
const date = release.published_at?.slice(0, 10)
return date ? `- [${title}](${url}) — ${date}` : `- [${title}](${url})`
})

return [
"# Release Notes",
"",
"Stay up to date with the latest Kestra releases and updates.",
"Append `.md` to any release URL below to retrieve its notes as plain Markdown.",
"",
...lines,
"",
].join("\n")
}
Loading