Skip to content

Commit

Permalink
In markdown, group by type, then by module (#16)
Browse files Browse the repository at this point in the history
* explicit failing on unknown type for valid changes

* regroup changes: type -> module

* add module name to change entry

* remove linebreak fix

* embed auto-links to PR in malformed section

* add test case with big note
  • Loading branch information
shvgn authored Feb 8, 2022
1 parent 0f7c115 commit 7c42ecd
Show file tree
Hide file tree
Showing 2 changed files with 124 additions and 98 deletions.
85 changes: 32 additions & 53 deletions src/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function groupByModuleAndType(acc: ChangesByModule, change: ChangeEntry) {
list = getTypeList("features")
break
default:
throw new Error("invalid type")
throw new Error("invalid type: " + change.type)
}

// add the change
Expand All @@ -58,8 +58,8 @@ function groupByModuleAndType(acc: ChangesByModule, change: ChangeEntry) {
return acc
}

const MARKDOWN_HEADER_TAG = "h2"
const MARKDOWN_MODULE_TAG = "h4"
const MARKDOWN_HEADER_TAG = "h1"
const MARKDOWN_TYPE_TAG = "h2"
const MARKDOWN_NOTE_PREFIX = "**NOTE!**"

/**
Expand All @@ -71,91 +71,70 @@ export function formatMarkdown(milestone: string, changes: ChangeEntry[]): strin
const body: DataObject[] = [
{ [MARKDOWN_HEADER_TAG]: `Changelog ${milestone}` }, // title
...formatMalformedEntries(changes),
...formatEntriesByModuleAndType(changes),
...formatFeatureEntries(changes),
...formatFixEntries(changes),
]

const md = json2md(body)

// Workaround to omit excessive empty lines
// https://github.com/IonicaBizau/json2md/issues/53
return fixLineBreaks(md)
return md
}

function fixLineBreaks(md: string): string {
const fixed = md
.split("\n")
// remove empty lines
.filter((s) => s.trim() != "")
// wrap subheaders with empty lines
.map((s) => (s.startsWith("###") ? `\n${s}\n` : s))
.map((s) => (s.startsWith("**") && s.endsWith("**") ? `\n${s}\n` : s))
.join("\n")

// add empty line to the end
return fixed + "\n"
function formatFeatureEntries(changes: ChangeEntry[]): DataObject[] {
return formatEntries(changes, "feature", "Features")
}

function formatEntriesByModuleAndType(changes: ChangeEntry[]): DataObject[] {
const body: DataObject[] = []
function formatFixEntries(changes: ChangeEntry[]): DataObject[] {
return formatEntries(changes, "fix", "Fixes")
}

const validEntries = changes
.filter((c) => c.valid()) //
.reduce(groupByModuleAndType, {})
function formatEntries(changes: ChangeEntry[], changeType: string, subHeader: string): DataObject[] {
const filtered = changes
.filter((c) => c.valid() && c.type == changeType) //
.sort((a, b) => (a.module < b.module ? -1 : 1)) // sort by module

// Collect valid change entries; sort by module name
const pairs = Object.entries(validEntries).sort((a, b) => (a[0] < b[0] ? -1 : 1))
for (const [modName, changes] of pairs) {
body.push({ [MARKDOWN_MODULE_TAG]: modName })
body.push(...moduleChangesMarkdown(changes))
const body: DataObject[] = []
if (filtered.length === 0) {
return body
}

body.push({ [MARKDOWN_TYPE_TAG]: subHeader })
body.push({ ul: filtered.map(changeMardown) })

return body
}

function formatMalformedEntries(changes: ChangeEntry[]): DataObject[] {
const body: DataObject[] = []

// Collect malformed on the top for easier fixing
const invalidEntries = changes
const malformed = changes
.filter((c) => !c.valid())
.sort((a, b) => (a.pull_request < b.pull_request ? -1 : 1))
.map((c) => parsePullRequestNumberFromURL(c.pull_request))
.map((x) => parseInt(x))
.sort()

if (invalidEntries.length > 0) {
body.push([{ [MARKDOWN_MODULE_TAG]: "[MALFORMED]" }])
if (malformed.length > 0) {
body.push([{ [MARKDOWN_TYPE_TAG]: "[MALFORMED]" }])

const ul: string[] = []
for (const c of invalidEntries) {
const prNum = parsePullRequestNumberFromURL(c.pull_request)
ul.push(`[#${prNum}](${c.pull_request})`)
for (const num of malformed) {
ul.push(`#${num}`)
}
body.push({ ul: ul.sort() })
}

return body
}

function moduleChangesMarkdown(moduleChanges: ModuleChanges): DataObject[] {
const md: DataObject[] = []
if (moduleChanges.features) {
md.push({ p: "**features**" })
md.push({ ul: moduleChanges.features.flatMap(changeMardown) })
}
if (moduleChanges.fixes) {
md.push({ p: "**fixes**" })
md.push({ ul: moduleChanges.fixes.flatMap(changeMardown) })
}
// console.log("mc", JSON.stringify(md, null, 2))
return md
}

function parsePullRequestNumberFromURL(prUrl: string): string {
const parts = prUrl.split("/")
return parts[parts.length - 1]
}

function changeMardown(c: Change): string {
const pr = parsePullRequestNumberFromURL(c.pull_request)
const lines = [`${c.description} [#${pr}](${pr})`]
function changeMardown(c: ChangeEntry): string {
const prNum = parsePullRequestNumberFromURL(c.pull_request)
const lines = [`**[${c.module}]** ${c.description} [#${prNum}](${c.pull_request})`]

if (c.note) {
lines.push(`${MARKDOWN_NOTE_PREFIX} ${c.note}`)
Expand Down
137 changes: 92 additions & 45 deletions test/format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,90 @@ import { formatMarkdown, formatYaml } from "../src/format"
import { ChangeEntry } from "../src/parse"

const changes: ChangeEntry[] = [
new ChangeEntry({ module: "yyy", type: "", description: "dm2", pull_request: "prm2" }),
new ChangeEntry({ module: "two", type: "fix", description: "d21", pull_request: "pr21", note: "x" }),
new ChangeEntry({ module: "one", type: "feature", description: "d12", pull_request: "pr12" }),
new ChangeEntry({ module: "two", type: "feature", description: "d22", pull_request: "pr22" }),
new ChangeEntry({ module: "one", type: "fix", description: "d11", pull_request: "pr11" }),
new ChangeEntry({ module: "xxx", type: "", description: "dm1", pull_request: "prm1" }),
new ChangeEntry({ module: "two", type: "fix", description: "d28", pull_request: "pr28" }),
new ChangeEntry({ module: "two", type: "fix", description: "d29", pull_request: "pr29" }),
new ChangeEntry({
module: "yyy",
type: "",
description: "dm2",
pull_request: "https://github.com/ow/re/533",
}),
new ChangeEntry({
module: "cloud-provider-yandex",
type: "fix",
description: "d21",
pull_request: "https://github.com/ow/re/210",
note: `Grafana will be restarted.
Now grafana using direct (proxy) type for deckhouse datasources (main, longterm, uncached), because direct(browse) datasources type is depreated now. And alerts don't work with direct data sources.
Provisioning datasources from secret instead configmap. Deckhouse datasources need client certificates to connect to prometheus or trickter. Old cm leave to prevent mount error while terminating.`,
}),
new ChangeEntry({
module: "chrony",
type: "feature",
description: "d12",
pull_request: "https://github.com/ow/re/120",
}),
new ChangeEntry({
module: "cloud-provider-yandex",
type: "feature",
description: "d22",
pull_request: "https://github.com/ow/re/220",
}),
new ChangeEntry({
module: "chrony",
type: "fix",
description: "d11",
pull_request: "https://github.com/ow/re/110",
}),
new ChangeEntry({
module: "xxx",
type: "",
description: "dm1",
pull_request: "https://github.com/ow/re/510",
}),
new ChangeEntry({
module: "kube-dns",
type: "fix",
description: "d48",
pull_request: "https://github.com/ow/re/480",
}),
new ChangeEntry({
module: "cloud-provider-yandex",
type: "fix",
description: "d29",
pull_request: "https://github.com/ow/re/290",
}),
]

describe("YAML", () => {
const expected = `one:
const expected = `chrony:
features:
- description: d12
pull_request: pr12
pull_request: https://github.com/ow/re/120
fixes:
- description: d11
pull_request: pr11
two:
pull_request: https://github.com/ow/re/110
cloud-provider-yandex:
features:
- description: d22
pull_request: pr22
pull_request: https://github.com/ow/re/220
fixes:
- description: d21
note: x
pull_request: pr21
- description: d28
pull_request: pr28
note: >-
Grafana will be restarted.
Now grafana using direct (proxy) type for deckhouse datasources (main, longterm, uncached),
because direct(browse) datasources type is depreated now. And alerts don't work with direct
data sources.
Provisioning datasources from secret instead configmap. Deckhouse datasources need client
certificates to connect to prometheus or trickter. Old cm leave to prevent mount error while
terminating.
pull_request: https://github.com/ow/re/210
- description: d29
pull_request: pr29
pull_request: https://github.com/ow/re/290
kube-dns:
fixes:
- description: d48
pull_request: https://github.com/ow/re/480
`
test("formats right", () => {
expect(formatYaml(changes)).toEqual(expected)
Expand All @@ -44,50 +98,43 @@ describe("Markdown", () => {

// This markdown formatting is implementation-dependant. The test only check that everything
// is in place.
const expected = `## Changelog v3.44.555
#### [MALFORMED]
- [#prm1](prm1)
- [#prm2](prm2)
#### one
**features**
const expected = `# Changelog v3.44.555
- d12 [#pr12](pr12)
## [MALFORMED]
**fixes**
- d11 [#pr11](pr11)
- #510
- #533
#### two
## Features
**features**
- **[chrony]** d12 [#120](https://github.com/ow/re/120)
- **[cloud-provider-yandex]** d22 [#220](https://github.com/ow/re/220)
- d22 [#pr22](pr22)
## Fixes
**fixes**
- d21 [#pr21](pr21)
**NOTE!** x
- d28 [#pr28](pr28)
- d29 [#pr29](pr29)
- **[chrony]** d11 [#110](https://github.com/ow/re/110)
- **[cloud-provider-yandex]** d21 [#210](https://github.com/ow/re/210)
**NOTE!** Grafana will be restarted.
Now grafana using direct (proxy) type for deckhouse datasources (main, longterm, uncached), because direct(browse) datasources type is depreated now. And alerts don't work with direct data sources.
Provisioning datasources from secret instead configmap. Deckhouse datasources need client certificates to connect to prometheus or trickter. Old cm leave to prevent mount error while terminating.
- **[cloud-provider-yandex]** d29 [#290](https://github.com/ow/re/290)
- **[kube-dns]** d48 [#480](https://github.com/ow/re/480)
`
test("has milestone header as h2", () => {
test("has chrony title as h1", () => {
const firstLine = md.split("\n")[0].trim()
expect(firstLine).toBe(`## Changelog v3.44.555`)
expect(firstLine).toBe(`# Changelog v3.44.555`)
})

test("formats module name as h4", () => {
test("formats type name as h2", () => {
const subheaders = md
.split("\n")
.map((s) => s.trim())
.filter((s) => s.startsWith("###"))
.filter((s) => s.startsWith("## "))

expect(subheaders).toStrictEqual(["#### [MALFORMED]", "#### one", "#### two"])
expect(subheaders).toStrictEqual(["## [MALFORMED]", "## Features", "## Fixes"])
})

test("formats right", () => {
Expand Down

0 comments on commit 7c42ecd

Please sign in to comment.