Skip to content

Commit be486c9

Browse files
author
Adam Duncan
committed
Initial commit
0 parents  commit be486c9

File tree

9 files changed

+810
-0
lines changed

9 files changed

+810
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules

Dockerfile

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
FROM node:10.15.1-alpine
2+
3+
WORKDIR /app
4+
COPY . .
5+
6+
RUN yarn
7+
8+
CMD /usr/local/bin/node /app/plugin.js

README.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Drone Config Plugin - Changeset Conditional
2+
3+
This implements the ability to have steps / pipelines only execute when certain files have changed, using the following additional YAML syntax:
4+
5+
```
6+
pipeline:
7+
frontend:
8+
image: node
9+
commands:
10+
- cd app
11+
- npm run test
12+
+ when:
13+
+ changeset:
14+
+ includes: [ **/**.js, **/**.css, **/**.html ]
15+
backend:
16+
image: golang
17+
commands:
18+
- go build
19+
- go test -v
20+
+ when:
21+
+ changeset:
22+
+ includes: [ **/**.go ]
23+
24+
+changeset:
25+
+ includes: [ **/**.go ]
26+
```
27+
28+
## Installation
29+
30+
PLEASE NOTE: At the moment it supports only github.com installations.
31+
32+
Generate a GitHub access token with repo permission. This token is used to fetch the `.drone.yml` file and details of the files changed.
33+
34+
Generate a shared secret key. This key is used to secure communication between the server and agents. The secret should be 32 bytes.
35+
```
36+
$ openssl rand -hex 16
37+
558f3eacbfd5928157cbfe34823ab921
38+
```
39+
40+
Run the container somewhere where the drone server can reach it:
41+
42+
```
43+
docker run \
44+
-p ${PLUGIN_PORT}:3000 \
45+
-e PLUGIN_SECRET=558f3eacbfd5928157cbfe34823ab921 \
46+
-e GITHUB_TOKEN=GITHUB8168c98304b \
47+
--name drone-changeset-conditional \
48+
microadam/drone-config-plugin-changeset-conditional
49+
```
50+
51+
Update your drone server with information about the plugin:
52+
53+
```
54+
-e DRONE_YAML_ENDPOINT=http://${PLUGIN_HOST}:${PLUGIN_PORT}
55+
-e DRONE_YAML_SECRET=558f3eacbfd5928157cbfe34823ab921
56+
```
57+
58+
See [the official docs](https://docs.drone.io/extend/config) for extra information on installing a Configuration Provider Plugin.
59+
60+
## Pattern Matching
61+
62+
This uses the [Glob](https://www.npmjs.com/package/glob) module under the hood, so supports all pattern matching syntaxes of this module.

lib/files-changed-determiner.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const { promisify } = require('util')
2+
const getCommit = gh => async data => {
3+
let commitData = null
4+
const options = {
5+
user: data.repo.namespace,
6+
repo: data.repo.name,
7+
base: data.build.before,
8+
head: data.build.after
9+
}
10+
const comparison = await promisify(gh.repos.compareCommits)(options)
11+
return comparison.files.map(f => f.filename)
12+
}
13+
14+
module.exports = getCommit

lib/parsed-yaml-retriever.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
const { promisify } = require('util')
2+
const yaml = require('yamljs')
3+
const getParsedYaml = gh => async data => {
4+
let file = null
5+
const options = {
6+
user: data.repo.namespace,
7+
repo: data.repo.name,
8+
ref: data.build.ref,
9+
path: data.repo.config_path
10+
}
11+
file = await promisify(gh.repos.getContent)(options)
12+
const contents = Buffer.from(file.content, 'base64').toString()
13+
const parsed = yaml.parse(contents)
14+
return parsed
15+
}
16+
17+
module.exports = getParsedYaml

lib/signature-validator.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
const httpSignature = require('http-signature')
2+
const isValidSig = (req, hmac) => {
3+
req.headers.signature = 'Signature ' + req.headers.signature
4+
const parsedSig = httpSignature.parseRequest(req, { authorizationHeaderName: 'signature' })
5+
return httpSignature.verifyHMAC(parsedSig, hmac)
6+
}
7+
8+
module.exports = isValidSig

package.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "drone-config-changeset-conditional",
3+
"version": "1.0.0",
4+
"description": "",
5+
"main": "plugin.js",
6+
"scripts": {
7+
"test": "echo \"Error: no test specified\" && exit 1"
8+
},
9+
"author": "",
10+
"license": "ISC",
11+
"dependencies": {
12+
"body-parser": "^1.18.3",
13+
"express": "^4.16.4",
14+
"github4": "^1.1.1",
15+
"globule": "^1.2.1",
16+
"http-signature": "^1.2.0",
17+
"yamljs": "^0.3.0"
18+
}
19+
}

plugin.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
const express = require('express')
2+
const bodyParser = require('body-parser')
3+
const GhApi = require('github4')
4+
const yaml = require('yamljs')
5+
const glob = require('globule')
6+
const createFilesChangedDeterminer = require('./lib/files-changed-determiner')
7+
const createParsedYamlRetriever = require('./lib/parsed-yaml-retriever')
8+
const isValidSig = require('./lib/signature-validator')
9+
10+
const githubToken = process.env.GITHUB_TOKEN
11+
const sharedKey = process.env.PLUGIN_SECRET
12+
13+
const gh = new GhApi({ version: '3.0.0' })
14+
gh.authenticate({ type: 'oauth', token: githubToken })
15+
16+
const determineFilesChanged = createFilesChangedDeterminer(gh)
17+
const getParsedYaml = createParsedYamlRetriever(gh)
18+
19+
const nullYaml = 'kind: pipeline\nname: default\ntrigger:\n event:\n exclude: [ "*" ]'
20+
21+
const app = express()
22+
app.post('/', bodyParser.json(), async (req, res) => {
23+
console.log('Processing...')
24+
if (!req.headers.signature) return res.status(400).send('Missing signature')
25+
if (!isValidSig(req, sharedKey)) return res.status(400).send('Invalid signature')
26+
if (!req.body) return res.sendStatus(400)
27+
const data = req.body
28+
29+
let filesChanged = []
30+
try {
31+
filesChanged = await determineFilesChanged(data)
32+
} catch (e) {
33+
console.log('ERROR:', e)
34+
return res.sendStatus(500)
35+
}
36+
37+
console.log('Files changed:', filesChanged)
38+
39+
let parsedYaml = null
40+
try {
41+
parsedYaml = await getParsedYaml(data)
42+
} catch (e) {
43+
if (e.code === 404) return res.sendStatus(204)
44+
console.log('ERROR:', e)
45+
return res.sendStatus(500)
46+
}
47+
48+
if (parsedYaml.trigger && parsedYaml.trigger.changeset && parsedYaml.trigger.changeset.includes) {
49+
const requiredFiles = parsedYaml.trigger.changeset.includes
50+
const matchedFiles = glob.match(requiredFiles, filesChanged, { dot: true })
51+
console.log('Matched files for pipeline:', matchedFiles.length, 'Allowed matches:', requiredFiles)
52+
if (!matchedFiles.length) return res.json({ Data: nullYaml })
53+
}
54+
55+
const trimmedSteps = parsedYaml.steps.filter(s => {
56+
if (!s.when || !s.when.changeset || !s.when.changeset.includes) return true
57+
const requiredFiles = s.when.changeset.includes
58+
const matchedFiles = glob.match(requiredFiles, filesChanged, { dot: true })
59+
console.log('Matched files for step:', matchedFiles.length, 'Allowed matches:', requiredFiles)
60+
return matchedFiles.length
61+
})
62+
63+
const returnYaml = trimmedSteps.length ? yaml.stringify({ ...parsedYaml, steps: trimmedSteps }) : nullYaml
64+
65+
res.json({ Data: returnYaml })
66+
})
67+
68+
app.listen(3000)

0 commit comments

Comments
 (0)