Skip to content
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
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@
"dependencies": {
"@open-draft/until": "^2.0.0",
"@types/conventional-commits-parser": "^3.0.2",
"@types/issue-parser": "^3.0.1",
"@types/node": "^16.11.27",
"@types/node-fetch": "2.x",
"@types/semver": "^7.3.9",
"@types/yargs": "^17.0.10",
"conventional-commits-parser": "^3.2.4",
"get-stream": "^6.0.1",
"git-log-parser": "^1.2.0",
"issue-parser": "^6.0.0",
"node-fetch": "2.6.7",
"outvariant": "^1.3.0",
"pino": "^7.10.0",
Expand Down
42 changes: 37 additions & 5 deletions src/commands/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ import { createTag } from '../utils/git/createTag'
import { getReleaseNotes, toMarkdown } from '../utils/getReleaseNotes'
import { createRelease } from '../utils/git/createRelease'
import { push } from '../utils/git/push'
import { getReleaseRefs } from '../utils/getReleaseRefs'
import { parseCommits } from '../utils/git/parseCommits'
import { createComment } from '../utils/github/createComment'
import { createReleaseComment } from '../utils/createReleaseComment'

export class Publish extends Command {
static command = 'publish'
Expand Down Expand Up @@ -50,7 +54,7 @@ export class Publish extends Command {

const commits = await getCommits({
after: latestRelease?.hash,
})
}).then(parseCommits)

if (commits.length === 0) {
log.warn('no commits since the latest release, skipping...')
Expand All @@ -59,7 +63,7 @@ export class Publish extends Command {

log.info('found %d new commit(s):', commits.length)
for (const commit of commits) {
log.info('- %s (%s)', commit.subject, commit.hash)
log.info('- %s (%s)', commit.header, commit.hash)
}

// Get the next release type and version number.
Expand Down Expand Up @@ -108,14 +112,15 @@ export class Publish extends Command {
log.info(publishResult.data)
log.info('published successfully!')

// The queue of actions to invoke if releasing fails.
const revertQueue: Array<() => Promise<void>> = []

const result = await until(async () => {
// Create a release commit containing the version bump in package.json
const commitResult = await until(() => {
return commit({
files: ['package.json'],
message: `chore: publish ${context.nextRelease.tag}`,
message: `chore(release): ${context.nextRelease.tag}`,
})
})

Expand Down Expand Up @@ -183,6 +188,10 @@ export class Publish extends Command {
)

log.info('pushed changes to "%s" (origin)!', repo.remote)

return {
releaseUrl,
}
})

if (result.error) {
Expand All @@ -200,8 +209,31 @@ export class Publish extends Command {
}

log.error(result.error)
console.error(result.error)
process.exit(1)
throw result.error
}

// Comment on each relevant GitHub issue.
const issueIds = await getReleaseRefs(commits)
const releaseCommentText = createReleaseComment({
context,
releaseUrl: result.data.releaseUrl,
})

if (issueIds.size > 0) {
log.info('commenting on %d referenced issue(s)...', issueIds.size)

const commentPromises = []
for (const issueId of issueIds) {
commentPromises.push(
createComment(issueId, releaseCommentText).catch((error) => {
log.error('commenting on issue "%s" failed: %s', error.message)
})
)
}

await Promise.allSettled(commentPromises)
} else {
log.info('no referenced issues, nothing to comment')
}

log.info('release "%s" completed!', context.nextRelease.tag)
Expand Down
50 changes: 50 additions & 0 deletions src/utils/__test__/createReleaseComment.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { createReleaseComment } from '../createReleaseComment'
import { testEnvironment } from '../../../test/env'
import { mockRepo } from '../../../test/fixtures'
import { createContext } from '../createContext'

const { setup, reset, cleanup, fs } = testEnvironment('create-release-comment')

beforeAll(async () => {
await setup()
})

afterEach(async () => {
await reset()
})

afterAll(async () => {
await cleanup()
})

it('creates a release comment out of given release context', async () => {
await fs.create({
'package.json': JSON.stringify({
name: 'my-package',
}),
})

const comment = createReleaseComment({
context: createContext({
repo: mockRepo(),
nextRelease: {
version: '1.2.3',
publishedAt: new Date(),
},
}),
releaseUrl: '/releases/1',
})

expect(comment).toBe(`## Released: v1.2.3 🎉

This has been released in v1.2.3!

- 📄 [**Release notes**](/releases/1)
- 📦 [npm package](https://www.npmjs.com/package/my-package/v/1.2.3)

Make sure to always update to the latest version (\`npm i my-package@latest\`) to get the newest features and bug fixes.

---

_Predictable release automation by [@ossjs/release](https://github.com/ossjs/release)_.`)
})
67 changes: 19 additions & 48 deletions src/utils/__test__/getNextReleaseType.test.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,11 @@
import * as gitLogParser from 'git-log-parser'
import { mockParsedCommit } from '../../../test/fixtures'
import { getNextReleaseType } from '../getNextReleaseType'

function mockCommit(
initialValues: Partial<gitLogParser.Commit> = {}
): gitLogParser.Commit {
return {
commit: {
short: '',
long: '',
},
subject: '',
body: '',
hash: '',
tree: {
short: '',
long: '',
},
author: {
name: 'John Doe',
email: 'john@doe.com',
date: new Date(),
},
committer: {
name: 'John Doe',
email: 'john@doe.com',
date: new Date(),
},
...initialValues,
}
}

it('returns "major" for a commit that contains a "BREAKING CHANGE" footnote', () => {
expect(
getNextReleaseType([
mockCommit({
subject: 'fix: stuff',
mockParsedCommit({
header: 'fix: stuff',
body: 'BREAKING CHANGE: This is a breaking change.',
}),
])
Expand All @@ -44,19 +15,19 @@ it('returns "major" for a commit that contains a "BREAKING CHANGE" footnote', ()
it('returns "minor" for "feat" commits', () => {
expect(
getNextReleaseType([
mockCommit({
subject: 'feat: adds graphql support',
mockParsedCommit({
header: 'feat: adds graphql support',
}),
])
).toBe('minor')

expect(
getNextReleaseType([
mockCommit({
subject: 'feat: adds graphql support',
mockParsedCommit({
header: 'feat: adds graphql support',
}),
mockCommit({
subject: 'fix: fix stuff',
mockParsedCommit({
header: 'fix: fix stuff',
}),
])
).toBe('minor')
Expand All @@ -65,19 +36,19 @@ it('returns "minor" for "feat" commits', () => {
it('returns patch for "fix" commits', () => {
expect(
getNextReleaseType([
mockCommit({
subject: 'fix: return signature',
mockParsedCommit({
header: 'fix: return signature',
}),
])
).toBe('patch')

expect(
getNextReleaseType([
mockCommit({
subject: 'fix: return signature',
mockParsedCommit({
header: 'fix: return signature',
}),
mockCommit({
subject: 'docs: mention stuff',
mockParsedCommit({
header: 'docs: mention stuff',
}),
])
).toBe('patch')
Expand All @@ -86,11 +57,11 @@ it('returns patch for "fix" commits', () => {
it('returns null when no commits bump the version', () => {
expect(
getNextReleaseType([
mockCommit({
subject: 'chore: design better releases',
mockParsedCommit({
header: 'chore: design better releases',
}),
mockCommit({
subject: 'docs: mention cli arguments',
mockParsedCommit({
header: 'docs: mention cli arguments',
}),
])
).toBe(null)
Expand Down
62 changes: 33 additions & 29 deletions src/utils/__test__/getReleaseNotes.test.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import type { Commit } from 'git-log-parser'
import { getReleaseNotes, toMarkdown } from '../getReleaseNotes'
import { mockRepo } from '../../../test/fixtures'
import { mockCommit, mockRepo } from '../../../test/fixtures'
import { createContext } from '../createContext'
import { parseCommits } from '../git/parseCommits'

describe(getReleaseNotes, () => {
it('groups commits by commit type', async () => {
const notes = await getReleaseNotes([
{
const commits = await parseCommits([
mockCommit({
subject: 'feat: support graphql',
},
{
}),
mockCommit({
subject: 'fix(ui): remove unsupported styles',
},
{
}),
mockCommit({
subject: 'chore: update dependencies',
},
] as Commit[])
}),
])
const notes = await getReleaseNotes(commits)

expect(Array.from(notes.keys())).toEqual(['feat', 'fix'])

Expand All @@ -36,11 +37,12 @@ describe(getReleaseNotes, () => {
})

it('includes issues references', async () => {
const notes = await getReleaseNotes([
{
const commits = await parseCommits([
mockCommit({
subject: 'feat(api): improves stuff (#1)',
},
] as Commit[])
}),
])
const notes = await getReleaseNotes(commits)

expect(Array.from(notes.keys())).toEqual(['feat'])

Expand Down Expand Up @@ -70,37 +72,39 @@ describe(toMarkdown, () => {
})

it('includes both issue and commit reference', async () => {
const notes = await getReleaseNotes([
{
const commits = await parseCommits([
mockCommit({
hash: 'abc123',
subject: 'feat(api): improves stuff (#1)',
},
] as Commit[])

}),
])
const notes = await getReleaseNotes(commits)
const markdown = toMarkdown(context, notes)

expect(markdown).toContain('- **api:** improves stuff (#1) (abc123)')
})

it('retains strict order of release sections', async () => {
const notes = await getReleaseNotes([
{
const commits = await parseCommits([
mockCommit({
hash: 'abc123',
subject: 'fix: second bugfix',
},
{
}),
mockCommit({
hash: 'def456',
subject: 'fix: first bugfix',
},
{
}),
mockCommit({
hash: 'fgh789',
subject: 'feat: second feature',
},
{
}),
mockCommit({
hash: 'xyz321',
subject: 'feat: first feature',
},
] as Commit[])
}),
])

const notes = await getReleaseNotes(commits)
const markdown = toMarkdown(context, notes)

expect(markdown).toEqual(`\
Expand Down
Loading