Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate github release notes from changelog + tags #553

Merged
merged 6 commits into from
Jul 26, 2022
Merged
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
5 changes: 5 additions & 0 deletions .buildkite/pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ steps:
agents:
queue: v1
commands:
- npm config set "//registry.npmjs.org/:_authToken" $${NPM_TOKEN}
- echo "--- Install dependencies"
- PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 HUSKY=0 yarn install --immutable
- echo "+++ Run tests"
- yarn constraints
- yarn run test:scripts

- label: "[Browser] Lint + Test"
key: build
Expand Down
9 changes: 8 additions & 1 deletion .github/workflows/create-github-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,17 @@ jobs:
steps:
- name: Checkout Repo
uses: actions/checkout@v3
- name: Setup Node.js 12.x
uses: actions/setup-node@v3
with:
node-version: 12.x
cache: "yarn"
- name: Install Dependencies
run: HUSKY=0 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 yarn install --immutable
- name: Create Github Release From Tags
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git config --global user.name "Segment Github"
git config --global user.email "[email protected]"
bash scripts/create-release-from-tags.sh
yarn ts-node-script --files scripts/create-release-from-tags/run.ts
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
},
"scripts": {
"test": "turbo run test",
"test:scripts": "jest --config scripts/jest.config.js",
"lint": "yarn constraints && turbo run lint",
"build": "turbo run build",
"build:packages": "turbo run build --filter='./packages/*'",
Expand All @@ -34,13 +35,15 @@
"devDependencies": {
"@changesets/changelog-github": "^0.4.5",
"@changesets/cli": "^2.23.2",
"@npmcli/promise-spawn": "^3.0.0",
"@types/jest": "^28.1.1",
"@typescript-eslint/eslint-plugin": "^5.21.0",
"@typescript-eslint/parser": "^5.21.0",
"concurrently": "^7.2.1",
"eslint": "^8.14.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.0.0",
"get-monorepo-packages": "^1.2.0",
"husky": "^8.0.0",
"jest": "^28.1.0",
"lint-staged": "^13.0.0",
Expand Down
9 changes: 0 additions & 9 deletions scripts/create-release-from-tags.sh

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# @segment/analytics-core

## 1.99.0

### Minor Changes

* [#606](https://github.com/segmentio/analytics-next/pull/606) [\`b9c6356\`](https://github.com/segmentio/analytics-next/commit/b9c6356b7d35ee8acb6ecbd1eebc468d18d63958) Thanks [@silesky] - foo!)

### Patch Changes

* [#404](https://github.com/segmentio/analytics-next/pull/404) [\`b9abc6\`](https://github.com/segmentio/analytics-next/commit/b9c6356b7d35ee8acb6ecbd1eebc468d18d63958) Thanks [@silesky] - bar!)
17 changes: 17 additions & 0 deletions scripts/create-release-from-tags/__tests__/fixtures/reg-example.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# @segment/analytics-core

## 1.99.0

### Minor Changes

* [#606](https://github.com/segmentio/analytics-next/pull/606) [\`b9c6356\`](https://github.com/segmentio/analytics-next/commit/b9c6356b7d35ee8acb6ecbd1eebc468d18d63958) Thanks [@silesky] - foo!)

### Patch Changes

* [#404](https://github.com/segmentio/analytics-next/pull/404) [\`b9abc6\`](https://github.com/segmentio/analytics-next/commit/b9c6356b7d35ee8acb6ecbd1eebc468d18d63958) Thanks [@silesky] - bar!)

## 1.39.2

### Patch Changes

* [#513](https://github.com/segmentio/analytics-next/pull/513) [\`1d36ca1\`](https://github.com/segmentio/analytics-next/commit/1d36ca1440fc5df9171d16278d8918b3e5a32128) Thanks [@silesky](https://github.com/silesky)! - test
40 changes: 40 additions & 0 deletions scripts/create-release-from-tags/__tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { parseReleaseNotes } from '..'
import fs from 'fs'
import path from 'path'

const readFixture = (filename: string) => {
return fs.readFileSync(path.join(__dirname, 'fixtures', filename), {
encoding: 'utf8',
})
}

describe('parseReleaseNotes', () => {
test('should work with reg example', () => {
const fixture = readFixture('reg-example.md')
expect(parseReleaseNotes(fixture, '1.99.0')).toMatchInlineSnapshot(`
"
### Minor Changes

* [#606](https://github.com/segmentio/analytics-next/pull/606) [\\\\\`b9c6356\\\\\`](https://github.com/segmentio/analytics-next/commit/b9c6356b7d35ee8acb6ecbd1eebc468d18d63958) Thanks [@silesky] - foo!)

### Patch Changes

* [#404](https://github.com/segmentio/analytics-next/pull/404) [\\\\\`b9abc6\\\\\`](https://github.com/segmentio/analytics-next/commit/b9c6356b7d35ee8acb6ecbd1eebc468d18d63958) Thanks [@silesky] - bar!)
"
`)
})

test('should work if first release', () => {
const fixture = readFixture('first-release-example.md')
expect(parseReleaseNotes(fixture, '1.99.0')).toMatchInlineSnapshot(`
"
### Minor Changes

* [#606](https://github.com/segmentio/analytics-next/pull/606) [\\\\\`b9c6356\\\\\`](https://github.com/segmentio/analytics-next/commit/b9c6356b7d35ee8acb6ecbd1eebc468d18d63958) Thanks [@silesky] - foo!)

### Patch Changes

* [#404](https://github.com/segmentio/analytics-next/pull/404) [\\\\\`b9abc6\\\\\`](https://github.com/segmentio/analytics-next/commit/b9c6356b7d35ee8acb6ecbd1eebc468d18d63958) Thanks [@silesky] - bar!)"
`)
})
})
192 changes: 192 additions & 0 deletions scripts/create-release-from-tags/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import spawn from '@npmcli/promise-spawn'
import getPackages from 'get-monorepo-packages'
import path from 'path'
import fs from 'fs'
import { exists } from '../utils/exists'

export type Config = {
isDryRun: boolean
tags: Tag[]
}

export type Tag = {
name: string
versionNumber: string
raw: string
}

/**
*
* @returns list of tags
* @example ["@segment/[email protected]", "@segment/[email protected]"]
*/
export const getCurrentGitTags = async (): Promise<Tag[]> => {
const { stdout, stderr, code } = await spawn('git', [
'tag',
'--points-at',
'HEAD',
'--column',
])
if (code !== 0) {
throw new Error(stderr.toString())
}

return parseRawTags(stdout.toString())
}

export const getConfig = async ({
DRY_RUN,
TAGS,
}: NodeJS.ProcessEnv): Promise<Config> => {
const isDryRun = Boolean(DRY_RUN)
const tags = TAGS ? parseRawTags(TAGS) : await getCurrentGitTags()

if (!tags.length) {
throw new Error('No git tags found.')
}
return {
isDryRun,
tags,
}
}

const getChangelogPath = (packageName: string): string | undefined => {
const result = getPackages('.').find((p) =>
p.package.name.includes(packageName)
)
if (!result)
throw new Error(`could not find package with name: ${packageName}.`)

let changelogPath = undefined
for (const fileName of ['CHANGELOG.MD', 'CHANGELOG.md']) {
if (changelogPath) break
const myPath = path.join(result.location, fileName)
const pathExists = fs.existsSync(myPath)
if (pathExists) {
changelogPath = myPath
}
}

if (changelogPath) {
return changelogPath
} else {
console.log(`could not find changelog path for ${result.location}`)
}
}

/**
*
* @returns list of tags
* @example ["@segment/[email protected]", "@segment/[email protected]"]
*/
const createGithubRelease = async (
tag: string,
releaseNotes?: string
): Promise<void> => {
const { stderr, code } = await spawn('gh', [
'release',
'create',
tag,
'--title',
tag,
'--notes',
releaseNotes || '',
])
if (code !== 0) {
throw new Error(stderr.toString())
}
}

/**
*
* @param rawTag - ex. "@segment/[email protected]"
*/
const extractPartsFromTag = (rawTag: string): Tag | undefined => {
const [name, version] = rawTag.split(/@(\d.*)/)
if (!name || !version) return undefined
return {
name,
versionNumber: version?.replace('\n', '') as string,
raw: rawTag,
}
}

/**
*
* @param rawTags - string delimited list of tags (e.g. `@segment/[email protected] @segment/[email protected]`)
*/
export const parseRawTags = (rawTags: string): Tag[] => {
return rawTags.trim().split(' ').map(extractPartsFromTag).filter(exists)
}

/**
*
* @returns the release notes that correspond to a given tag.
*/
export const parseReleaseNotes = (
changelogText: string,
versionNumber: string
): string => {
const h2tag = /(##\s.*\d.*)/gi
let begin: number
let end: number

changelogText.split('\n').forEach((line, idx) => {
if (begin && end) return
if (line.includes(versionNumber)) {
begin = idx + 1
} else if (begin && h2tag.test(line)) {
end = idx - 1
}
})

const result = changelogText.split('\n').filter((_, idx) => {
return idx >= begin && idx <= (end ?? Infinity)
})
return result.join('\n')
}

const getReleaseNotes = (tag: Tag): string | undefined => {
const { name, versionNumber } = tag
const changelogPath = getChangelogPath(name)
if (!changelogPath) {
console.log(`no changelog path for ${name}... skipping.`)
return
}
const changelogText = fs.readFileSync(changelogPath, { encoding: 'utf8' })
const releaseNotes = parseReleaseNotes(changelogText, versionNumber)
if (!releaseNotes) {
console.log(
`Could not find release notes for tags ${tag.raw} in ${changelogPath}.`
)
}
return releaseNotes
}

const createGithubReleaseFromTag = async (
tag: Tag,
{ dryRun = false } = {}
): Promise<void> => {
const notes = getReleaseNotes(tag)
if (notes) {
console.log(
`\n ---> Outputting release titled: ${tag.raw} with notes: \n ${notes}`
)
}

if (dryRun) {
console.log(`--> Dry run: ${tag.raw} not released.`)
return undefined
}

await createGithubRelease(tag.raw, notes)
return undefined
}

export const createReleaseFromTags = async (config: Config) => {
console.log('Processing tags:', config.tags, '\n')

for (const tag of config.tags) {
await createGithubReleaseFromTag(tag, { dryRun: config.isDryRun })
}
}
8 changes: 8 additions & 0 deletions scripts/create-release-from-tags/run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createReleaseFromTags, getConfig } from '.'

async function run() {
const config = await getConfig(process.env)
return createReleaseFromTags(config)
}

void run()
10 changes: 10 additions & 0 deletions scripts/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ["**/?(*.)+(test).[jt]s?(x)"],
globals: {
'ts-jest': {
isolatedModules: true,
},
},
}
7 changes: 7 additions & 0 deletions scripts/utils/exists.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* This type guard can be passed into a function such as native filter
* in order to remove nullish values from a list in a type-safe way.
*/
export const exists = <T>(value: T): value is NonNullable<T> => {
return value != null && value !== undefined
}
9 changes: 9 additions & 0 deletions typings/get-monorepo-packages.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
declare module 'get-monorepo-packages' {
export default function getPackages(pathToRoot: string): {
location: string
package: {
name: string
version: string
}
}[]
}
10 changes: 10 additions & 0 deletions typings/spawn.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
declare module '@npmcli/promise-spawn' {
import { EventEmitter } from 'events'
import { SpawnOptions } from 'child_process'

export default function spawn(
cmd: string,
args?: string[],
opts?: SpawnOptions
): Promise<{ stdout: Buffer; code: number; stderr: Buffer }> & EventEmitter
}
Loading