Skip to content

Commit e648f45

Browse files
committed
feat(github-release): a github release addon for simple-release
1 parent 17606de commit e648f45

File tree

8 files changed

+496
-0
lines changed

8 files changed

+496
-0
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"extends": [
3+
"@trigen/eslint-config/typescript",
4+
"@trigen/eslint-config/typescript-requiring-type-checking",
5+
"@trigen/eslint-config/jest"
6+
],
7+
"parserOptions": {
8+
"tsconfigRootDir": "./packages/github-release",
9+
"project": ["./tsconfig.json"]
10+
},
11+
"rules": {
12+
"@typescript-eslint/naming-convention": "off",
13+
"prefer-destructuring": "off"
14+
}
15+
}

packages/github-release/README.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# @simple-release/github-release
2+
3+
[![ESM-only package][package]][package-url]
4+
[![NPM version][npm]][npm-url]
5+
[![Node version][node]][node-url]
6+
[![Dependencies status][deps]][deps-url]
7+
[![Install size][size]][size-url]
8+
[![Build status][build]][build-url]
9+
[![Coverage status][coverage]][coverage-url]
10+
11+
[package]: https://img.shields.io/badge/package-ESM--only-ffe536.svg
12+
[package-url]: https://nodejs.org/api/esm.html
13+
14+
[npm]: https://img.shields.io/npm/v/@simple-release/github-release.svg
15+
[npm-url]: https://www.npmjs.com/package/@simple-release/github-release
16+
17+
[node]: https://img.shields.io/node/v/@simple-release/github-release.svg
18+
[node-url]: https://nodejs.org
19+
20+
[deps]: https://img.shields.io/librariesio/release/npm/@simple-release/github-release
21+
[deps-url]: https://libraries.io/npm/@simple-release%2Fcore/tree
22+
23+
[size]: https://packagephobia.com/badge?p=@simple-release/github-release
24+
[size-url]: https://packagephobia.com/result?p=@simple-release/github-release
25+
26+
[build]: https://img.shields.io/github/actions/workflow/status/TrigenSoftware/simple-release-tools/tests.yml?branch=main
27+
[build-url]: https://github.com/TrigenSoftware/simple-release-tools/actions
28+
29+
[coverage]: https://coveralls.io/repos/github/TrigenSoftware/simple-release-tools/badge.svg?branch=main
30+
[coverage-url]: https://coveralls.io/github/TrigenSoftware/simple-release-tools?branch=main
31+
32+
A github release addon for simple-release.
33+
34+
## Install
35+
36+
```bash
37+
# pnpm
38+
pnpm add @simple-release/github-release
39+
# yarn
40+
yarn add @simple-release/github-release
41+
# npm
42+
npm i @simple-release/github-release
43+
```
44+
45+
## Usage
46+
47+
```js
48+
import { Releaser } from '@simple-release/core'
49+
import { PnpmProject } from '@simple-release/pnpm'
50+
import { GithubReleaseCreator } from '@simple-release/github-release'
51+
52+
const project = new PnpmProject()
53+
54+
await new Releaser(project)
55+
.bump()
56+
.commit()
57+
.tag()
58+
.push()
59+
.publish()
60+
.release(new GithubReleaseCreator({
61+
token: process.env.GITHUB_TOKEN
62+
}))
63+
.run()
64+
```
65+
66+
## Options
67+
68+
### `token`
69+
70+
The GitHub personal access token to authenticate with the GitHub API.
71+
72+
### `owner`
73+
74+
The GitHub owner (username or organization) of the repository. If not provided, it will be inferred from the remote URL.
75+
76+
### `project`
77+
78+
The GitHub project (repository name) to create releases in. If not provided, it will be inferred from the remote URL.
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
{
2+
"name": "@simple-release/github-release",
3+
"type": "module",
4+
"version": "1.0.0",
5+
"description": "A github release addon for simple-release.",
6+
"author": {
7+
"name": "Dan Onoshko",
8+
"email": "[email protected]",
9+
"url": "https://github.com/dangreen"
10+
},
11+
"license": "MIT",
12+
"homepage": "https://github.com/TrigenSoftware/simple-release-tools/tree/master/packages/github-release#readme",
13+
"funding": "https://ko-fi.com/dangreen",
14+
"repository": {
15+
"type": "git",
16+
"url": "https://github.com/TrigenSoftware/simple-release-tools.git",
17+
"directory": "packages/github-release"
18+
},
19+
"bugs": {
20+
"url": "https://github.com/TrigenSoftware/simple-release-tools/issues"
21+
},
22+
"keywords": [
23+
"simple-release",
24+
"github",
25+
"release",
26+
"changelog"
27+
],
28+
"engines": {
29+
"node": ">=18"
30+
},
31+
"exports": "./src/index.ts",
32+
"publishConfig": {
33+
"exports": {
34+
"types": "./dist/index.d.ts",
35+
"import": "./dist/index.js"
36+
},
37+
"directory": "package",
38+
"linkDirectory": false
39+
},
40+
"files": [
41+
"dist"
42+
],
43+
"scripts": {
44+
"clear:package": "del ./package",
45+
"clear:dist": "del ./dist",
46+
"clear": "del ./package ./dist ./coverage",
47+
"prepublishOnly": "run build clear:package clean-publish",
48+
"postpublish": "pnpm clear:package",
49+
"build": "tsc -p tsconfig.build.json",
50+
"lint": "eslint --parser-options tsconfigRootDir:. '**/*.{js,ts}'",
51+
"test:unit": "vitest run --coverage",
52+
"test:types": "tsc --noEmit",
53+
"test": "run -p lint test:unit test:types"
54+
},
55+
"dependencies": {
56+
"@octokit/rest": "^21.1.1",
57+
"@simple-libs/hosted-git-info": "^1.0.0",
58+
"@simple-release/core": "workspace:^"
59+
},
60+
"devDependencies": {
61+
"test": "workspace:^"
62+
}
63+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { join } from 'path'
2+
import {
3+
describe,
4+
it,
5+
expect
6+
} from 'vitest'
7+
import { packageJsonProject } from 'test'
8+
import {
9+
type LoggerMessage,
10+
PackageJsonProject,
11+
Logger
12+
} from '@simple-release/core'
13+
import { GithubReleaseCreator } from './index.js'
14+
15+
describe('github-release', () => {
16+
describe('GithubReleaseCreator', () => {
17+
it('should run smoke test', async () => {
18+
const path = await packageJsonProject()
19+
const project = new PackageJsonProject({
20+
path: join(path, 'package.json')
21+
})
22+
const log: LoggerMessage[] = []
23+
const logger = new Logger({
24+
verbose: true,
25+
printer(message) {
26+
log.push(message)
27+
}
28+
})
29+
const publusher = new GithubReleaseCreator({
30+
token: ''
31+
})
32+
33+
await publusher.create({
34+
project,
35+
dryRun: true,
36+
logger: logger.createChild('release')
37+
})
38+
39+
const message = log[0].message
40+
const json = message.slice(message.indexOf('{'))
41+
const release = JSON.parse(json)
42+
43+
expect(release).toEqual({
44+
owner: 'TrigenSoftware',
45+
repo: 'test-repo',
46+
tag_name: 'v2.0.0',
47+
name: 'v2.0.0',
48+
body: 'RELEASE NOTES',
49+
prerelease: false
50+
})
51+
})
52+
})
53+
})
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { Octokit } from '@octokit/rest'
2+
import { parseHostedGitUrl } from '@simple-libs/hosted-git-info'
3+
import {
4+
type GenericReleaseCreatorOptions,
5+
GenericReleaseCreator
6+
} from '@simple-release/core'
7+
8+
export interface GithubReleaseCreatorOptions {
9+
/**
10+
* The GitHub personal access token to authenticate with the GitHub API.
11+
*/
12+
token: string
13+
/**
14+
* The GitHub owner (username or organization) of the repository.
15+
* If not provided, it will be inferred from the remote URL.
16+
*/
17+
owner?: string
18+
/**
19+
* The GitHub project (repository name) to create releases in.
20+
* If not provided, it will be inferred from the remote URL.
21+
*/
22+
project?: string
23+
}
24+
25+
const OK = 200
26+
const noop = () => { /* no-op */ }
27+
28+
/**
29+
* A class that creates releases on GitHub using the GitHub REST API.
30+
*/
31+
export class GithubReleaseCreator extends GenericReleaseCreator {
32+
readonly octokit: Octokit
33+
34+
/**
35+
* Creates a new instance of the GitHub release creator.
36+
* @param options - The options for the GitHub release creator.
37+
*/
38+
constructor(
39+
private readonly options: GithubReleaseCreatorOptions
40+
) {
41+
super()
42+
43+
this.octokit = new Octokit({
44+
auth: options.token,
45+
log: {
46+
debug: noop,
47+
info: noop,
48+
warn: noop,
49+
error: noop
50+
}
51+
})
52+
}
53+
54+
private async hasRelease(
55+
owner: string,
56+
project: string,
57+
tag: string
58+
): Promise<boolean> {
59+
try {
60+
const { status } = await this.octokit.rest.repos.getReleaseByTag({
61+
owner,
62+
repo: project,
63+
tag
64+
})
65+
66+
return status === OK
67+
} catch {
68+
return false
69+
}
70+
}
71+
72+
async create(options: GenericReleaseCreatorOptions): Promise<void> {
73+
const {
74+
project,
75+
dryRun,
76+
logger
77+
} = options
78+
let {
79+
owner: githubOwner,
80+
project: githubProject
81+
} = this.options
82+
const { octokit } = this
83+
const data = await project.getReleaseData()
84+
85+
if (!githubOwner || !githubProject) {
86+
const remote = await project.gitClient.getConfig('remote.origin.url')
87+
const repo = parseHostedGitUrl(remote)
88+
89+
if (repo) {
90+
if (!githubOwner) {
91+
githubOwner = repo.owner
92+
}
93+
94+
if (!githubProject) {
95+
githubProject = repo.project
96+
}
97+
}
98+
}
99+
100+
for (const releaseData of data) {
101+
if (await this.hasRelease(githubOwner!, githubProject!, releaseData.nextTag)) {
102+
logger?.verbose(`Release ${releaseData.nextTag} already exists.`)
103+
continue
104+
}
105+
106+
const params = {
107+
owner: githubOwner!,
108+
repo: githubProject!,
109+
tag_name: releaseData.nextTag,
110+
name: releaseData.title,
111+
body: releaseData.notes,
112+
prerelease: releaseData.isPrerelease
113+
}
114+
115+
logger?.verbose(`Creating release with params:\n${JSON.stringify(params, null, 2)}`)
116+
117+
if (!dryRun) {
118+
await octokit.rest.repos.createRelease(params)
119+
}
120+
}
121+
}
122+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"extends": "../../tsconfig.json",
3+
"compilerOptions": {
4+
"outDir": "dist"
5+
},
6+
"include": [
7+
"src"
8+
],
9+
"exclude": [
10+
"**/*.spec.ts"
11+
]
12+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"extends": "./tsconfig.build.json",
3+
"include": [
4+
"src"
5+
],
6+
"exclude": []
7+
}

0 commit comments

Comments
 (0)