Fully working proof of concept.

- Track gitea labels
- Add deck card and avoid duplication
- Re-add deck card if removed (based on gitea label) 
- Remove deck card based on label
- archive deck card when issue closed
- Post actions as comment on issue for user convenience.
- Auto create gitea and netcloud labels if not exist.
master
Thijs de Vries 5 years ago
parent 97d4d6a99d
commit 8638f8465c

@ -1,3 +1,34 @@
# deck-gitea-integration # deck-gitea-integration
This NodeJS app automatically creates nextcloud [Deck](https://apps.nextcloud.com/apps/deck) cards based on [gitea](https://gitea.io/en-us/) issues.
May one day be published as a nextcloud/gitea plugin. Right now it's just for internal use. May one day be published as a nextcloud/gitea plugin. Right now it's just for internal use. It's stable in my environment, but claims no guarantees to yours.
## Overview
You'll need to set-up a gitea webhook for each repository you want to auto-add to deck. The webhook must point to the address where this node app is running.
The webhook requests that are now being sent to NodeJS will be parsed and published on Deck. In order to do so you'll need to provide
- A Nextcloud isntallation (URL, not the api)
- A Nextcloud username+password (or username+appPassword) that has access to your Deck board
- A Nextcloud Deck board (numeric ID)
- The deck stack(column) where you want your cards to appear when first created (numeric ID).
- Gitea API url
- Gitea API key, owned by a dedicated 'Deck' or 'Deckbot' user or something (Don't use your own username!). User must have colaboration access to the repo.
After that, a 'Deck' label will be added to your gitea repo, and a 'Gitea' label to your deck board.
When you add the 'Deck' label to an issue, the NodeJS app will automatically pick-up on it, and add the issue to deck. It will also post a notification in the issue that a Deck card has been created.
When the 'Deck' label is removed from an issue, the Deck card is destroyed.
When you close an issue, the Deck card is archived.
When you remove a Deck card that's being tracked, the card will be re-added as soon as a webhook has been fired (AKA an action like commenting)
## Under the hood
WIP
## Deploy your own
WIP

@ -1,33 +1,216 @@
class Gitea { const axios = require('axios')
class Deck {
constructor(url, user, pass) { // Nextcloud's base URL, not the api! constructor(url, user, pass, board, defaultstack) { // Nextcloud's base URL, not the api!
this.api = `${url}/index.php/apps/deck/api/v1.0` this.api = `${url}/index.php/apps/deck/api/v1.0/boards/${board}`
this.user = user this.user = user
this.pass = pass this.pass = pass
this.board = board
this.defaultstack = defaultstack
}
checkLabelExist(label) {
let tmpVal = false
return new Promise((resolve, reject) => {
axios.get(
this.api,
{auth: {username: this.user, password: this.pass}}
).then((res) => {
res.data.labels.forEach((el) => {
if (el.title == label) {
resolve(el)
tmpVal = true
}
})
}).then(() => {
if(!tmpVal) resolve(false)
})
.catch(e => console.log(e))
})
}
createLabel(label, color) {
this.checkLabelExist(label).then((exist) => {
if (!exist) {
axios.post(`${this.api}/labels`,
{
title: label,
color: color
},
{auth: {username: this.user, password: this.pass}}
)
.then(console.log(`[${this.api}] - CREATE LABEL: '${label}' added`))
.catch(e => console.log(e))
} else {
console.log(`[${this.api}] - CREATE LABEL: '${label}' already exists.`)
}
})
}
checkCardExist(tracking) {
let tmpVal = false
return new Promise((resolve, reject) => {
axios.get(
`${this.api}/stacks`,
{auth: {username: this.user, password: this.pass}}
).then((res) => {
res.data.forEach((el) => {
if (el.cards) el.cards.forEach((el) => {
if(el.description) console.log("match"+el.description.match(tracking))
if (el.description && el.description.match(tracking) != null) {
resolve(el)
tmpVal = true
} }
})
})
}).then(() => {
checkTagExist() {
//Check archived stack aswell... Wrapped inside .then because we only want resolve(false) ADTER we checked everywhere, not simoultaniously
axios.get(
`${this.api}/stacks/archived`,
{auth: {username: this.user, password: this.pass}}
).then((res) => {
res.data.forEach((el) => {
if (el.cards) el.cards.forEach((el) => {
if (el.description && el.description.match(tracking) != null) {
resolve(el)
tmpVal = true
} }
})
})
}).then(() => {
if(!tmpVal) resolve(false)
})
})
.catch(e => console.log(e))
createTag() { })
}
createCard(title,description, tracking, tags, stack) {
let tmpVal
return new Promise((resolve, reject) => {
this.checkCardExist(tracking).then((exist) => {
console.log("checkcardexist then:" + exist)
if (!exist) {
axios.post(
`${this.api}/stacks/${this.defaultstack}/cards`,
{title: 'Giteabot comming trough... please reload :-)'},
{auth: {username: this.user, password: this.pass}}
).then((res) => {
res.data.title = `🍵 ${title}`
res.data.description = `[tracking]:${tracking}:\n\n${description}\n`
axios.put(
`${this.api}/stacks/${this.defaultstack}/cards/${res.data.id}`,
res.data,
{auth: {username: this.user, password: this.pass}}
).then(() => {
console.log(`[${this.api}] - ADD CARD: '${tracking}' added to stack '${this.defaultstack}'`)
resolve(true)
tmpVal = true
})
.catch(e => console.log(e))
})
.catch(e => console.log(e))
}
else {
if(!tmpVal){
resolve(false)
console.log(`[${this.api}] - ADD CARD: '${tracking}' already exists in board '${this.board}'`)
}
} }
})
checkCardExist() { })
}
archiveCard(tracking) {
this.checkCardExist(tracking).then((card) => {
if (card) {
card.archived = true
axios.put(
`${this.api}/stacks/${this.defaultstack}/cards/${card.id}`,
card,
{auth: {username: this.user, password: this.pass}}
).then(() => {
console.log(`[${this.api}] - ARCHIVE CARD: '${tracking}' archived`)
})
.catch(e => console.log(e))
} else {
console.log(`[${this.api}] - ARCHIVE CARD: '${tracking}' doesn't exist`)
}
})
}
unarchiveCard(tracking) {
this.checkCardExist(tracking).then((card) => {
if (card) {
card.archived = false
axios.put(
`${this.api}/stacks/${this.defaultstack}/cards/${card.id}`,
card,
{auth: {username: this.user, password: this.pass}}
).then(() => {
console.log(`[${this.api}] - UNARCHIVE CARD: '${tracking}' archived`)
})
.catch(e => console.log(e))
} else {
console.log(`[${this.api}] - UNARCHIVE CARD: '${tracking}' doesn't exist`)
}
})
} }
createCard(board, card, title, tags) {
cardSet(tracking, property, value) {
let tmpVal = false
return new Promise((resolve,reject) => {
this.checkCardExist(tracking).then((card) => {
if (card) {
card[property] = value
axios.put(
`${this.api}/stacks/${this.defaultstack}/cards/${card.id}`,
card,
{auth: {username: this.user, password: this.pass}}
).then(() => {
console.log(`[${this.api}] - CARD PROPERTY SET: '${property}' to '${value}' on '${tracking}'`)
resolve(true)
tmpVal = true
})
.catch(e => console.log(e))
} else {
if(!tmpVal)
console.log(`[${this.api}] - CARD PROPERTY SET: '${tracking}' doesn't exist`)
resolve(false)
} }
})
closeCard() { })
} }
deleteCard() { deleteCard(tracking) {
return new Promise((resolve,reject) => {
this.checkCardExist(tracking).then((card) => {
if(card) {
axios.delete(
`${this.api}/stacks/${this.defaultstack}/cards/${card.id}`,
{auth: {username: this.user, password: this.pass}}
).then(() => {
console.log(`[${this.api}] - DELETE CARD: '${tracking}' deleted`)
resolve(true)
})
}
else{
console.log(`[${this.api}] - DELETE CARD: '${tracking}' doesn't exist`)
resolve(false)
}
})
})
} }
} }
module.exports = Deck

@ -1,11 +1,14 @@
const axios = require('axios') const axios = require('axios')
const md5 = require('md5')
class Gitea { class Gitea {
constructor(api, key) { constructor(api, key) {
this.api = `${api}/repos` this.api = `${api}/repos`
this.key = key this.key = key
} }
trackingCode(repo, issue) {
return(md5(`${this.api}${repo}${issue}`))
}
checkLabelExistance(label, repo) { checkLabelExistance(label, repo) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -32,13 +35,14 @@ class Gitea {
{color: color,name: label}, {color: color,name: label},
{"headers": {"Authorization" : `token ${this.key}`} {"headers": {"Authorization" : `token ${this.key}`}
}) })
.then(console.log(`Label ${label} created`)) .then(console.log(`[${this.api}] - CREATE LABEL: '${label}|${color}' created in '${repo}'`))
} }
}) })
.catch(e => console.log(e)) .catch(e => console.log(e))
} }
labelStatus(label, repo, issue) { labelStatus(label, repo, issue) {
let tmpVal = false
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
axios.get( axios.get(
`${this.api}/${repo}/issues/${issue}`, `${this.api}/${repo}/issues/${issue}`,
@ -46,9 +50,18 @@ class Gitea {
}) })
.then((response) => { .then((response) => {
response.data.labels.forEach((el) => { response.data.labels.forEach((el) => {
if (el.name == label) resolve(true) if (el.name == label) {
console.log(`[${this.api}] - LABEL STATUS: '${label}' detected in issue '${repo} ${issue}'`)
resolve(true)
tmpVal = true
}
})
}) })
.then(() => {
if(!tmpVal) {
console.log(`[${this.api}] - LABEL STATUS: '${label}' not detected in issue '${repo} ${issue}'`)
resolve(false) resolve(false)
}
}) })
.catch(e => console.log(e)) .catch(e => console.log(e))
}) })
@ -61,7 +74,7 @@ class Gitea {
{body: message}, {body: message},
{"headers": {"Authorization" : `token ${this.key}`} {"headers": {"Authorization" : `token ${this.key}`}
}) })
.then(console.log(`Message posted`)) .then(console.log(`[${this.api}] - POST MESSAGE: '${message}' posted to '${repo} ${issue}'`))
.catch(e => console.log(e)) .catch(e => console.log(e))
} }
} }

@ -1,17 +1,15 @@
const Gitea = require('./gitea.js') // We'll need some functions to manage our gitea installation. Check labels, post comments etc.
const Deck = require('./deck.js') // Same goes for deck.
const express = require('express') const express = require('express')
const bodyParser = require('body-parser') const bodyParser = require('body-parser')
const axios = require('axios') const axios = require('axios')
const app = express() const app = express()
const Gitea = require('./gitea.js') // Hardcoded for now...
const gitea = new Gitea("https://yourgiteainstall.com/api/v1", 'apikey') const gitea = new Gitea("https://yourgiteainstall.com/api/v1", 'apikey')
const deck = new Deck("https://yournextcloudinstall.com", 'nextcloudusername', 'password or apppassword', board-integer, deckStack-integer)
const ncUser = "" // autoparse JSON and allow CORS.
const ncPass = ""
const ncApi = "https://yournextcloudinstall.com/index.php/apps/deck/api/v1.0"
const ncBoard = 2
app.use(bodyParser.json()) app.use(bodyParser.json())
app.use(function(req, res, next) { app.use(function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*") res.header("Access-Control-Allow-Origin", "*")
@ -20,35 +18,70 @@ app.use(function(req, res, next) {
next() next()
}) })
// Create label Gitea inside deck, if it doesn't exist that is.
deck.createLabel("Gitea","609A21")
// gitea only POSTs and only to a single URL. We'll need to parse json for more info on what's happening // gitea only POSTs and only to a single URL. We'll need to parse json for more info on what's happening
app.post("/", (req, res) => { app.post("/", (req, res) => {
// Let's make our console a little more readable :)
console.log('\n ----- request -----')
// Just for convenience
let repo = req.body.repository.full_name let repo = req.body.repository.full_name
let issue = req.body.issue.number let issue = req.body.issue.number
let issueUrl = `https://git.thijsdevries.net/${repo}/issues/${issue}`
let action = req.body.action let action = req.body.action
gitea.createLabel('Deck',"#0082C9", repo) // Double check existace of issue label, if not; create. let title = req.body.issue.title
console.log(`${req.body.action} | [${req.body.repository.full_name}] #${req.body.issue.number}:${req.body.issue.title}`) let tracking = gitea.trackingCode(repo,issue) // We MD5 all the issue-specific data that won't change during the lifetime of this issue. That generated sum is then used to track the issue on deck.
// console.log(`(${req.body.issue.url})`)
gitea.createLabel('Deck',"#0082C9", repo) // Double check existace of issue label, if not; create.
console.log(`${req.body.action} | ${tracking}`)
if(action == "label_updated" || action == "reopened" || action == "opened" || action == "label_cleared") { // Checl if somging label-related changed. The existance of the 'Deck' label wil dictate the existance of the Deck card.
if(action == "label_updated" || action == "opened" || action == "label_cleared") {
gitea.labelStatus('Deck', repo, issue).then((set) => { gitea.labelStatus('Deck', repo, issue).then((set) => {
if(set) { if(set) {
console.log("Label Deck detected") deck.createCard(`[${repo}] #${issue}: ${title}`, `# [Open Issue](${issueUrl})`, tracking)
gitea.comment(`Issue added to deck :-)`, repo, issue) .then((result) => {
if (result) gitea.comment(`I've added a card to Deck, and I'm tracking this issue for changes :-)`, repo, issue)
})
.catch(e => console.log(e))
} else { } else {
console.log("Label Deck not detected") deck.deleteCard(tracking).then((result) => {
if (result) gitea.comment(`I've removed the corresponding Deck card, and I'll stop tracking this issue. Bye!`, repo, issue)
})
} }
}) })
} }
// If closed, Deck card should be archived.
else if(action == 'closed'){ else if(action == 'closed'){
console.log('Issue closed') deck.cardSet(tracking, "archive", true)
}
else if(action == "reopened"){
deck.cardSet(tracking, "archive", false).then((result) => {
if(result) if (result) {
gitea.comment(`I've dug up that archived Deck card for you. You're welcome :)`, repo, issue)
} }
else { else {
console.log(`Received unsupported action "${action}"`) deck.createCard(`[${repo}] #${issue}: ${title}`, `# [Open Issue](${issueUrl})`, tracking)
// console.log(JSON.stringify(req.headers, null,2)) .then((result) => {
// console.log(JSON.stringify(req.body, null,2)) if (result) gitea.comment(`Seems like you've recycled this Deck card. I'll re-add it.`, repo, issue)
})
}
})
}
else {
gitea.labelStatus('Deck', repo, issue).then((set) => {
if(set) deck.createCard(`[${repo}] #${issue}: ${title}`, `# [Open Issue](${issueUrl})`, tracking)
.then((result) => {
if (result) gitea.comment(`Looks like someone acidentally removed or archived the corresponding Deck card! If you're done, just close this issue and I'll archive the card. If you don't want it to show up at al, remove the 'Deck' label from this issue and I'll delete it.
I've re-added the card to Deck.`, repo, issue)
})
.catch(e => console.log(e))
})
} }
res.send("okay") // generate some feedback for gitea res.send("okay") // generate some feedback for gitea
}) })
app.listen(80, () => console.log(`Aapie running on 80`)) app.listen(80, () => console.log(`Listening on :80`))

2291
app/package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,27 @@
{
"name": "deck-gitea-integration",
"version": "0.0.0",
"description": "Integrate gitea issues into Nextcloud Deck using webhooks.",
"main": "index.js",
"dependencies": {
"axios": "^0.19.0",
"express": "^4.17.1",
"md5": "^2.2.1"
},
"devDependencies": {},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "https://git.thijsdevries.net/dodedodo/deck-gitea-integration.git"
},
"keywords": [
"deck",
"gitea",
"integration",
"nextcloud"
],
"author": "Thijs de Vries",
"license": "GPL-3.0"
}
Loading…
Cancel
Save