Skip to content

Commit

Permalink
build: migrate changelog generation to deno (#234)
Browse files Browse the repository at this point in the history
* build: use import map for dax

* chore: indent size

* feat: parse commits

* feat: feature parity

* chore: flatten `scripts/`

* feat: cli

* test: update render result checks
  • Loading branch information
scarf005 committed Mar 13, 2024
1 parent a6d98fa commit 9b965b9
Show file tree
Hide file tree
Showing 22 changed files with 311 additions and 5 deletions.
3 changes: 3 additions & 0 deletions .vscode/settings.json
Expand Up @@ -6,6 +6,9 @@
"[markdown]": {
"editor.defaultFormatter": "denoland.vscode-deno"
},
"[typescript]": {
"editor.indentSize": 2
},
"[jsonc]": {
"editor.defaultFormatter": "denoland.vscode-deno"
},
Expand Down
35 changes: 35 additions & 0 deletions changelog/changelog_test.ts
@@ -0,0 +1,35 @@
import { assertEquals } from "$std/assert/assert_equals.ts"
import { getSections, parseCommits } from "./mod.ts"

export const exampleCommits = parseCommits([
"feat: asdf (#1234)",
"feat!: test (#123)",
"fix(L10n): `language` stuff (#415)",
])
export const exampleSections = getSections(exampleCommits)

Deno.test("parseCommits() correctly parse commits", () => {
assertEquals(exampleCommits, [
{
type: "feat",
scopes: undefined,
breaking: undefined,
subject: "asdf",
pr: "1234",
},
{
type: "feat",
scopes: undefined,
breaking: "!",
subject: "test",
pr: "123",
},
{
type: "fix",
scopes: "L10n",
breaking: undefined,
subject: "`language` stuff",
pr: "415",
},
])
})
74 changes: 74 additions & 0 deletions changelog/mod.ts
@@ -0,0 +1,74 @@
import $ from "$dax/mod.ts"
import { typedRegEx } from "https://deno.land/x/typed_regex@0.2.0/mod.ts"
import type { RegExCaptureResult } from "https://deno.land/x/typed_regex@0.2.0/type_parser.ts"

import { renderBBCode } from "./render_bbcode.ts"
import { getNextVersion } from "./semver.ts"
import { renderMarkdown } from "./render_markdown.ts"
import { renderStS } from "./render_sts.ts"

export type Commit = RegExCaptureResult<typeof re>
export type Sections = Record<string, Commit[]>

export const getTags = () => $`git tag --sort=-v:refname`.lines()

type CommitRange = { from: string; to: string }
export const getCommits = ({ from, to }: CommitRange) =>
$`git log ${from}..${to} --pretty=format:"%s"`.lines()

const re =
"^(?<type>\\w+)(\\((?<scopes>.*)?\\))?(?<breaking>!)?:\\s*(?<subject>.*?)\\s*\\(#(?<pr>\\d+)\\)$"

const commitParser = typedRegEx(re)

export const parseCommits = (xs: string[]) =>
xs
.map(commitParser.captures)
.filter((x): x is Commit => x !== undefined)
.filter((x) => x.breaking || ["fix", "feat"].includes(x.type))

export const getSections = (commits: Commit[]): Sections => ({
"Breaking Changes": commits.filter((x) => x.breaking),
"New Features": commits.filter((x) => x.type === "feat"),
Fixes: commits.filter((x) => x.type === "fix"),
})

if (import.meta.main) {
const [latestTag] = await getTags()
const commits = await getCommits({ from: latestTag, to: "main" }).then(parseCommits)
if (commits.length === 0) {
console.log("No new commits")
Deno.exit(0)
}

const option = {
version: getNextVersion(latestTag, commits),
date: new Date().toISOString().split("T")[0],
sections: getSections(commits),
}

const files = {
bbcode: renderBBCode,
md: renderMarkdown,
"sts.txt": renderStS,
}

const changelogPath = `${import.meta.dirname}/../docs/changelog`

const writes = Object.entries(files)
.map(([ext, render]) => [ext, render(option)])
.map(([ext, text]) => {
if (ext === "md") console.log(text)
return Deno.writeTextFile(`${changelogPath}/changelog.${ext}`, text)
})
.concat(Deno.writeTextFile(`${changelogPath}/version.txt`, option.version.replace("v", "")))

await Promise.all(writes)

switch (Deno.args[0]) {
case "nextversion": {
await $`git tag ${option.version}`
console.log(await getTags())
}
}
}
18 changes: 18 additions & 0 deletions changelog/render.ts
@@ -0,0 +1,18 @@
import { Commit, Sections } from "./mod.ts"

export type ChangelogRenderer = (option: {
version: string
date: string
sections: Sections
}) => string

export type SectionFormatter = (tup: [section: string, commits: Commit[]]) => string
export type SectionsFormatter = { fmtSection: SectionFormatter; sections: Sections }

export const renderSections = (
{ fmtSection, sections }: SectionsFormatter,
) =>
Object.entries(sections)
.filter(([, commits]) => commits.length > 0)
.map(fmtSection)
.join("\n\n")
17 changes: 17 additions & 0 deletions changelog/render_bbcode.ts
@@ -0,0 +1,17 @@
import { outdent } from "$dax/src/deps.ts"
import { Commit } from "./mod.ts"
import { ChangelogRenderer, renderSections, SectionFormatter } from "./render.ts"

const fmtCommit = (x: Commit) =>
` [*] ${x.subject} ([url=https://github.com/scarf005/marisa/pull/${x.pr}]#${x.pr}[/url])`
.replace(/`([^`]+)`/g, "[i]$1[/i]")

const fmtSection: SectionFormatter = ([section, commits]) =>
`[h2]${section}[/h2]\n` + "[list]\n" + commits.map(fmtCommit).join("\n") + "\n[/list]"

export const renderBBCode: ChangelogRenderer = ({ version, date, sections }) =>
outdent`
[h1]${version} (${date})[/h1]
${renderSections({ fmtSection, sections })}
`
28 changes: 28 additions & 0 deletions changelog/render_bbcode_test.ts
@@ -0,0 +1,28 @@
import { assertEquals } from "$std/assert/assert_equals.ts"
import { outdent } from "$dax/src/deps.ts"
import { renderBBCode } from "./render_bbcode.ts"
import { exampleSections } from "./changelog_test.ts"

Deno.test("renderBBCode() outputs identical text", () =>
assertEquals(
renderBBCode({ version: "v1.0.0", date: "2024-03-12", sections: exampleSections }),
outdent`
[h1]v1.0.0 (2024-03-12)[/h1]
[h2]Breaking Changes[/h2]
[list]
[*] test ([url=https://github.com/scarf005/marisa/pull/123]#123[/url])
[/list]
[h2]New Features[/h2]
[list]
[*] asdf ([url=https://github.com/scarf005/marisa/pull/1234]#1234[/url])
[*] test ([url=https://github.com/scarf005/marisa/pull/123]#123[/url])
[/list]
[h2]Fixes[/h2]
[list]
[*] [i]language[/i] stuff ([url=https://github.com/scarf005/marisa/pull/415]#415[/url])
[/list]
`,
))
16 changes: 16 additions & 0 deletions changelog/render_markdown.ts
@@ -0,0 +1,16 @@
import { outdent } from "$dax/src/deps.ts"
import { Commit } from "./mod.ts"
import { ChangelogRenderer, renderSections, SectionFormatter } from "./render.ts"

const fmtCommit = (x: Commit) =>
`- ${x.subject} (#${x.pr})`

const fmtSection: SectionFormatter = ([section, commits]) =>
`## ${section}\n\n` + commits.map(fmtCommit).join("\n")

export const renderMarkdown: ChangelogRenderer = ({ version, date, sections }) =>
outdent`
# ${version} (${date})
${renderSections({ fmtSection, sections })}
`
21 changes: 21 additions & 0 deletions changelog/render_sts.ts
@@ -0,0 +1,21 @@
import { outdent } from "$dax/src/deps.ts"
import { Commit } from "./mod.ts"
import { ChangelogRenderer, renderSections, SectionFormatter } from "./render.ts"

export const fmtCommit = (x: Commit) =>
`- ${x.subject}`
.replace(/`([^`]+)`/g, "[$1]")
.replace(/\(#(\d+)\)/g, "")
.trim()

const fmtSection: SectionFormatter = ([section, commits]) =>
`* ${section}\n\n` + commits.map(fmtCommit).join("\n")

export const renderStS: ChangelogRenderer = ({ version, date, sections }) =>
outdent`
What's new in ${version} (${date})
${renderSections({ fmtSection, sections })}
please visit https://github.com/scarf005/Marisa/releases/latest for more info
`
8 changes: 8 additions & 0 deletions changelog/render_sts_test.ts
@@ -0,0 +1,8 @@
import { assertEquals } from "$std/assert/assert_equals.ts"
import { Commit } from "./mod.ts"
import { fmtCommit } from "./render_sts.ts"

Deno.test(
"fmtCommit() strips invalid characters",
() => assertEquals(fmtCommit({ subject: "`foo` (#123)" } as unknown as Commit), "- [foo]"),
)
17 changes: 17 additions & 0 deletions changelog/semver.ts
@@ -0,0 +1,17 @@
import type { SemVer } from "$std/semver/types.ts"
import type { Commit } from "./mod.ts"

import { format, increment, parse } from "$std/semver/mod.ts"

const increaseBy = (commit: Commit): "major" | "minor" | "patch" =>
commit.breaking ? "major" : commit.type === "feat" ? "minor" : "patch"

export const increaseVersion = (begin: SemVer, commits: Commit[]): SemVer =>
commits.reduce(({ ver, count }, commit) => {
const by = increaseBy(commit)
count[by]++
return (count[by] > 1) ? { ver, count } : { ver: increment(ver, increaseBy(commit)), count }
}, { ver: begin, count: { major: 0, minor: 0, patch: 0 } }).ver

export const getNextVersion = (latest: string, commits: Commit[]): string =>
"v" + format(increaseVersion(parse(latest), commits))
35 changes: 35 additions & 0 deletions changelog/semver_test.ts
@@ -0,0 +1,35 @@
import { assertObjectMatch } from "$std/assert/assert_object_match.ts"
import { assertEquals } from "$std/assert/assert_equals.ts"
import { getNextVersion, increaseVersion } from "./semver.ts"

const major = { type: "fix", breaking: "!", subject: "big stuff", pr: "123", scopes: undefined }
const minor = { type: "feat", breaking: undefined, subject: "feat", pr: "124", scopes: undefined }
const patch = { type: "fix", breaking: undefined, subject: "fix", pr: "125", scopes: undefined }

Deno.test("increaseVersion() only increases biggest change", async (t) => {
await t.step("major", () => {
assertObjectMatch(
increaseVersion({ major: 1, minor: 0, patch: 0 }, [
minor,
patch,
major,
]),
{ major: 2, minor: 0, patch: 0 },
)
})

await t.step("minor", () => {
assertObjectMatch(
increaseVersion({ major: 1, minor: 0, patch: 0 }, [patch, minor]),
{ major: 1, minor: 1, patch: 0 },
)
})
})

Deno.test("nextVersion() returns correct version", () => {
assertEquals(getNextVersion("v1.0.0", [minor, patch]), "v1.1.1")
})

Deno.test("nextVersion() handles multiple patch in single release", () => {
assertEquals(getNextVersion("v1.0.0", [patch, patch, patch]), "v1.0.1")
})
3 changes: 2 additions & 1 deletion deno.jsonc
@@ -1,7 +1,8 @@
{
"tasks": {
"l10n": "deno task --cwd localization/",
"link": "deno run --allow-read --allow-write --allow-env ./scripts/link/mod.ts"
"link": "deno run --allow-read --allow-write --allow-env ./link/mod.ts",
"changelog": "deno run -A ./changelog/mod.ts"
},
"exclude": ["src/main/kotlin/", "src/main/resources/marisa/img", "build/"],
"fmt": {
Expand Down
32 changes: 32 additions & 0 deletions deno.lock
Expand Up @@ -268,6 +268,35 @@
"https://deno.land/std@0.219.0/path/windows/resolve.ts": "8dae1dadfed9d46ff46cc337c9525c0c7d959fb400a6308f34595c45bdca1972",
"https://deno.land/std@0.219.0/path/windows/to_file_url.ts": "40e560ee4854fe5a3d4d12976cef2f4e8914125c81b11f1108e127934ced502e",
"https://deno.land/std@0.219.0/path/windows/to_namespaced_path.ts": "4ffa4fb6fae321448d5fe810b3ca741d84df4d7897e61ee29be961a6aac89a4c",
"https://deno.land/std@0.219.0/semver/_constants.ts": "5ef89c5f33e6095546ae3e57920592feefcb8372d4cc05542f6bf15a1977e3c9",
"https://deno.land/std@0.219.0/semver/_shared.ts": "5c53a675225cba9ad74ae2e17c124e333728fc2b551a13e8a32b99433b90c1c2",
"https://deno.land/std@0.219.0/semver/can_parse.ts": "d4a26f74be078f3ab10293b07bf022021a2f362b3e21b58422c214e7268110b2",
"https://deno.land/std@0.219.0/semver/compare.ts": "e8871844a35cc8fe16e883c16e5237e06a93aa4830ae10d06501abe63586fc57",
"https://deno.land/std@0.219.0/semver/constants.ts": "a0daa58502949654af044928f86288d8b27bd1880218e9faba7733ec0bde63ab",
"https://deno.land/std@0.219.0/semver/difference.ts": "be4f01b7745406408a16b708185a48c1c652cc87e0244b12a5ca75c5585db668",
"https://deno.land/std@0.219.0/semver/equals.ts": "8b9b18260c9a55feee9d3f9250fba345be922380f2e8f8009e455c394ce5e81d",
"https://deno.land/std@0.219.0/semver/format.ts": "26d3a357ac5abd73dee0fe7dbbac6107fbdce0a844370c7b1bcb673c92e46bf6",
"https://deno.land/std@0.219.0/semver/format_range.ts": "6ad2d0c27aac63dfb7efca6286a6ab7742accfb986cc53662047740f17dacfe5",
"https://deno.land/std@0.219.0/semver/greater_or_equal.ts": "89c26f68070896944676eb9704cbb617febc6ed693720282741d6859c3d1fe80",
"https://deno.land/std@0.219.0/semver/greater_than.ts": "d8c4a227cd28ea80a1de9c80215d7f3f95786fe1b196f0cb5ec91d6567adad27",
"https://deno.land/std@0.219.0/semver/increment.ts": "427a043be71d6481e45c1a3939b955e800924d70779cb297b872d9cbf9f0e46d",
"https://deno.land/std@0.219.0/semver/is_range.ts": "15dd9a8d6a8dee56dea6799d8c8210e06c0a38cc1a9aa6152aeea39ce45e2111",
"https://deno.land/std@0.219.0/semver/is_semver.ts": "57914027d6141e593eb04418aaabbfd6f4562a1c53c6c33a1743fa50ada8d849",
"https://deno.land/std@0.219.0/semver/less_or_equal.ts": "7dbf8190f37f3281048c30cf11e072a7af18685534ae88d295baa170b485bd90",
"https://deno.land/std@0.219.0/semver/less_than.ts": "b0c7902c54cecadcc7c1c80afc2f6a0f1bf0b3f53c8d2bfd11f01a3a414cccfe",
"https://deno.land/std@0.219.0/semver/max_satisfying.ts": "03e5182a7424c308ddbb410e4b927da0dabc4e07d4b5a72f7e9b26fb18a02152",
"https://deno.land/std@0.219.0/semver/min_satisfying.ts": "b6fadc9af17278289481c416e1eb135614f88063f4fc2b7b72b43eb3baa2f08f",
"https://deno.land/std@0.219.0/semver/mod.ts": "6e1f8854cec50c027037a597d3dd54af72e063f763ec0cbc4ea1e534a627ffae",
"https://deno.land/std@0.219.0/semver/not_equals.ts": "17147a6f68b9d14f4643c1e2150378ccf6954710309f9618f75b411752a8e13d",
"https://deno.land/std@0.219.0/semver/parse.ts": "b64052ff8ce0b0bba9ed97b835a224c828fc7ad227585c3e8c2bac72a07bf572",
"https://deno.land/std@0.219.0/semver/parse_range.ts": "5190afffc90cc14410ce2478fb81ed83b55d7e96b6fbbd69fc715c457082ec94",
"https://deno.land/std@0.219.0/semver/range_intersects.ts": "2c358f7c27b51960a9889be5462ec1cac44feeb5e40041a5c5a03700c0ddc017",
"https://deno.land/std@0.219.0/semver/range_max.ts": "4c43d018841ba67d018e515f0aef32658dab611a39fdc74e31b1e48709be281c",
"https://deno.land/std@0.219.0/semver/range_min.ts": "37c5e3dac7bd63812ae249add9ea815a351826f510d2baf391c225d1d8272d17",
"https://deno.land/std@0.219.0/semver/test_range.ts": "72ba2af827e4ad94db9a29e22e86cbec9b3f8519fc36fd6ce0d4308717536c70",
"https://deno.land/std@0.219.0/semver/try_parse.ts": "7e2a3594212445d9d6f6154f02288d66a0c0b79ce3e859c41f3d47e29dfa439a",
"https://deno.land/std@0.219.0/semver/try_parse_range.ts": "4f211f0ff4f5fdaa85622ab96c360123bbcf0e5a91a57eb7a8258af6b7a3c704",
"https://deno.land/std@0.219.0/semver/types.ts": "13e1e0c64a4ac76f0add74afee6240c92a7dba184e63e0bd4cb456afed8c7291",
"https://deno.land/std@0.219.0/testing/asserts.ts": "0cb9c745d9b157bed062a4aa8647168d2221f6456c385a548b0ca24de9e0f3ca",
"https://deno.land/x/cliffy@v1.0.0-rc.3/_utils/distance.ts": "02af166952c7c358ac83beae397aa2fbca4ad630aecfcd38d92edb1ea429f004",
"https://deno.land/x/cliffy@v1.0.0-rc.3/command/_argument_types.ts": "ab269dacea2030f865a07c2a1e953ec437a64419a05bad1f1ddaab3f99752ead",
Expand Down Expand Up @@ -410,6 +439,9 @@
"https://deno.land/x/jszip@0.11.0/mod.ts": "5661ddc18e9ac9c07e3c5d2483bc912a7022b6af0d784bb7b05035973e640ba1",
"https://deno.land/x/jszip@0.11.0/types.ts": "1528d1279fbb64dd118c371331c641a3a5eff2b594336fb38a7659cf4c53b2d1",
"https://deno.land/x/promise_object@v0.10.0/index.ts": "095e9fed1c0948110c8ded200814b051ac105ec2315b7ffc2c8ab51250d5a82b",
"https://deno.land/x/typed_regex@0.2.0/api_types.ts": "27a3f41894244da95b60646962231a3b02c4ed55bf7042747c737869fe3d4574",
"https://deno.land/x/typed_regex@0.2.0/mod.ts": "2a51f6fca3f6cabe28d420680c4bc6609704e3cc833ba987efe2c44c911330bb",
"https://deno.land/x/typed_regex@0.2.0/type_parser.ts": "a265790f94234a5338c7854af3794bcb012015f7c3e1c091ebf7b160b1627d31",
"https://deno.land/x/which@0.3.0/mod.ts": "3e10d07953c14e4ddc809742a3447cef14202cdfe9be6678a1dfc8769c4487e6",
"https://deno.land/x/zod@v3.20.5/ZodError.ts": "10bb0d014b0ece532c3bc395c50ae25996315a5897c0216517d9174c2fb570b5",
"https://deno.land/x/zod@v3.20.5/errors.ts": "5285922d2be9700cc0c70c95e4858952b07ae193aa0224be3cbd5cd5567eabef",
Expand Down
3 changes: 2 additions & 1 deletion import_map.json
Expand Up @@ -2,6 +2,7 @@
"imports": {
"$std/": "https://deno.land/std@0.219.0/",
"$cliffy/": "https://deno.land/x/cliffy@v1.0.0-rc.3/",
"$zod/": "https://deno.land/x/zod@v3.20.5/"
"$zod/": "https://deno.land/x/zod@v3.20.5/",
"$dax/": "https://deno.land/x/dax@0.39.2/"
}
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
4 changes: 2 additions & 2 deletions releases.ts
@@ -1,6 +1,6 @@
import { join } from "$std/path/join.ts"
import $ from "https://deno.land/x/dax@0.39.2/mod.ts"
import { steam as SteamPath } from "./scripts/paths.ts"
import $ from "$dax/mod.ts"
import { steam as SteamPath } from "./paths.ts"

export const basePath = "docs/changelog"

Expand Down
2 changes: 1 addition & 1 deletion releases_test.ts
@@ -1,6 +1,6 @@
import { join } from "$std/path/join.ts"
import { basePath, changelogPath, jarPath, stsPath, version } from "./releases.ts"
import { verifyHardLink } from "./scripts/link/mod.ts"
import { verifyHardLink } from "./link/mod.ts"
import { assertEquals } from "$std/assert/assert_equals.ts"
import { assertStringIncludes } from "$std/assert/assert_string_includes.ts"
import { readZip } from "https://deno.land/x/jszip@0.11.0/mod.ts"
Expand Down

0 comments on commit 9b965b9

Please sign in to comment.