Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Adam Duncan committed Feb 9, 2019
0 parents commit be486c9
Show file tree
Hide file tree
Showing 9 changed files with 810 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
8 changes: 8 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FROM node:10.15.1-alpine

WORKDIR /app
COPY . .

RUN yarn

CMD /usr/local/bin/node /app/plugin.js
62 changes: 62 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Drone Config Plugin - Changeset Conditional

This implements the ability to have steps / pipelines only execute when certain files have changed, using the following additional YAML syntax:

```
pipeline:
frontend:
image: node
commands:
- cd app
- npm run test
+ when:
+ changeset:
+ includes: [ **/**.js, **/**.css, **/**.html ]
backend:
image: golang
commands:
- go build
- go test -v
+ when:
+ changeset:
+ includes: [ **/**.go ]
+changeset:
+ includes: [ **/**.go ]
```

## Installation

PLEASE NOTE: At the moment it supports only github.com installations.

Generate a GitHub access token with repo permission. This token is used to fetch the `.drone.yml` file and details of the files changed.

Generate a shared secret key. This key is used to secure communication between the server and agents. The secret should be 32 bytes.
```
$ openssl rand -hex 16
558f3eacbfd5928157cbfe34823ab921
```

Run the container somewhere where the drone server can reach it:

```
docker run \
-p ${PLUGIN_PORT}:3000 \
-e PLUGIN_SECRET=558f3eacbfd5928157cbfe34823ab921 \
-e GITHUB_TOKEN=GITHUB8168c98304b \
--name drone-changeset-conditional \
microadam/drone-config-plugin-changeset-conditional
```

Update your drone server with information about the plugin:

```
-e DRONE_YAML_ENDPOINT=http://${PLUGIN_HOST}:${PLUGIN_PORT}
-e DRONE_YAML_SECRET=558f3eacbfd5928157cbfe34823ab921
```

See [the official docs](https://docs.drone.io/extend/config) for extra information on installing a Configuration Provider Plugin.

## Pattern Matching

This uses the [Glob](https://www.npmjs.com/package/glob) module under the hood, so supports all pattern matching syntaxes of this module.
14 changes: 14 additions & 0 deletions lib/files-changed-determiner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const { promisify } = require('util')
const getCommit = gh => async data => {
let commitData = null
const options = {
user: data.repo.namespace,
repo: data.repo.name,
base: data.build.before,
head: data.build.after
}
const comparison = await promisify(gh.repos.compareCommits)(options)
return comparison.files.map(f => f.filename)
}

module.exports = getCommit
17 changes: 17 additions & 0 deletions lib/parsed-yaml-retriever.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const { promisify } = require('util')
const yaml = require('yamljs')
const getParsedYaml = gh => async data => {
let file = null
const options = {
user: data.repo.namespace,
repo: data.repo.name,
ref: data.build.ref,
path: data.repo.config_path
}
file = await promisify(gh.repos.getContent)(options)
const contents = Buffer.from(file.content, 'base64').toString()
const parsed = yaml.parse(contents)
return parsed
}

module.exports = getParsedYaml
8 changes: 8 additions & 0 deletions lib/signature-validator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const httpSignature = require('http-signature')
const isValidSig = (req, hmac) => {
req.headers.signature = 'Signature ' + req.headers.signature
const parsedSig = httpSignature.parseRequest(req, { authorizationHeaderName: 'signature' })
return httpSignature.verifyHMAC(parsedSig, hmac)
}

module.exports = isValidSig
19 changes: 19 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "drone-config-changeset-conditional",
"version": "1.0.0",
"description": "",
"main": "plugin.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"body-parser": "^1.18.3",
"express": "^4.16.4",
"github4": "^1.1.1",
"globule": "^1.2.1",
"http-signature": "^1.2.0",
"yamljs": "^0.3.0"
}
}
68 changes: 68 additions & 0 deletions plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
const express = require('express')
const bodyParser = require('body-parser')
const GhApi = require('github4')
const yaml = require('yamljs')
const glob = require('globule')
const createFilesChangedDeterminer = require('./lib/files-changed-determiner')
const createParsedYamlRetriever = require('./lib/parsed-yaml-retriever')
const isValidSig = require('./lib/signature-validator')

const githubToken = process.env.GITHUB_TOKEN
const sharedKey = process.env.PLUGIN_SECRET

const gh = new GhApi({ version: '3.0.0' })
gh.authenticate({ type: 'oauth', token: githubToken })

const determineFilesChanged = createFilesChangedDeterminer(gh)
const getParsedYaml = createParsedYamlRetriever(gh)

const nullYaml = 'kind: pipeline\nname: default\ntrigger:\n event:\n exclude: [ "*" ]'

const app = express()
app.post('/', bodyParser.json(), async (req, res) => {
console.log('Processing...')
if (!req.headers.signature) return res.status(400).send('Missing signature')
if (!isValidSig(req, sharedKey)) return res.status(400).send('Invalid signature')
if (!req.body) return res.sendStatus(400)
const data = req.body

let filesChanged = []
try {
filesChanged = await determineFilesChanged(data)
} catch (e) {
console.log('ERROR:', e)
return res.sendStatus(500)
}

console.log('Files changed:', filesChanged)

let parsedYaml = null
try {
parsedYaml = await getParsedYaml(data)
} catch (e) {
if (e.code === 404) return res.sendStatus(204)
console.log('ERROR:', e)
return res.sendStatus(500)
}

if (parsedYaml.trigger && parsedYaml.trigger.changeset && parsedYaml.trigger.changeset.includes) {
const requiredFiles = parsedYaml.trigger.changeset.includes
const matchedFiles = glob.match(requiredFiles, filesChanged, { dot: true })
console.log('Matched files for pipeline:', matchedFiles.length, 'Allowed matches:', requiredFiles)
if (!matchedFiles.length) return res.json({ Data: nullYaml })
}

const trimmedSteps = parsedYaml.steps.filter(s => {
if (!s.when || !s.when.changeset || !s.when.changeset.includes) return true
const requiredFiles = s.when.changeset.includes
const matchedFiles = glob.match(requiredFiles, filesChanged, { dot: true })
console.log('Matched files for step:', matchedFiles.length, 'Allowed matches:', requiredFiles)
return matchedFiles.length
})

const returnYaml = trimmedSteps.length ? yaml.stringify({ ...parsedYaml, steps: trimmedSteps }) : nullYaml

res.json({ Data: returnYaml })
})

app.listen(3000)
Loading

0 comments on commit be486c9

Please sign in to comment.