Skip to content

Commit 4ddf80e

Browse files
committed
feat:
Added maintainer feature Completed CLI
1 parent 9eda314 commit 4ddf80e

13 files changed

+303
-63
lines changed

index.ts

Lines changed: 9 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,12 @@
1-
/**
2-
* @module confluence-outdated
3-
*/
4-
/**
5-
*/
1+
#!/usr/bin/env node
62

7-
// import needed modules
3+
import * as Path from 'path'
4+
import { CLI, Shim } from 'clime'
85

9-
import * as loglevel from 'loglevel'
10-
import Bluebird = require('bluebird')
6+
// The second parameter is the path to folder that contains command modules.
7+
const cli = new CLI('confluence-outdated', Path.join(__dirname, 'lib', 'commands'))
118

12-
/**
13-
* confluence-outdated - Constant validation of Confluence document outdates
14-
*/
15-
export class ExampleClass {
16-
/**
17-
* Use a logger instance to log, what you're doing
18-
*/
19-
private _log: loglevel.Logger = null
20-
21-
constructor() {
22-
this._log = loglevel.getLogger('confluence-outdated:ExampleClass')
23-
}
24-
25-
/**
26-
* Be nice.
27-
* @param {string} name Who are you?
28-
* @return {Bluebird<string>} My greets to you
29-
*/
30-
public helloWorld(name: string): Bluebird<string> {
31-
if (name === '') {
32-
return Bluebird.reject(new Error('Found nobody to greet'))
33-
}
34-
return Bluebird.resolve(`Hello ${name}`)
35-
}
36-
}
9+
// Clime in its core provides an object-based command-line infrastructure.
10+
// To have it work as a common CLI, a shim needs to be applied:
11+
const shim = new Shim(cli)
12+
shim.execute(process.argv)

lib/DefaultOptions.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { option, Options } from 'clime'
2+
import * as log from 'loglevel'
3+
import * as prefix from 'loglevel-plugin-prefix'
4+
5+
export class DefaultOptions extends Options {
6+
@option({
7+
name: 'url',
8+
flag: 'U',
9+
description: 'URL to your Confluence instance',
10+
required: true,
11+
})
12+
confluenceUrl: string
13+
14+
@option({
15+
name: 'user',
16+
flag: 'u',
17+
description: 'Username for checking all confluence documents',
18+
required: true,
19+
})
20+
confluenceUser: string
21+
22+
@option({
23+
name: 'password',
24+
flag: 'p',
25+
description: 'Password for the user',
26+
required: true,
27+
})
28+
confluencePassword: string
29+
30+
@option({
31+
description: 'Log-Level to use (trace, debug, verbose, info, warn, error)',
32+
default: 'error',
33+
validator: /trace|debug|verbose|info|warn|error/,
34+
})
35+
public loglevel: string
36+
37+
public getLogger(): log.Logger {
38+
prefix.reg(log)
39+
prefix.apply(log, {
40+
template: '[%t] %l (%n)',
41+
})
42+
log.setDefaultLevel(this.loglevel as log.LogLevelDesc)
43+
return log.getLogger('cli')
44+
}
45+
}

lib/api/Configuration.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ConfigurationError } from '../error/ConfigurationError'
55
import * as SMTPTransport from 'nodemailer/lib/smtp-transport'
66
import log = require('loglevel')
77
import * as cheerio from 'cheerio'
8+
import { Maintainer } from './Maintainer'
89

910
/**
1011
* Configuration API
@@ -63,6 +64,11 @@ export class Configuration {
6364
*/
6465
public checks: Array<Check>
6566

67+
/**
68+
* A list of page maintainers
69+
*/
70+
public maintainer: Array<Maintainer>
71+
6672
/**
6773
* A Handlebars template for the notification mail subject
6874
*/
@@ -130,6 +136,9 @@ export class Configuration {
130136
throw e
131137
}
132138

139+
this._log.trace(`Configuration document content:
140+
${configurationDocument.body.storage.value}`)
141+
133142
const $ = cheerio.load(configurationDocument.body.storage.value)
134143

135144
if ($ === null) {
@@ -155,6 +164,15 @@ export class Configuration {
155164
}
156165
})
157166

167+
// Load maintainer
168+
169+
this.maintainer = this._getConfigurationFromPanel($, 'Maintainer').map<Maintainer>((value) => {
170+
return {
171+
pagePattern: new RegExp(value.pagepattern),
172+
maintainer: value.maintainer,
173+
}
174+
})
175+
158176
this.transportOptions = this._getConfigurationFromPanel($, 'SMTP')[0]
159177

160178
this.notificationSubjectTemplate = $(

lib/api/DocumentInfo.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export interface DocumentInfo {
1919
/**
2020
* The date of the last version
2121
*/
22-
lastVersionDate: Moment
22+
lastVersionDate: Moment | string
2323
/**
2424
* The edit message of the last version
2525
*/
@@ -33,12 +33,12 @@ export interface DocumentInfo {
3333
export class DocumentInfo implements DocumentInfo {
3434
public id: number
3535
public author: string
36-
public lastVersionDate: moment.Moment
36+
public lastVersionDate: Moment | string
3737
public lastVersionMessage: string
3838
public title: string
3939
public url: string
4040

41-
constructor(id: number, author: string, lastVersionDate: moment.Moment, lastVersionMessage: string, title: string, url: string) {
41+
constructor(id: number, author: string, lastVersionDate: Moment, lastVersionMessage: string, title: string, url: string) {
4242
this.id = id
4343
this.author = author
4444
this.lastVersionDate = lastVersionDate

lib/api/Maintainer.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* A maintainer definition for pages matching a pattern.
3+
* Will override the last version author
4+
*/
5+
export interface Maintainer {
6+
/**
7+
* A pattern matching the document title
8+
*/
9+
pagePattern: RegExp
10+
/**
11+
* The username of the user maintaining the pages matching the pattern
12+
*/
13+
maintainer: string
14+
}
15+
16+
export class Maintainer implements Maintainer {
17+
public maintainer: string
18+
public pagePattern: RegExp
19+
20+
constructor(maintainer: string, pagePattern: RegExp) {
21+
this.maintainer = maintainer
22+
this.pagePattern = pagePattern
23+
}
24+
}

lib/api/Notification.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,52 @@ import { createTransport } from 'nodemailer'
44
import * as Mail from 'nodemailer/lib/mailer'
55
import { DocumentInfo } from './DocumentInfo'
66
import * as Handlebars from 'handlebars'
7+
import { Logger } from 'loglevel'
8+
import { Moment } from 'moment'
9+
import log = require('loglevel')
10+
import moment = require('moment')
711

812
export class Notification {
913
private _configuration: Configuration
1014
private _confluence: Confluence
1115
private _transport: Mail
16+
private _log: Logger
17+
private readonly _dryRun: boolean
1218

13-
constructor(configuration: Configuration, confluence: Confluence, transport: Mail = null) {
19+
constructor(configuration: Configuration, confluence: Confluence, transport: Mail = null, dryRun = false) {
1420
this._configuration = configuration
1521
this._confluence = confluence
1622
this._transport = transport || createTransport(this._configuration.transportOptions)
23+
24+
this._log = log.getLogger('Notification')
25+
this._dryRun = dryRun
1726
}
1827

1928
public async notify(documentInfo: DocumentInfo): Promise<void> {
29+
Handlebars.registerHelper('moment', (text, format) => {
30+
return moment(text).format(format)
31+
})
2032
const subjectTemplate = Handlebars.compile(this._configuration.notificationSubjectTemplate)
2133
const bodyTemplate = Handlebars.compile(this._configuration.notificationBodyTemplate)
22-
await this._transport.sendMail({
34+
35+
documentInfo.lastVersionDate = (documentInfo.lastVersionDate as Moment).toISOString() as string
36+
37+
for (const maintainer of this._configuration.maintainer) {
38+
if (maintainer.pagePattern.test(documentInfo.title)) {
39+
documentInfo.author = maintainer.maintainer
40+
}
41+
}
42+
43+
const mailOptions = {
2344
from: this._configuration.notificationFrom,
2445
to: `${documentInfo.author}@${this._configuration.domain}`,
2546
subject: subjectTemplate(documentInfo),
2647
html: bodyTemplate(documentInfo),
27-
})
48+
}
49+
this._log.info(`Notifying ${mailOptions.to} about ${documentInfo.title}`)
50+
this._log.trace(mailOptions)
51+
if (!this._dryRun) {
52+
await this._transport.sendMail(mailOptions)
53+
}
2854
}
2955
}

lib/commands/Check.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { command, Command, metadata, option } from 'clime'
2+
import { Configuration } from '../api/Configuration'
3+
import { Confluence } from '../api/Confluence'
4+
import { Notification } from '../api/Notification'
5+
import { DefaultOptions } from '../DefaultOptions'
6+
7+
export class CheckOptions extends DefaultOptions {
8+
@option({
9+
name: 'id',
10+
flag: 'i',
11+
description: 'ID of configuration document',
12+
required: true,
13+
})
14+
configurationDocumentId: string
15+
16+
@option({
17+
name: 'dryrun',
18+
flag: 'd',
19+
description: 'Do not send notifications',
20+
toggle: true,
21+
})
22+
dryRun: boolean
23+
}
24+
25+
@command({
26+
description: 'Check confluence for outdated documents',
27+
})
28+
export default class extends Command {
29+
@metadata
30+
public async execute(options: CheckOptions): Promise<void> {
31+
const log = options.getLogger()
32+
33+
log.info('Checking for outdated documents')
34+
35+
const configuration = new Configuration(
36+
options.confluenceUrl,
37+
options.confluenceUser,
38+
options.confluencePassword,
39+
options.configurationDocumentId
40+
)
41+
await configuration.load()
42+
43+
const confluence = new Confluence(options.confluenceUrl, options.confluenceUser, options.confluencePassword)
44+
45+
const notification = new Notification(configuration, confluence, null, options.dryRun)
46+
47+
for (const check of configuration.checks) {
48+
log.debug(`Checking for documents older than ${check.maxAge} day(s) with label(s) ${check.labels.join(',')}`)
49+
let filter = `label = ${check.labels.join('AND label = ')}`
50+
if (configuration.space && configuration.space != '') {
51+
filter = `${filter} AND space = ${configuration.space}`
52+
}
53+
const checkedDocuments = await confluence.findDocumentsOlderThan(filter, check.maxAge)
54+
55+
for (const checkedDocument of checkedDocuments) {
56+
await notification.notify(checkedDocument)
57+
}
58+
}
59+
}
60+
}

0 commit comments

Comments
 (0)