Skip to content

Commit cb8842c

Browse files
author
Timo Notheisen
committed
test: add tests and refactoring
1 parent f60c290 commit cb8842c

6 files changed

Lines changed: 212 additions & 56 deletions

File tree

.github/workflows/test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ jobs:
1313
node-version: current
1414
- run: npm ci
1515
- run: npm run lint
16+
- run: npm test

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@
66
"lint": "npm run lint:prettier && npm run lint:tsc",
77
"lint:prettier": "prettier --check .",
88
"lint:tsc": "tsc -p release-dependency-updates/tsconfig.json --noEmit",
9+
"test": "node --import tsx --test release-dependency-updates/src/*.test.ts",
910
"release-dependency-updates": "node --import tsx release-dependency-updates/src/index.ts"
1011
},
1112
"dependencies": {
1213
"@actions/core": "^3.0.1",
1314
"@actions/github": "^9.1.1"
1415
},
1516
"devDependencies": {
17+
"@octokit/plugin-rest-endpoint-methods": "^17.0.0",
1618
"@types/node": "^25.9.1",
1719
"prettier": "^3.8.3",
1820
"tsx": "^4.21.0",

release-dependency-updates/src/index.ts

Lines changed: 17 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,15 @@
11
import * as core from "@actions/core"
22
import * as github from "@actions/github"
3+
import type { RestEndpointMethodTypes } from "@octokit/plugin-rest-endpoint-methods"
4+
5+
import { checkReleaseCommits, type ReleaseCheckCommit, type ReleaseCommit } from "./release-check.js"
36

47
type Octokit = ReturnType<typeof github.getOctokit>
5-
type CompareResponse = Awaited<ReturnType<Octokit["rest"]["repos"]["compareCommitsWithBasehead"]>>["data"]
6-
type ListedCommit = Awaited<ReturnType<Octokit["rest"]["repos"]["listCommits"]>>["data"][number]
7-
type ReleaseCommit = CompareResponse["commits"][number] | ListedCommit
8-
type LatestRelease = Awaited<ReturnType<Octokit["rest"]["repos"]["getLatestRelease"]>>["data"]
8+
type LatestRelease = RestEndpointMethodTypes["repos"]["getLatestRelease"]["response"]["data"]
99

1010
const initialVersion = "0.1.0"
1111
const patchVersionPattern = /^(v?)(\d+)\.(\d+)\.(\d+)$/
1212

13-
const botAuthorLogins = new Set(["renovate[bot]", "dependabot[bot]"])
14-
const botAuthorNames = new Set(["renovate bot", "renovate[bot]", "dependabot[bot]", "dependabot"])
15-
const dependencyPullRequestLabels = new Set(["dependencies"])
16-
const maintenancePullRequestLabels = new Set(["test", "chore", "refactoring", "ci"])
17-
const botEmailFragments = [
18-
"renovate[bot]@users.noreply.github.com",
19-
"bot@renovateapp.com",
20-
"dependabot[bot]@users.noreply.github.com",
21-
"dependabot@github.com"
22-
]
23-
2413
run().catch((error: unknown) => {
2514
core.setFailed(error instanceof Error ? error.message : String(error))
2615
})
@@ -44,7 +33,7 @@ async function run() {
4433
}
4534

4635
const { commitCount, commits, nextTag, revisionDescription } = releasePreparation
47-
const { blockingCommits, hasDependencyUpdateCommit } = await checkReleaseCommits(octokit, owner, repo, commits)
36+
const { blockingCommits, hasDependencyUpdateCommit } = await getReleaseCheck(octokit, owner, repo, commits)
4837
if (blockingCommits.length > 0) {
4938
const commitList = blockingCommits
5039
.slice(0, 10)
@@ -223,49 +212,25 @@ async function listCommits(octokit: Octokit, owner: string, repo: string, branch
223212
})
224213
}
225214

226-
async function checkReleaseCommits(octokit: Octokit, owner: string, repo: string, commits: ReleaseCommit[]) {
227-
const blockingCommits = []
228-
let hasDependencyUpdateCommit = false
229-
230-
for (const commit of commits) {
231-
const pullRequestLabels = await getAssociatedPullRequestLabels(octokit, owner, repo, commit)
232-
233-
if (isDependencyUpdateCommit(commit, pullRequestLabels)) {
234-
hasDependencyUpdateCommit = true
235-
continue
236-
}
237-
238-
if (isMaintenanceCommit(pullRequestLabels)) {
239-
continue
240-
}
241-
242-
blockingCommits.push(commit)
243-
}
215+
async function getReleaseCheck(octokit: Octokit, owner: string, repo: string, commits: ReleaseCommit[]) {
216+
const releaseCheckCommits = await getReleaseCheckCommits(octokit, owner, repo, commits)
217+
const { blockingCommits, hasDependencyUpdateCommit } = checkReleaseCommits(releaseCheckCommits)
244218

245219
return { blockingCommits, hasDependencyUpdateCommit }
246220
}
247221

248-
function isDependencyUpdateCommit(commit: ReleaseCommit, pullRequestLabels: string[]) {
249-
return isDependencyBotCommit(commit) || hasPullRequestLabel(pullRequestLabels, dependencyPullRequestLabels)
250-
}
251-
252-
function isMaintenanceCommit(pullRequestLabels: string[]) {
253-
return hasPullRequestLabel(pullRequestLabels, maintenancePullRequestLabels)
254-
}
255-
256-
function isDependencyBotCommit(commit: ReleaseCommit) {
257-
const login = commit.author?.login.toLowerCase()
258-
if (login && botAuthorLogins.has(login)) {
259-
return true
260-
}
222+
async function getReleaseCheckCommits(octokit: Octokit, owner: string, repo: string, commits: ReleaseCommit[]) {
223+
const releaseCheckCommits: Array<ReleaseCommit & ReleaseCheckCommit> = []
261224

262-
const authorName = commit.commit.author?.name?.toLowerCase()
263-
if (authorName && botAuthorNames.has(authorName)) {
264-
return true
225+
for (const commit of commits) {
226+
const pullRequestLabels = await getAssociatedPullRequestLabels(octokit, owner, repo, commit)
227+
releaseCheckCommits.push({
228+
...commit,
229+
pullRequestLabels
230+
})
265231
}
266232

267-
const authorEmail = commit.commit.author?.email?.toLowerCase()
268-
return Boolean(authorEmail && botEmailFragments.some((fragment) => authorEmail.includes(fragment)))
233+
return releaseCheckCommits
269234
}
270235

271236
async function getAssociatedPullRequestLabels(octokit: Octokit, owner: string, repo: string, commit: ReleaseCommit) {
@@ -278,10 +243,6 @@ async function getAssociatedPullRequestLabels(octokit: Octokit, owner: string, r
278243
return pullRequests.flatMap((pullRequest) => pullRequest.labels.map((label) => label.name.toLowerCase()))
279244
}
280245

281-
function hasPullRequestLabel(labelNames: string[], matchingLabels: Set<string>) {
282-
return labelNames.some((labelName) => matchingLabels.has(labelName))
283-
}
284-
285246
async function tagExists(octokit: Octokit, owner: string, repo: string, tag: string) {
286247
try {
287248
await octokit.rest.git.getRef({ owner, repo, ref: `tags/${tag}` })
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import assert from "node:assert/strict"
2+
import { describe, it } from "node:test"
3+
4+
import { checkReleaseCommits, type ReleaseCheckCommit } from "./release-check.js"
5+
6+
let commitId = 0
7+
8+
function commit(options: { authorEmail?: string; authorLogin?: string; authorName?: string; labels?: string[] } = {}) {
9+
return {
10+
author: options.authorLogin ? { login: options.authorLogin } : null,
11+
commit: {
12+
author: {
13+
email: options.authorEmail,
14+
name: options.authorName
15+
},
16+
id: String(commitId++)
17+
},
18+
pullRequestLabels: options.labels ?? []
19+
} as unknown as ReleaseCheckCommit
20+
}
21+
22+
describe("checkReleaseCommits", () => {
23+
it("creates a release for a dependency bot commit", () => {
24+
const result = checkReleaseCommits([commit({ authorLogin: "renovate[bot]" })])
25+
26+
assert.equal(result.shouldCreateRelease, true)
27+
assert.equal(result.hasDependencyUpdateCommit, true)
28+
assert.deepEqual(result.blockingCommits, [])
29+
})
30+
31+
it("creates a release for dependency bot author names", () => {
32+
const result = checkReleaseCommits([commit({ authorName: "dependabot" })])
33+
34+
assert.equal(result.shouldCreateRelease, true)
35+
assert.equal(result.hasDependencyUpdateCommit, true)
36+
assert.deepEqual(result.blockingCommits, [])
37+
})
38+
39+
it("creates a release for dependency bot author emails", () => {
40+
const result = checkReleaseCommits([commit({ authorEmail: "renovate[bot]@users.noreply.github.com" })])
41+
42+
assert.equal(result.shouldCreateRelease, true)
43+
assert.equal(result.hasDependencyUpdateCommit, true)
44+
assert.deepEqual(result.blockingCommits, [])
45+
})
46+
47+
it("creates a release for a commit with a dependencies pull request label", () => {
48+
const result = checkReleaseCommits([commit({ labels: ["dependencies"] })])
49+
50+
assert.equal(result.shouldCreateRelease, true)
51+
assert.equal(result.hasDependencyUpdateCommit, true)
52+
assert.deepEqual(result.blockingCommits, [])
53+
})
54+
55+
it("matches pull request labels case-insensitively", () => {
56+
const result = checkReleaseCommits([commit({ labels: ["Dependencies"] }), commit({ labels: ["CI"] })])
57+
58+
assert.equal(result.shouldCreateRelease, true)
59+
assert.equal(result.hasDependencyUpdateCommit, true)
60+
assert.deepEqual(result.blockingCommits, [])
61+
})
62+
63+
it("creates a release for dependency update commits with maintenance commits", () => {
64+
const result = checkReleaseCommits([
65+
commit({ labels: ["dependencies"] }),
66+
commit({ labels: ["test"] }),
67+
commit({ labels: ["chore"] }),
68+
commit({ labels: ["refactoring"] }),
69+
commit({ labels: ["ci"] })
70+
])
71+
72+
assert.equal(result.shouldCreateRelease, true)
73+
assert.equal(result.hasDependencyUpdateCommit, true)
74+
assert.deepEqual(result.blockingCommits, [])
75+
})
76+
77+
it("does not create a release when there are no commits", () => {
78+
const result = checkReleaseCommits([])
79+
80+
assert.equal(result.shouldCreateRelease, false)
81+
assert.equal(result.hasDependencyUpdateCommit, false)
82+
assert.deepEqual(result.blockingCommits, [])
83+
})
84+
85+
it("does not create a release for only maintenance commits", () => {
86+
const result = checkReleaseCommits([
87+
commit({ labels: ["test"] }),
88+
commit({ labels: ["chore"] }),
89+
commit({ labels: ["refactoring"] }),
90+
commit({ labels: ["ci"] })
91+
])
92+
93+
assert.equal(result.shouldCreateRelease, false)
94+
assert.equal(result.hasDependencyUpdateCommit, false)
95+
assert.deepEqual(result.blockingCommits, [])
96+
})
97+
98+
it("does not create a release for only blocking commits", () => {
99+
const commits = [commit(), commit({ labels: ["feature"] })]
100+
const result = checkReleaseCommits(commits)
101+
102+
assert.equal(result.shouldCreateRelease, false)
103+
assert.equal(result.hasDependencyUpdateCommit, false)
104+
assert.deepEqual(result.blockingCommits, commits)
105+
})
106+
107+
it("does not create a release when dependency updates are mixed with blocking commits", () => {
108+
const commits = [
109+
commit({ authorLogin: "dependabot[bot]" }),
110+
commit({ labels: ["feature"] }),
111+
commit({ labels: ["ci"] }),
112+
commit()
113+
]
114+
const result = checkReleaseCommits(commits)
115+
116+
assert.equal(result.shouldCreateRelease, false)
117+
assert.equal(result.hasDependencyUpdateCommit, true)
118+
assert.deepEqual(result.blockingCommits, [commits[1], commits[3]])
119+
})
120+
})
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import type { RestEndpointMethodTypes } from "@octokit/plugin-rest-endpoint-methods"
2+
3+
type CompareCommit =
4+
RestEndpointMethodTypes["repos"]["compareCommitsWithBasehead"]["response"]["data"]["commits"][number]
5+
type ListedCommit = RestEndpointMethodTypes["repos"]["listCommits"]["response"]["data"][number]
6+
7+
export type ReleaseCommit = CompareCommit | ListedCommit
8+
export type ReleaseCheckCommit = {
9+
pullRequestLabels: readonly string[]
10+
} & ReleaseCommit
11+
12+
const botAuthorLogins = new Set(["renovate[bot]", "dependabot[bot]"])
13+
const botAuthorNames = new Set(["renovate bot", "renovate[bot]", "dependabot[bot]", "dependabot"])
14+
const botEmailFragments = [
15+
"renovate[bot]@users.noreply.github.com",
16+
"bot@renovateapp.com",
17+
"dependabot[bot]@users.noreply.github.com",
18+
"dependabot@github.com"
19+
]
20+
21+
export function checkReleaseCommits<TCommit extends ReleaseCheckCommit>(commits: readonly TCommit[]) {
22+
const blockingCommits = []
23+
let hasDependencyUpdateCommit = false
24+
25+
for (const commit of commits) {
26+
if (isDependencyUpdateCommit(commit)) {
27+
hasDependencyUpdateCommit = true
28+
continue
29+
}
30+
31+
if (isMaintenanceCommit(commit)) {
32+
continue
33+
}
34+
35+
blockingCommits.push(commit)
36+
}
37+
38+
return {
39+
blockingCommits,
40+
hasDependencyUpdateCommit,
41+
shouldCreateRelease: hasDependencyUpdateCommit && blockingCommits.length === 0
42+
}
43+
}
44+
45+
function isDependencyUpdateCommit(commit: ReleaseCheckCommit) {
46+
return isDependencyBotCommit(commit) || hasPullRequestLabel(commit.pullRequestLabels, ["dependencies"])
47+
}
48+
49+
function isMaintenanceCommit(commit: ReleaseCheckCommit) {
50+
return hasPullRequestLabel(commit.pullRequestLabels, ["test", "chore", "refactoring", "ci"])
51+
}
52+
53+
function isDependencyBotCommit(commit: ReleaseCheckCommit) {
54+
const login = commit.author?.login?.toLowerCase()
55+
if (login && botAuthorLogins.has(login)) {
56+
return true
57+
}
58+
59+
const authorName = commit.commit.author?.name?.toLowerCase()
60+
if (authorName && botAuthorNames.has(authorName)) {
61+
return true
62+
}
63+
64+
const authorEmail = commit.commit.author?.email?.toLowerCase()
65+
return Boolean(authorEmail && botEmailFragments.some((fragment) => authorEmail.includes(fragment)))
66+
}
67+
68+
function hasPullRequestLabel(labelNames: readonly string[], matchingLabels: readonly string[]) {
69+
const normalizedLabels = new Set(labelNames.map((labelName) => labelName.toLowerCase()))
70+
return matchingLabels.some((labelName) => normalizedLabels.has(labelName))
71+
}

0 commit comments

Comments
 (0)