implemented issueBranchName
nclazz/gitea-bot/pipeline/head This commit looks good Details

master
Niclas Thobaben 2022-07-09 00:43:40 +02:00
parent 79e604f832
commit f133114bcf
9 changed files with 274 additions and 2 deletions

5
Jenkinsfile vendored
View File

@ -4,6 +4,7 @@ pipeline {
environment {
NEXUS = credentials('jenkins_nexus')
NCLAZZ = credentials('nclazz_api_token')
GITEA_TOKEN = credentials('jenkins_gitea_token')
DOCKER_REGISTRY = "docker.nclazz.de"
DOCKER_GROUP = 'nclazz-bots'
@ -46,7 +47,9 @@ pipeline {
env: [
PORT: 7007,
VERSION: env.DOCKER_VERSION,
AUTH_TOKEN: env.NCLAZZ
AUTH_TOKEN: env.NCLAZZ,
GITEA_TOKEN: env.GITEA_TOKEN,
GITEA_BASE_URL: 'https://git.l--n.de/api/v1'
]
)
exposeService(

View File

@ -1,4 +1,22 @@
Gitea Bot
============================
This bot/server manages issues in gitea repositories.
This bot/server manages issues in gitea repositories.
## Webhooks
The bot provides some useful webhooks. Webhooks can either be enabled opt-in by
listing the required webhooks in the query-parameter `hooks` as a comma separated list
or enable all hooks by leaving the hooks parameter empty.
Send gitea events to `https://gitea-bot.nclazz.de/webhook`
### issueBranchName
Updates the branch name in the issues body. The name is created from the title,
issue number and optionally a `bug` label, resulting in the following format:
> `feature/{issue}-{title}`
the prefix is either`feature` or `bugfix`. Whitespaces in the title are replaced by
`-`.

114
package-lock.json generated
View File

@ -9,6 +9,7 @@
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"axios": "^0.27.2",
"body-parser": "^1.20.0",
"dotenv": "^16.0.1",
"express": "^4.18.1"
@ -33,6 +34,22 @@
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT"
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://nexus.nclazz.de/repository/npm_public/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/axios": {
"version": "0.27.2",
"resolved": "https://nexus.nclazz.de/repository/npm_public/axios/-/axios-0.27.2.tgz",
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.14.9",
"form-data": "^4.0.0"
}
},
"node_modules/body-parser": {
"version": "1.20.0",
"resolved": "https://nexus.nclazz.de/repository/npm_public/body-parser/-/body-parser-1.20.0.tgz",
@ -79,6 +96,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://nexus.nclazz.de/repository/npm_public/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://nexus.nclazz.de/repository/npm_public/content-disposition/-/content-disposition-0.5.4.tgz",
@ -124,6 +153,15 @@
"ms": "2.0.0"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://nexus.nclazz.de/repository/npm_public/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://nexus.nclazz.de/repository/npm_public/depd/-/depd-2.0.0.tgz",
@ -242,6 +280,40 @@
"node": ">= 0.8"
}
},
"node_modules/follow-redirects": {
"version": "1.15.1",
"resolved": "https://nexus.nclazz.de/repository/npm_public/follow-redirects/-/follow-redirects-1.15.1.tgz",
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://nexus.nclazz.de/repository/npm_public/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://nexus.nclazz.de/repository/npm_public/forwarded/-/forwarded-0.2.0.tgz",
@ -672,6 +744,20 @@
"resolved": "https://nexus.nclazz.de/repository/npm_public/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
},
"asynckit": {
"version": "0.4.0",
"resolved": "https://nexus.nclazz.de/repository/npm_public/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"axios": {
"version": "0.27.2",
"resolved": "https://nexus.nclazz.de/repository/npm_public/axios/-/axios-0.27.2.tgz",
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
"requires": {
"follow-redirects": "^1.14.9",
"form-data": "^4.0.0"
}
},
"body-parser": {
"version": "1.20.0",
"resolved": "https://nexus.nclazz.de/repository/npm_public/body-parser/-/body-parser-1.20.0.tgz",
@ -705,6 +791,14 @@
"get-intrinsic": "^1.0.2"
}
},
"combined-stream": {
"version": "1.0.8",
"resolved": "https://nexus.nclazz.de/repository/npm_public/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"requires": {
"delayed-stream": "~1.0.0"
}
},
"content-disposition": {
"version": "0.5.4",
"resolved": "https://nexus.nclazz.de/repository/npm_public/content-disposition/-/content-disposition-0.5.4.tgz",
@ -736,6 +830,11 @@
"ms": "2.0.0"
}
},
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://nexus.nclazz.de/repository/npm_public/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
},
"depd": {
"version": "2.0.0",
"resolved": "https://nexus.nclazz.de/repository/npm_public/depd/-/depd-2.0.0.tgz",
@ -823,6 +922,21 @@
"unpipe": "~1.0.0"
}
},
"follow-redirects": {
"version": "1.15.1",
"resolved": "https://nexus.nclazz.de/repository/npm_public/follow-redirects/-/follow-redirects-1.15.1.tgz",
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA=="
},
"form-data": {
"version": "4.0.0",
"resolved": "https://nexus.nclazz.de/repository/npm_public/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
},
"forwarded": {
"version": "0.2.0",
"resolved": "https://nexus.nclazz.de/repository/npm_public/forwarded/-/forwarded-0.2.0.tgz",

View File

@ -9,6 +9,7 @@
"author": "Niclas Thobaben",
"license": "MIT",
"dependencies": {
"axios": "^0.27.2",
"body-parser": "^1.20.0",
"dotenv": "^16.0.1",
"express": "^4.18.1"

36
src/gitea-api.js 100644
View File

@ -0,0 +1,36 @@
const axios = require('axios')
const api = {}
const BASE_URL = process.env.GITEA_BASE_URL || 'https://git.l--n.de/api/v1'
const TOKEN = process.env.GITEA_TOKEN
console.log(`Use gitea api @ ${BASE_URL}`)
api.post = (path, payload) => {
const url = `${BASE_URL}${path}`
return axios.post(
url,
payload,
{
headers: {
Authorization: `token ${TOKEN}`
}
}
)
}
api.patch = (path, payload) => {
const url = `${BASE_URL}${path}`
return axios.patch(
url,
payload,
{
headers: {
Authorization: `token ${TOKEN}`
}
}
)
}
module.exports = api

17
src/hooks/index.js 100644
View File

@ -0,0 +1,17 @@
const fs = require('fs')
const path = require('path')
const hooks = {}
fs.readdirSync(__dirname).forEach(file => {
if(file === 'index.js') {
return
}
const name = file.replace(/\.[^/.]+$/, '')
const hook = require(path.join(__dirname, file))
hook.name = name
hooks[name] = hook
})
console.log(`Loaded available Hooks [${Object.keys(hooks)}]`)
module.exports = hooks

View File

@ -0,0 +1,37 @@
const gitea = require('../gitea-api')
const log = (msg) => {
console.log('[issueBranchName]', msg)
}
module.exports = {
exec: (req) => {
if(req.headers['x-gitea-event'] !== 'issues') {
return
}
const { issue, repository } = req.body
const isBug = issue.labels.length && !!issue.labels.find(label => label.name === 'bug')
const prefix = isBug ? 'bugfix' : 'feature'
const branch = issue.title.replace(/[^a-z0-9\s]/gi, '').replaceAll(' ', '-')
const branchName = `${prefix}/${issue.number}-${branch}`
log(`Created branch name ${branchName} in ${repository.full_name}`)
issue.body = issue.body || ''
let body
if(issue.body.includes('Branchname')) {
body = issue.body.replaceAll(/\*\*Branchname\*\*: <code>.+<\/code>/g, `**Branchname**: <code>${branchName}</code>`)
}else {
body = `${issue.body}<br>**Branchname**: <code>${branchName}</code>`
}
body = body.replaceAll('\n', '\\n')
const path = `/repos/${repository.full_name}/issues/${issue.number}`
return gitea.patch(path, { body })
.then(() => {})
.catch(reason => reason)
}
}

View File

@ -0,0 +1,14 @@
const log = (msg) => {
console.log('[issueBranchRef]', msg)
}
module.exports = {
exec: (req) => {
if(req.headers['x-gitea-event'] !== 'issues') {
return
}
log('issue branch ref')
}
}

View File

@ -1,5 +1,6 @@
const express = require('express')
const bodyParser = require('body-parser')
const hooks = require('./hooks')
console.log('Initialize express server')
@ -9,5 +10,36 @@ app.use(bodyParser.json())
const port = process.env.SERVER_PORT || 8080
app.post('/webhook', async (req, res) => {
const selectedHooks = []
if(req.query['hooks']) {
req.query.hooks.split(',').forEach(hookName => {
const hook = hooks[hookName.trim()]
if(!hook) {
console.warn(`Hook ${hookName} does not exist! from=${req.ip}`)
return
}
selectedHooks.push(hook)
})
}else {
selectedHooks.push(...Object.values(hooks))
}
const errors = {}
const promises = selectedHooks.map(async hook => {
console.log(`Execute hook ${hook.name} from=${req.ip}`)
const error = await hook.exec(req)
if(error) {
errors[hook.name] = error
}
})
await Promise.all(promises)
if(Object.keys(errors).length) {
res.status(400).json(errors)
return
}
res.status(200).send()
})
app.listen(port)
console.log(`Started express server on port ${port}`)