Skip to content

Commit b3b62e4

Browse files
committed
fix(backport): do not push if there's no differences and fix error catch and replace ts-node by tsx
Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
1 parent d7e863f commit b3b62e4

File tree

9 files changed

+81
-43
lines changed

9 files changed

+81
-43
lines changed

.vscode/launch.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
"version": "1.0.0",
66
"configurations": [
77
{
8-
"name": "TS-Node",
8+
"name": "TSX Debug",
99
"type": "node",
1010
"request": "launch",
11-
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/ts-node",
11+
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/tsx",
1212
"program": "src/index.ts",
1313
"cwd": "${workspaceRoot}",
1414
"internalConsoleOptions": "openOnSessionStart",
1515
"skipFiles": ["<node_internals>/**", "node_modules/**"]
1616
}
1717
]
18-
}
18+
}

docker-compose.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
version: "3.7"
2+
3+
services:
4+
app:
5+
image: node:lts
6+
working_dir: "/app"
7+
command: sh -c "npm i && npm run serve"
8+
9+
ports:
10+
- "3000:3000"
11+
12+
environment:
13+
APP_ID: 12345
14+
WEBHOOK_SECRET: 123456789abcdefghijklmnop
15+
PRIVATE_KEY_PATH: /private-key.pem
16+
17+
volumes:
18+
- ./backportbot:/app
19+
- ./private-key.pem:/private-key.pem:ro

package.json

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"description": "A bot that backport commits to other branches",
55
"main": "index.ts",
66
"scripts": {
7-
"serve": "ts-node src/index.ts",
7+
"serve": "tsx src/index.ts",
88
"lint": "eslint src/**/*.ts",
99
"lint:fix": "eslint src/**/*.ts --fix"
1010
},
@@ -28,11 +28,10 @@
2828
"dependencies": {
2929
"@octokit/app": "^14.0.2",
3030
"@octokit/rest": "^20.0.2",
31-
"@octokit/webhooks": "^12.0.11",
32-
"octokit": "^3.1.2",
3331
"p-queue": "^8.0.1",
3432
"simple-git": "^3.22.0",
35-
"ts-node": "^10.9.2"
33+
"ts-node": "^10.9.2",
34+
"tsx": "^4.7.0"
3635
},
3736
"devDependencies": {
3837
"@tsconfig/recommended": "^1.0.3",
@@ -41,5 +40,9 @@
4140
"@typescript-eslint/parser": "^6.18.1",
4241
"eslint": "^8.56.0",
4342
"typescript": "^5.3.3"
43+
},
44+
"engines": {
45+
"node": "^20.0.0",
46+
"npm": "^9.0.0"
4447
}
4548
}

src/appUtils.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import { App } from '@octokit/app'
2-
import { join } from 'node:path'
32
import { readFileSync } from 'node:fs'
43

5-
import { APP_ID, PRIVATE_KEY_FILENAME, ROOT_DIR, WEBHOOK_SECRET } from './constants'
4+
import { APP_ID, PRIVATE_KEY_PATH, WEBHOOK_SECRET } from './constants'
65

76
const initApp = (): App => {
8-
const privateKey = readFileSync(join(ROOT_DIR, PRIVATE_KEY_FILENAME), 'utf-8').toString()
7+
const privateKey = readFileSync(PRIVATE_KEY_PATH, 'utf-8').toString()
98
return new App({
109
appId: APP_ID,
1110
privateKey,

src/backport.ts

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { existsSync, rmSync } from 'node:fs'
22
import { Octokit } from '@octokit/rest'
33

4-
import { cherryPickCommits, cloneAndCacheRepo, hasEmptyCommits, hasSkipCiCommits, pushBranch } from './gitUtils'
4+
import { cherryPickCommits, cloneAndCacheRepo, hasDiff, hasEmptyCommits, hasSkipCiCommits, pushBranch } from './gitUtils'
55
import { CherryPickResult, Task } from './constants'
66
import { debug, error, info, warn } from './logUtils'
77
import { Reaction, addReaction, getAuthToken, getAvailableLabels, getLabelsFromPR, getAvailableMilestones, requestReviewers, getReviewers, createBackportPullRequest, setPRLabels, setPRMilestone, getChangesFromPR, updatePRBody, commentOnPR, assignToPR } from './githubUtils'
@@ -27,8 +27,7 @@ export const backport = (task: Task) => new Promise<void>((resolve, reject) => {
2727
tmpDir = await cloneAndCacheRepo(task, backportBranch)
2828
info(task, `Cloned to ${tmpDir}`)
2929
} catch (e) {
30-
reject(`Failed to clone repository: ${e.message}`)
31-
throw e
30+
throw new Error(`Failed to clone repository: ${e.message}`)
3231
}
3332

3433
// Cherry pick the commits
@@ -40,17 +39,20 @@ export const backport = (task: Task) => new Promise<void>((resolve, reject) => {
4039
info(task, `Cherry picking commits successful`)
4140
}
4241
} catch (e) {
43-
reject(`Failed to cherry pick commits: ${e.message}`)
44-
throw e
42+
throw new Error(`Failed to cherry pick commits: ${e.message}`)
43+
}
44+
45+
// Check if there are any changes to backport
46+
if (!await hasDiff(tmpDir, task.branch, backportBranch)) {
47+
throw new Error(`No changes found in backport branch`)
4548
}
4649

4750
// Push the branch
4851
try {
4952
await pushBranch(task, tmpDir, token)
5053
info(task, `Pushed branch ${backportBranch}`)
5154
} catch (e) {
52-
reject(`Failed to push branch: ${e.message}`)
53-
throw e
55+
throw new Error(`Failed to push branch: ${e.message}`)
5456
}
5557

5658
// Create the pull request
@@ -71,11 +73,10 @@ export const backport = (task: Task) => new Promise<void>((resolve, reject) => {
7173
await requestReviewers(octokit, task, prNumber, [task.author])
7274
info(task, `Requested reviews from ${[...reviewers, task.author].join(', ')}`)
7375
} catch (e) {
74-
reject(`Failed to request reviews: ${e.message}`)
76+
throw new Error(`Failed to request reviews: ${e.message}`)
7577
}
7678
} catch (e) {
77-
reject(`Failed to create pull request: ${e.message}`)
78-
throw e
79+
throw new Error(`Failed to create pull request: ${e.message}`)
7980
}
8081

8182
// Get labels from original PR and set them on the new PR
@@ -86,7 +87,8 @@ export const backport = (task: Task) => new Promise<void>((resolve, reject) => {
8687
await setPRLabels(octokit, task, prNumber, labels)
8788
info(task, `Set labels: ${labels.join(', ')}`)
8889
} catch (e) {
89-
reject(`Failed to get labels: ${e.message}`)
90+
error(task, `Failed to get and set labels: ${e.message}`)
91+
// continue, this is not a fatal error
9092
}
9193

9294
// Find new appropriate Milestone and set it on the new PR
@@ -96,15 +98,17 @@ export const backport = (task: Task) => new Promise<void>((resolve, reject) => {
9698
await setPRMilestone(octokit, task, prNumber, milestone)
9799
info(task, `Set milestone: ${milestone.title}`)
98100
} catch (e) {
99-
warn(task, `Failed to find appropriate milestone: ${e.message}`)
101+
error(task, `Failed to find appropriate milestone: ${e.message}`)
102+
// continue, this is not a fatal error
100103
}
101104

102105
// Assign the PR to the author of the original PR
103106
try {
104107
await assignToPR(octokit, task, prNumber, [task.author])
105108
info(task, `Assigned original author: ${task.author}`)
106109
} catch (e) {
107-
reject(`Failed to assign PR: ${e.message}`)
110+
error(task, `Failed to assign PR: ${e.message}`)
111+
// continue, this is not a fatal error
108112
}
109113

110114
// Compare the original PR with the new PR
@@ -125,10 +129,12 @@ export const backport = (task: Task) => new Promise<void>((resolve, reject) => {
125129
await updatePRBody(octokit, task, prNumber, newBody)
126130
}
127131
} catch (e) {
128-
reject(`Failed to update PR body: ${e.message}`)
132+
error(task, `Failed to update PR body: ${e.message}`)
133+
// continue, this is not a fatal error
129134
}
130135
} catch (e) {
131-
reject(`Failed to compare changes: ${e.message}`)
136+
error(task, `Failed to compare changes: ${e.message}`)
137+
// continue, this is not a fatal error
132138
}
133139

134140
// Success! We're done here
@@ -140,8 +146,11 @@ export const backport = (task: Task) => new Promise<void>((resolve, reject) => {
140146
const failureComment = getFailureCommentBody(task, backportBranch, e?.message)
141147
await commentOnPR(octokit, task, failureComment)
142148
} catch (e) {
143-
reject(`Failed to comment failure on PR: ${e.message}`)
149+
error(task, `Failed to comment failure on PR: ${e.message}`)
150+
// continue, this is not a fatal error
144151
}
152+
153+
reject(`Failed to backport: ${e.message}`)
145154
}
146155

147156
// Remove the temp dir if it exists

src/constants.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,20 @@
1-
import { resolve } from 'node:path'
1+
import { join, resolve } from 'node:path'
22

33
export const SERVE_PORT = 8123
44
export const SERVE_HOST = '0.0.0.0'
55

6-
7-
export const APP_ID = process.env.APP_ID
8-
export const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET
9-
export const PRIVATE_KEY_FILENAME = 'private-key.pem'
10-
116
export const ROOT_DIR = resolve(__dirname + '/../')
127
export const CACHE_DIRNAME = 'cache' // relative to the root dir
138
export const WORK_DIRNAME = 'work' // relative to the root dir
149

10+
export const APP_ID = process.env.APP_ID || 0
11+
export const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || ''
12+
export const PRIVATE_KEY_FILENAME = 'private-key.pem'
13+
export const PRIVATE_KEY_PATH = process.env.PRIVATE_KEY_PATH || join(ROOT_DIR, PRIVATE_KEY_FILENAME)
14+
1515
export const LOG_FILE = 'backport.log'
16-
export const QUEUE_FILE = 'queue.json'
1716

18-
export const COMMAND_PREFIX = '/skjnldsv-backport'
17+
export const COMMAND_PREFIX = '/backport'
1918
export const TO_SEPARATOR = ' to '
2019
export const COMMIT_REGEX = /^\b[0-9a-f]{7,40}$\b/i
2120
export const BRANCH_REGEX = /^\b[a-z0-9-_./]{1,100}\b$/i

src/gitUtils.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,16 +156,19 @@ export const hasSkipCiCommits = async (repoRoot: string, commits: number): Promi
156156
return commitMessages.some(message => message.includes('[skip ci]'))
157157
}
158158

159+
export const hasDiff = async (repoRoot: string, base: string, head: string): Promise<boolean> => {
160+
const git = simpleGit(repoRoot)
161+
const diff = await git.raw(['diff', '--stats', base, head])
162+
return diff !== ''
163+
}
159164

160165
export const hasEmptyCommits = async (repoRoot: string, commits: number): Promise<boolean> => {
161-
const git = simpleGit(repoRoot)
162166
let hasEmptyCommits = false
163167
for (let count = 0; count < commits; count++) {
164-
const diff = await git.raw(['diff', '--stat', `HEAD~${count}`, `HEAD~${count + 1}`])
165-
if (diff === '') {
168+
if (!await hasDiff(repoRoot, `HEAD~${count}`, `HEAD~${count + 1}`)) {
166169
hasEmptyCommits = true
167170
break
168171
}
169172
}
170173
return hasEmptyCommits
171-
}
174+
}

src/index.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { info, error } from 'node:console'
44
import { Octokit } from '@octokit/rest'
55

66
import { addToQueue } from './queue'
7-
import { CACHE_DIRNAME, COMMAND_PREFIX, LABEL_BACKPORT, ROOT_DIR, SERVE_HOST, SERVE_PORT, TO_SEPARATOR, Task } from './constants'
7+
import { CACHE_DIRNAME, COMMAND_PREFIX, LABEL_BACKPORT, PRIVATE_KEY_PATH, ROOT_DIR, SERVE_HOST, SERVE_PORT, TO_SEPARATOR, Task, WEBHOOK_SECRET } from './constants'
88
import { extractBranchFromPayload, extractCommitsFromPayload } from './payloadUtils'
99
import { getApp } from './appUtils'
1010
import { Reaction, addPRLabel, addReaction, getAuthToken, getBackportRequestsFromPR, getCommitsForPR, removePRLabel, setPRLabels } from './githubUtils'
@@ -227,14 +227,17 @@ app.webhooks.on(['issue_comment.created'], async ({ payload }) => {
227227

228228
// If the PR is already merged, we can start the backport right away
229229
if (isMerged) {
230-
addToQueue(task).then(async () => {
230+
try {
231+
await addToQueue(task)
231232
// Remove the backport label from the PR on success
232233
try {
233234
await removePRLabel(authOctokit, task, prNumber, LABEL_BACKPORT)
234235
} catch (e) {
235236
error(`\nFailed to remove backport label from PR ${htmlUrl}: ${e.message}`)
236237
}
237-
})
238+
} catch (e) {
239+
// Safely ignore
240+
}
238241
}
239242
} catch (e) {
240243
// This should really not happen, but if it does, we want to know about it
@@ -256,11 +259,14 @@ Subscribed events: ${data.events}`)
256259
process.exit(1)
257260
}
258261

262+
const obfuscatedWebhookSecret = WEBHOOK_SECRET.slice(0, 8) + '*'.repeat(WEBHOOK_SECRET.length - 8)
259263
info(`Listening on ${SERVE_HOST}:${SERVE_PORT}`)
260264
info(`├ Authenticated as ${data.name}`)
261265
info(`├ Monitoring events`, data.events)
262266
info(`├ Command prefix: ${COMMAND_PREFIX}`)
263267
info(`├ Root dir: ${ROOT_DIR}`)
264-
info(`└ Cache dir: ${ROOT_DIR}/${CACHE_DIRNAME}`)
268+
info(`├ Cache dir: ${ROOT_DIR}/${CACHE_DIRNAME}`)
269+
info(`├ Private key in ${PRIVATE_KEY_PATH}`)
270+
info(`└ Webhook secret is ${obfuscatedWebhookSecret}`)
265271
createServer(createNodeMiddleware(app.webhooks)).listen(SERVE_PORT, SERVE_HOST)
266272
})

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"extends": "@tsconfig/recommended",
3-
"include": ["src/**/*"],
3+
"include": ["src/**/*.ts"],
44
"exclude": ["node_modules"],
55
"compilerOptions": {
66
"allowJs": true

0 commit comments

Comments
 (0)