├── views
├── failure.ejs
├── console.ejs
├── login.ejs
├── index.ejs
└── layout.ejs
├── .npmignore
├── .gitignore
├── public
├── images
│ ├── logo.png
│ ├── hubot_login.png
│ ├── building-bot.gif
│ ├── robawt-status.gif
│ └── disclosure-arrow.png
└── css
│ └── base.css
├── src
├── jinkies.coffee
├── user.coffee
├── jenkins
│ ├── utils
│ │ ├── http_request.coffee
│ │ ├── job_build_request.coffee
│ │ └── campfire.coffee
│ ├── server.coffee
│ ├── job.coffee
│ └── build.coffee
├── robot.coffee
└── app.coffee
├── spec
├── base64_spec.coffee
├── trigger_build_spec.coffee
├── helper.coffee
├── job_spec.coffee
├── campfire_spec.coffee
└── server_spec.coffee
├── Makefile
├── package.json
├── LICENSE
├── README.md
└── bin
└── jinkies
/views/failure.ejs:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | src
2 | spec
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.js
2 | .DS_Store
3 |
--------------------------------------------------------------------------------
/views/console.ejs:
--------------------------------------------------------------------------------
1 |
<%- output %>
2 |
--------------------------------------------------------------------------------
/public/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atmos/jinkies/HEAD/public/images/logo.png
--------------------------------------------------------------------------------
/public/images/hubot_login.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atmos/jinkies/HEAD/public/images/hubot_login.png
--------------------------------------------------------------------------------
/public/images/building-bot.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atmos/jinkies/HEAD/public/images/building-bot.gif
--------------------------------------------------------------------------------
/public/images/robawt-status.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atmos/jinkies/HEAD/public/images/robawt-status.gif
--------------------------------------------------------------------------------
/public/images/disclosure-arrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atmos/jinkies/HEAD/public/images/disclosure-arrow.png
--------------------------------------------------------------------------------
/views/login.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/views/index.ejs:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/src/jinkies.coffee:
--------------------------------------------------------------------------------
1 | require.paths.unshift(__dirname)
2 |
3 | Robot = require "./robot"
4 | Server = require "jenkins/server"
5 | ExpressApp = require "./app"
6 |
7 | exports.Job = Server.Job
8 | exports.Robot = Robot.Robot
9 | exports.Build = Server.Build
10 | exports.Server = Server.Server
11 | exports.ExpressApp = ExpressApp.App
12 | exports.CampfireRobot = Robot.CampfireRobot
13 |
--------------------------------------------------------------------------------
/spec/base64_spec.coffee:
--------------------------------------------------------------------------------
1 | Helper = require "./helper"
2 | Vows = Helper.Vows
3 | assert = Helper.Assert
4 | Options = Helper.default_options
5 |
6 | Vows
7 | .describe("Jenkins Base64 Library")
8 | .addBatch
9 | "Jenkins Base64 can":
10 | topic: ->
11 | new Buffer "Jinkies Paradise"
12 | "encode strings": (buffer) ->
13 | assert.equal buffer.toString("base64"), "Smlua2llcyBQYXJhZGlzZQ=="
14 |
15 | .export(module)
16 |
--------------------------------------------------------------------------------
/spec/trigger_build_spec.coffee:
--------------------------------------------------------------------------------
1 | Helper = require "./helper"
2 | Vows = Helper.Vows
3 | assert = Helper.Assert
4 | Options = Helper.default_options
5 |
6 | Job = require("jinkies").Job
7 |
8 | Vows
9 | .describe("Jenkins Job Build Trigger API")
10 | .addBatch
11 | "Jenkins Jobs Build can":
12 | topic: ->
13 | j = new Job Options.server, Options.job
14 | j.triggerBuild "master", "{}", @callback
15 |
16 | "trigger a build": (err, data) ->
17 | assert.ok data.status
18 |
19 | .export(module)
20 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | deps:
2 | @test `which coffee` || echo "You need to have CoffeeScript in your PATH.\nPlease install it using `brew install coffee-script` or `npm install coffee-script`."
3 |
4 | build: deps
5 | @coffee -o lib src/
6 | @coffee -o spec spec/
7 |
8 | install: deps
9 | @coffee -o lib src/
10 | @npm install
11 |
12 | publish: deps
13 | @coffee -o lib src/
14 | @npm publish
15 |
16 | test: build
17 | @vows spec/*_spec.js --spec
18 |
19 | http: build
20 | @node lib/app.js
21 |
22 | dev: deps
23 | @coffee -wc --bare -o lib src/
24 |
25 | .PHONY: all
26 |
--------------------------------------------------------------------------------
/spec/helper.coffee:
--------------------------------------------------------------------------------
1 | require.paths.unshift(__dirname + "/../lib")
2 |
3 | Vows = require "vows"
4 | exports.Vows = Vows
5 |
6 | Assert = require "assert"
7 | exports.Assert = Assert
8 |
9 | exports.default_options =
10 | job: process.env.JENKINS_JOB || "default"
11 | server: process.env.JENKINS_SERVER || "http://localhost:8080"
12 | campfire:
13 | user: process.env.JENKINS_CAMPFIRE_USER || 42
14 | room: process.env.JENKINS_CAMPFIRE_ROOM || 42
15 | token: process.env.JENKINS_CAMPFIRE_TOKEN || "xxxxxxxxxxxxxxxxxxxxxxxx"
16 | account: process.env.JENKINS_CAMPFIRE_ACCOUNT || "unknown"
17 |
--------------------------------------------------------------------------------
/views/layout.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | <%= title %>
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | <%- body %>
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/user.coffee:
--------------------------------------------------------------------------------
1 | HTTPS = require "https"
2 |
3 | class User
4 | constructor: (@data) ->
5 | @login = @data.login
6 |
7 | memberOf: (name, callback) ->
8 | path = "/api/v2/json/user/show/#{@login}/organizations"
9 | params = { host: "github.com", path: path }
10 | isMember = false
11 |
12 | console.log path
13 | req = HTTPS.request params, (res) ->
14 | body = ""
15 | res.setEncoding "utf8"
16 | res.on "end", ->
17 | orgs = JSON.parse body
18 | orgs.organizations.forEach (element) ->
19 | if element.login == name
20 | isMember = true
21 |
22 | callback null, isMember
23 | res.on "data", (chunk) ->
24 | body += chunk
25 | req.end()
26 |
27 | exports.User = User
28 |
--------------------------------------------------------------------------------
/src/jenkins/utils/http_request.coffee:
--------------------------------------------------------------------------------
1 | Url = require "url"
2 | Http = require "http"
3 |
4 | class HttpRequest
5 | constructor: (@host) ->
6 | @url = Url.parse @host
7 | @port = @url.port
8 | @path = @url.pathname || ""
9 | @hostname = @url.hostname
10 |
11 | fetch: (path, callback) ->
12 | result = ""
13 | headers =
14 | "Accept": "application/json"
15 |
16 | params =
17 | "host": @hostname
18 | "port": @port
19 | "path": "#{@path}#{path}"
20 | "headers": headers
21 |
22 | request = Http.request params, (response) ->
23 | response.on "end", ->
24 | if response.statusCode == 200
25 | callback null, JSON.parse(result)
26 | else
27 | callback null, { "error": response.statusCode, "actions": [ ], "jobs": [ ] } # shady
28 | response.on "data", (chunk) ->
29 | result += chunk
30 | response.on "error", (err) ->
31 | callback err, { }
32 | request.end()
33 |
34 | exports.HttpRequest = HttpRequest
35 |
--------------------------------------------------------------------------------
/src/jenkins/server.coffee:
--------------------------------------------------------------------------------
1 | Job = require("jenkins/job").Job
2 | Map = require("async").map
3 | Build = require("jenkins/build").Build
4 | HttpRequest = require("jenkins/utils/http_request").HttpRequest
5 |
6 | class Server
7 | constructor: (@host) ->
8 | @client = new HttpRequest(@host)
9 | @url = @client.url
10 |
11 | job_for: (name) ->
12 | new Job @host, name
13 |
14 | job_names: (callback) ->
15 | @jobs (err, jobs) ->
16 | callback err, (job.name for job in jobs)
17 |
18 | jobs: (callback) ->
19 | self = @
20 | @client.fetch "/api/json", (err, data) ->
21 | callback err, (self.job_for(job.name) for job in data.jobs)
22 |
23 | jobs_info: (callback) ->
24 | info_callback = (job, infoCallback) ->
25 | job.branches_for "master", (err, data) ->
26 | infoCallback err, data[0]
27 |
28 | @jobs (err, jobs) ->
29 | Map jobs, info_callback, (err, results) ->
30 | callback err, results
31 |
32 | exports.Job = Job
33 | exports.Build = Build
34 | exports.Server = Server
35 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jinkies",
3 | "description": "GitHub integration with Jenkins CI",
4 | "keywords": "github post-receive hudson jenkins dashboard",
5 | "author": "Corey Donohoe",
6 | "version": "0.3.6",
7 | "licenses": [{
8 | "type": "MIT",
9 | "url": "http://github.com/atmos/jinkies/raw/master/LICENSE"
10 | }],
11 |
12 | "repository" : {
13 | "type" : "git",
14 | "url" : "http://github.com/atmos/jinkies.git"
15 | },
16 |
17 | "dependencies" : {
18 | "ejs" : ">= 0.3.1",
19 | "async" : ">= 0.1.8",
20 | "oauth" : ">= 0.9.0",
21 | "redis-node" : ">= 0.4.0",
22 | "coffee-script" : ">= 1.0.1",
23 | "express" : ">= 1.0.7",
24 | "connect-auth" : ">= 0.2.1",
25 | "optparse" : ">= 1.0.1",
26 | "ansi-color" : ">= 0.2.1"
27 | },
28 |
29 | "engines": {
30 | "node": ">=0.4.1"
31 | },
32 | "directories": {
33 | "lib": "./lib"
34 | },
35 | "main": "./lib/jinkies",
36 | "bin": {
37 | "jinkies": "./bin/jinkies"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2011 Corey Donohoe
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/spec/job_spec.coffee:
--------------------------------------------------------------------------------
1 | Helper = require "./helper"
2 | Vows = Helper.Vows
3 | assert = Helper.Assert
4 | Options = Helper.default_options
5 |
6 | Job = require("jinkies").Job
7 |
8 | Vows
9 | .describe("Jenkins Job API")
10 | .addBatch
11 | "Jenkins Jobs can":
12 | topic: ->
13 | new Job Options.server, Options.job
14 | "get the hostname": (job) ->
15 | assert.equal job.host, Options.server
16 | "get the job name": (job) ->
17 | assert.equal job.name, Options.job
18 | "Jenkins Jobs#builds can":
19 | topic: ->
20 | j = new Job Options.server, Options.job
21 | j.builds @callback
22 | "get a list of builds": (err, info) ->
23 | latest = info[0]["number"]
24 | assert.ok latest.toString().match(/\d+/)
25 | "Jenkins Jobs#status can":
26 | topic: ->
27 | j = new Job Options.server, Options.job
28 | j.status @callback
29 | "get the status of a job": (err, status) ->
30 | assert.equal status, "successful"
31 | "Jenkins Jobs#build_for can":
32 | topic: ->
33 | j = new Job Options.server, Options.job
34 | j.build_for 10, @callback
35 | "get info about a build number": (err, build) ->
36 | assert.ok build.status
37 | assert.equal build.branch, "master"
38 | assert.equal build.sha1 , "3ba21ee37953c344f698171cb2ab725ef7f9b776"
39 | assert.equal build.output, "#{Options.server}/job/#{Options.job}/10/consoleText"
40 |
41 | .export(module)
42 |
--------------------------------------------------------------------------------
/src/jenkins/utils/job_build_request.coffee:
--------------------------------------------------------------------------------
1 | Url = require "url"
2 | Http = require "http"
3 | QueryString = require "querystring"
4 |
5 | class JobBuildRequest
6 | constructor: (@url, @name, @branch, @payload) ->
7 | @port = @url.port
8 | @path = "/job/#{@name}/build"
9 | @hostname = @url.hostname
10 |
11 | @options =
12 | parameter: [
13 | {"name": "GITHUB_BRANCH", "value": @branch},
14 | {"name": "GITHUB_PAYLOAD", "value": @payload }
15 | ]
16 |
17 | trigger: (callback) ->
18 | result = ""
19 | client = Http.createClient(@port, @hostname)
20 | client.on "error", (err) ->
21 | console.log "Unable to connect to #{@host}, did you set a JENKINS_SERVER environmental variable?"
22 | callback err, { }
23 |
24 | data = QueryString.stringify({json:JSON.stringify(@options)}, "&", "=", false)
25 |
26 | postParams =
27 | "host": @hostname
28 | "Content-Length": data.length
29 | "Content-Type": "application/x-www-form-urlencoded"
30 |
31 | request = client.request "POST", @path, postParams
32 | request.on "response", (response) ->
33 | response.on "end", ->
34 | callback null, {"status": response.statusCode == 302}
35 | response.on "data", (chunk) ->
36 | result += chunk
37 | response.on "error", (err) ->
38 | callback err, { }
39 | request.end data
40 |
41 | exports.JobBuildRequest = JobBuildRequest
42 |
--------------------------------------------------------------------------------
/spec/campfire_spec.coffee:
--------------------------------------------------------------------------------
1 | Helper = require "./helper"
2 | Vows = Helper.Vows
3 | assert = Helper.Assert
4 | Options = Helper.default_options
5 |
6 | Campfire = require("jenkins/utils/campfire").Campfire
7 |
8 | Vows
9 | .describe("Jenkins Campfire Library")
10 | .addBatch
11 | "Jenkins Campfire can":
12 | topic: ->
13 | new Campfire({"token": Options.campfire.token, "account": Options.campfire.account})
14 | "retrieve its token": (campfire) ->
15 | assert.equal campfire.token, Options.campfire.token
16 | "retrieve its account name": (campfire) ->
17 | assert.equal campfire.account, Options.campfire.account
18 | #"Jenkins Campfire Users can":
19 | #topic: ->
20 | #cf = new Campfire({"token": Options.campfire.token, "account": Options.campfire.account})
21 | #cf.User Options.campfire.user, @callback
22 | #"retrieves a user's information": (user) ->
23 | #console.log user
24 | "Jenkins Campfire Rooms can":
25 | topic: ->
26 | cf = new Campfire({"token": Options.campfire.token, "account": Options.campfire.account})
27 | cf.Rooms @callback
28 | "retrieves a list of rooms": (rooms) ->
29 | console.log rooms
30 | "Jenkins Campfire Rooms can":
31 | topic: ->
32 | cf = new Campfire({"token": Options.campfire.token, "account": Options.campfire.account})
33 | room = cf.Room(Options.campfire.room)
34 | room.speak "Vows Test Post", @callback
35 | "retrieve its token": (err, data) ->
36 | assert.equal "Vows Test Post", data.message.body
37 |
38 | .export(module)
39 |
--------------------------------------------------------------------------------
/spec/server_spec.coffee:
--------------------------------------------------------------------------------
1 | Helper = require "./helper"
2 | Vows = Helper.Vows
3 | assert = Helper.Assert
4 | Options = Helper.default_options
5 |
6 | Server = require("jinkies").Server
7 |
8 | Vows
9 | .describe("Jenkins Server API")
10 | .addBatch
11 | "Jenkins Servers can":
12 | topic: ->
13 | new Server Options.server
14 | "get the hostname": (jenkins) ->
15 | assert.equal jenkins.host, Options.server
16 | "parse the hostname": (jenkins) ->
17 | assert.ok Options.server.match("#{jenkins.url.hostname}")
18 | "parse the port number": (jenkins) ->
19 | assert.ok jenkins.url.port.match(/\d+/)
20 | "Jenkins Server#job_names":
21 | topic: ->
22 | s = new Server Options.server
23 | s.job_names @callback
24 |
25 | "list the job names": (err, results) ->
26 | found = (name for name in results when name == Options.job)
27 | assert.equal found, Options.job
28 |
29 | "Jenkins Server#jobs":
30 | topic: ->
31 | s = new Server Options.server
32 | s.jobs @callback
33 |
34 | "list the jobs": (err, results) ->
35 | found = (job for job in results when job.name == Options.job)[0]
36 | assert.equal found.url, "#{Options.server}/job/#{Options.job}/"
37 | assert.equal found.name, Options.job
38 | assert.equal found.color, 'blue'
39 |
40 | "Jenkins.job can":
41 | topic: ->
42 | j = new Server Options.server
43 | j.job_for(Options.job)
44 | "get the hostname": (job) ->
45 | assert.equal job.host, Options.server
46 | "get the job name": (job) ->
47 | assert.equal job.name, Options.job
48 |
49 | .export(module)
50 |
--------------------------------------------------------------------------------
/src/jenkins/job.coffee:
--------------------------------------------------------------------------------
1 | Map = require("async").map
2 | Redis = require("redis-node")
3 | Build = require("jenkins/build").Build
4 | HttpRequest = require("jenkins/utils/http_request").HttpRequest
5 | BuildRequest = require("jenkins/utils/job_build_request").JobBuildRequest
6 |
7 | RedisClient = Redis.createClient()
8 | RedisClient.select 4
9 |
10 | class Job
11 | constructor: (@host, @name) ->
12 | @client = new HttpRequest @host
13 |
14 | info: (callback) ->
15 | @client.fetch "/job/#{@name}/api/json", (err, data) ->
16 | callback err, data
17 |
18 | builds: (callback) ->
19 | self = @
20 | @info (err, data) ->
21 | numbers = (hash.number for hash in data.builds)
22 | build_for =
23 | self.build_for.bind(self)
24 | Map numbers, build_for, (err, results) ->
25 | callback err, results
26 |
27 | status: (callback) ->
28 | @branches_for 'master', (err, branches) ->
29 | branch = branches[0] || { status: 'failed' }
30 | callback err, branch.status
31 |
32 | build_for: (number, callback) ->
33 | host = @host
34 | name = @name
35 | client = @client
36 |
37 | RedisClient.get "#{@name}:#{number}", (err, cachedBuild) ->
38 | if cachedBuild
39 | callback err, JSON.parse cachedBuild
40 | else
41 | client.fetch "/job/#{name}/#{number}/api/json", (err, data) ->
42 | build = new Build host, name, number, data
43 | if build.status != "building"
44 | build.consoleText (err, data) ->
45 | build.consoleText = data
46 | RedisClient.set "#{name}:#{number}", JSON.stringify(build), (err, data) ->
47 | callback err, build
48 | else
49 | callback err, build
50 |
51 | branches_for: (branch, callback) ->
52 | self = @
53 | @builds (err, data) ->
54 | results = (hash for hash in data when hash.branch == branch)
55 | callback err, results[0..10]
56 |
57 | triggerBuild: (branch, payload, callback) ->
58 | req = new BuildRequest @client.url, @name, branch, payload
59 | req.trigger (err, data) ->
60 | callback err, data
61 |
62 | exports.Job = Job
63 |
--------------------------------------------------------------------------------
/src/jenkins/build.coffee:
--------------------------------------------------------------------------------
1 | Url = require("url")
2 | Http = require("http")
3 |
4 | class Build
5 | constructor: (@host, @name, @number, @data) ->
6 | @sha1 = "0000000000000000000000000000000000000000"
7 | @branch = "master"
8 | @compare = ""
9 | @output = "#{@host}/job/#{@name}/#{@number}/consoleText"
10 |
11 | switch @data.result
12 | when "SUCCESS"
13 | @status = "successful"
14 | when "FAILURE"
15 | @status = "failed"
16 | else # null
17 | @status = "building"
18 |
19 | @statusClass = @statusClass()
20 |
21 | info = (action for action in @data.actions when action.parameters)
22 | if info[0]
23 | params = info[0].parameters
24 |
25 | @branch = (hash.value for hash in params when hash.name == "GITHUB_BRANCH")[0]
26 | payload = (hash.value for hash in params when hash.name == "GITHUB_PAYLOAD")[0]
27 |
28 | if payload && payload.length > 2
29 | try
30 | @payload = payload.slice(1, payload.length - 1) # strip beginning and end quote :\
31 | @payload = JSON.parse @payload
32 | @sha1 = @payload.after if @payload.after
33 | @branch = @payload.ref.split("/")[2] if @payload.ref
34 |
35 | consoleText: (callback) ->
36 | result = ""
37 | url = Url.parse @output
38 |
39 | client = Http.createClient url.port, url.hostname
40 | client.on 'error', (err) ->
41 | console.log url
42 | console.log "Unable to connect to #{@host}, did you set a JENKINS_SERVER environmental variable?"
43 | callback(err, { })
44 |
45 | request = client.request 'GET', url.pathname, {'host': url.hostname }
46 | request.on 'response', (response) ->
47 | response.on 'end', ->
48 | if response.statusCode == 200
49 | callback null, result
50 | else
51 | callback null, "Unable to fetch the output\nWTF\n"
52 | response.on 'data', (chunk) ->
53 | result += chunk
54 | response.on 'error', (err) ->
55 | callback err, { }
56 | request.end()
57 |
58 | statusClass: ->
59 | switch @status
60 | when 'successful'
61 | 'good'
62 | when 'building'
63 | 'building'
64 | else
65 | 'janky'
66 |
67 | exports.Build = Build
68 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [![Velma Dinkley said "Jinkies"][1]][2]
2 |
3 | > Her catchphrases are: "**Jinkies!**", "My glasses! I can't see without my glasses!", and "You wouldn't hit someone wearing glasses, would you?".
4 |
5 | --------------------------------------------------------------------------
6 |
7 | Jinkies is a [coffee-script](http://jashkenas.github.com/coffee-script/) interface to the [Jenkins](http://jenkins-ci.org) ([Continuous Integration](http://martinfowler.com/articles/continuousIntegration.html)) server's JSON API.
8 |
9 | I'd written half of this before I realized I was cloning a lot of [hudson.rb](https://github.com/cowboyd/hudson.rb).
10 |
11 | --------------------------------------------------------------------------
12 |
13 | You need a current [node.js](http://nodejs.org) development environment.
14 |
15 | [Setting up your environment](https://github.com/atmos/jinkies/wiki/Development)
16 | ---------------------------------------------------------------------------------------------------
17 |
18 | Jinkies is [available](https://github.com/atmos/jinkies) under an [MIT license](https://github.com/atmos/jinkies/blob/master/LICENSE).
19 |
20 | [If you care about testing your hacks](https://github.com/atmos/jinkies/wiki/Testing)
21 | -------------------------------------------------------------------------------------
22 |
23 | Jinkies has tests that you can run too.
24 |
25 | [If you're interested with GitHub integration](https://github.com/atmos/jinkies/wiki/The-Web-API)
26 | -------------------------------------------------------------------------------------
27 |
28 | Jinkies works as a post-receive endpoint for GitHub webhooks.
29 |
30 | [If you're interested in the command line](https://github.com/atmos/jinkies/wiki/Command-Line)
31 | -----------------------------------------------------------------------------------------------
32 |
33 | Jinkies gives you a simple executable that uses the library.
34 |
35 | $ bin/jinkies -h
36 | Usage jinkies [options]
37 |
38 | Available options:
39 | -h, --help Display the help information
40 | -j, --job JOB Specify the jenkins job
41 | -t, --trigger-build Trigger a build for the jenkins jobs
42 | -b, --branch BRANCH Specify the jenkins build should locate
43 | -s, --sha1 SHA1 Specify the SHA1 the jenkins build should locate
44 | -p, --express-port PORT Specify the express port, defaults to 45678
45 | -e, --express-app Start the express webhook endpoint
46 | -r, --robot Start the robot!!!!1
47 | -d, --server SERVER Specify jinkies server to use
48 | -a, --all List all the jobs on the jenkins server
49 |
50 | [1]: http://f.cl.ly/items/370L3N2X363C2S38110W/img-rn_jinkies.jpeg
51 | [2]: http://en.wikipedia.org/wiki/Velma_Dinkley
52 |
--------------------------------------------------------------------------------
/src/robot.coffee:
--------------------------------------------------------------------------------
1 | Campfire = require("jenkins/utils/campfire").Campfire
2 | HttpRequest = require("jenkins/utils/http_request").HttpRequest
3 | EventEmitter = require("events").EventEmitter
4 |
5 | class Build
6 | constructor: (@job, @number) ->
7 |
8 | info: (callback) ->
9 | @job.client.fetch "/jobs/#{@job.name}/builds/#{@number}", (err, data) ->
10 | callback err, data
11 |
12 | notify: (callback) ->
13 | self = @
14 | @info (err, data) ->
15 | sha = data.sha1.slice(0,7)
16 | number = data.number
17 | branch = data.branch
18 | reply = "Build ##{number} (#{sha}) of #{self.job.name}/#{branch}"
19 | compare = data.payload && data.payload.compare
20 | duration = data.data.duration / 1000 || 0.0
21 |
22 | switch data.status
23 | when "successful"
24 | reply += " was successful "
25 | when "failed"
26 | reply += " failed "
27 | when "building"
28 | reply += " building now "
29 | else
30 | reply += " unknown[#{data.status}] "
31 |
32 | self.status = data.status
33 |
34 | reply += "(#{Math.floor(duration)}s)."
35 | reply += " #{compare}" if compare
36 |
37 | callback err, self, reply, data.consoleText
38 |
39 | class Job
40 | constructor: (@client, @name) ->
41 | @number = null
42 |
43 | poll: (callback) ->
44 | self = @
45 | @client.fetch "/jobs/#{@name}", (err, data) ->
46 | if !self.number or data.lastCompletedBuild.number > self.number
47 | prev_number = self.number
48 | self.number = data.lastCompletedBuild.number
49 | if prev_number?
50 | build = new Build(self, self.number)
51 | build.notify (err, build, notification, output) ->
52 | callback err, build, notification, output
53 |
54 | setTimeout (->
55 | self.poll callback
56 | ), 15000
57 |
58 | class Robot extends EventEmitter
59 | constructor: (@host) ->
60 | @client = new HttpRequest(@host)
61 |
62 | findJobs: (callback) ->
63 | client = @client
64 | client.fetch "/jobs", (err, data) ->
65 | for job in data
66 | do (job) ->
67 | callback err, new Job(client, job.name)
68 |
69 | run: (callback) ->
70 | self = @
71 | @findJobs (err, job) ->
72 | job.poll (err, build, notification, output) ->
73 | self.emit("build", err, build, notification, output)
74 |
75 | class CampfireRobot
76 | constructor: (@host, @options) ->
77 | @robot = new Robot(@host)
78 | @campfire = new Campfire(@options)
79 | @room = @campfire.Room(@options.room)
80 |
81 | run: ->
82 | room = @room
83 | @robot.on "build", (err, build, notification, output) ->
84 | room.speak notification, (err, data) ->
85 | console.log "Sent: #{notification}"
86 | if build.status == "failed"
87 | room.paste output, (err, data) ->
88 | console.log "Pasted failures"
89 | @robot.run()
90 |
91 | exports.Robot = Robot
92 | exports.CampfireRobot = CampfireRobot
93 |
--------------------------------------------------------------------------------
/src/jenkins/utils/campfire.coffee:
--------------------------------------------------------------------------------
1 | HTTPS = require "https"
2 | EventEmitter = require("events").EventEmitter
3 |
4 | class Campfire extends EventEmitter
5 | constructor: (options) ->
6 | @token = options.token
7 | @rooms = options.rooms and options.rooms.split(",")
8 | @account = options.account
9 | @domain = @account + ".campfirenow.com"
10 | @authorization = "Basic " + new Buffer("#{@token}:x").toString("base64")
11 |
12 | Rooms: (callback) ->
13 | @get "/rooms", callback
14 |
15 | User: (id, callback) ->
16 | @get "/users/#{id}", callback
17 |
18 | Me: (callback) ->
19 | @get "/users/me", callback
20 |
21 | Room: (id) ->
22 | self = @
23 |
24 | show: (callback) ->
25 | self.post "/room/#{id}", "", callback
26 | join: (callback) ->
27 | self.post "/room/#{id}/join", "", callback
28 | leave: (callback) ->
29 | self.post "/room/#{id}/leave", "", callback
30 | lock: (callback) ->
31 | self.post "/room/#{id}/lock", "", callback
32 | unlock: (callback) ->
33 | self.post "/room/#{id}/unlock", "", callback
34 |
35 | # say things to this channel on behalf of the token user
36 | paste: (text, callback) ->
37 | @message text, "PasteMessage", callback
38 | sound: (text, callback) ->
39 | @message text, "SoundMessage", callback
40 | speak: (text, callback) ->
41 | @message text, "TextMessage", callback
42 | message: (text, type, callback) ->
43 | body = { message: { "body":text, "type":type } }
44 | self.post "/room/#{id}/speak", body, callback
45 |
46 | # listen for activity in channels
47 | listen: ->
48 | headers =
49 | "Host" : "streaming.campfirenow.com",
50 | "Authorization" : self.authorization
51 |
52 | options =
53 | "host" : "streaming.campfirenow.com"
54 | "port" : 443
55 | "path" : "/room/#{id}/live.json"
56 | "method" : "GET"
57 | "headers": headers
58 |
59 | request = HTTPS.request options, (response) ->
60 | response.setEncoding("utf8")
61 | response.on "data", (chunk) ->
62 | #console.log "#{new Date}: Received #{id} \"#{chunk}\""
63 | if chunk.match(/^\S+/)
64 | try
65 | chunk.split("\r").forEach (part) ->
66 | data = JSON.parse part
67 |
68 | self.emit data.type, data.id, data.created_at, data.room_id, data.user_id, data.body
69 | data
70 |
71 | response.on "end", ->
72 | console.log "Streaming Connection closed. :("
73 |
74 | response.on "error", (err) ->
75 | console.log err
76 | request.end()
77 | request.on "error", (err) ->
78 | console.log err
79 | console.log err.stack
80 |
81 | # Convenience HTTP Methods for posting on behalf of the token"d user
82 | get: (path, callback) ->
83 | @request "GET", path, null, callback
84 |
85 | post: (path, body, callback) ->
86 | @request "POST", path, body, callback
87 |
88 | request: (method, path, body, callback) ->
89 | headers =
90 | "Authorization" : @authorization
91 | "Host" : @domain
92 | "Content-Type" : "application/json"
93 |
94 | options =
95 | "host" : @domain
96 | "port" : 443
97 | "path" : path
98 | "method" : method
99 | "headers": headers
100 |
101 | if method == "POST"
102 | if typeof(body) != "string"
103 | body = JSON.stringify body
104 | options.headers["Content-Length"] = body.length
105 |
106 | request = HTTPS.request options, (response) ->
107 | data = ""
108 | response.on "data", (chunk) ->
109 | data += chunk
110 | response.on "end", ->
111 | try
112 | callback null, JSON.parse(data)
113 | catch err
114 | callback null, data || { }
115 | response.on "error", (err) ->
116 | callback err, { }
117 |
118 | if method == "POST"
119 | request.end(body)
120 | else
121 | request.end()
122 | request.on "error", (err) ->
123 | console.log err
124 | console.log err.stack
125 |
126 | exports.Campfire = Campfire
127 |
--------------------------------------------------------------------------------
/bin/jinkies:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env coffee
2 | require.paths.unshift(__dirname + "/../lib")
3 |
4 | Color = require "ansi-color"
5 | Jinkies = require "jinkies"
6 | OptParse = require "optparse"
7 |
8 | Switches = [
9 | [ "-h", "--help", "Display the help information"],
10 | [ "-j", "--job JOB", "Specify the jenkins job"],
11 | [ "-t", "--trigger-build", "Trigger a build for the jenkins jobs"],
12 | [ "-b", "--branch BRANCH", "Specify the jenkins build should locate"],
13 | [ "-s", "--sha1 SHA1", "Specify the SHA1 the jenkins build should locate"],
14 | [ "-p", "--express-port PORT", "Specify the express port, defaults to 45678"],
15 | [ "-e", "--express-app", "Start the express webhook endpoint"],
16 | [ "-r", "--robot", "Start a shell robot"],
17 | [ "-c", "--campfire", "Start a campfire notifier"],
18 | [ "-d", "--server SERVER", "Specify jinkies server to use"],
19 | [ "-a", "--all", "List all the jobs on the jenkins server"]
20 | ]
21 |
22 | Options =
23 | mode: "all"
24 | sha1: ""
25 | port: 45678
26 | job: process.env.JENKINS_JOB || "default"
27 | server: process.env.JENKINS_SERVER || "http://localhost:8080"
28 | branch: process.env.JENKINS_BRANCH || "master"
29 |
30 | Parser = new OptParse.OptionParser(Switches)
31 | Parser.banner = "Usage jinkies [options]"
32 |
33 | Parser.on "server", (opt, value) ->
34 | Options.server = value
35 |
36 | Parser.on "all", ->
37 | Options.mode = "all"
38 |
39 | Parser.on "branch", (opt, value) ->
40 | Options.branch = value
41 | Options.mode = "job"
42 |
43 | Parser.on "sha1", (opt, value) ->
44 | Options.sha1 = value
45 | Options.mode = "job"
46 |
47 | Parser.on "job", (opt, value) ->
48 | Options.job = value
49 | Options.mode = "job"
50 |
51 | Parser.on "robot", (opt, value) ->
52 | Options.mode = "robot"
53 |
54 | Parser.on "campfire", (opt, value) ->
55 | Options.mode = "campfire"
56 |
57 | Parser.on "trigger-build", (opt, value) ->
58 | Options.mode = "build"
59 |
60 | Parser.on "express-app", (opt, value) ->
61 | Options.mode = "express"
62 |
63 | Parser.on "express-port", (opt, value) ->
64 | Options.port = value
65 |
66 | Parser.on "help", (opt, value) ->
67 | console.log Parser.toString()
68 | process.exit 0
69 |
70 | Parser.parse process.ARGV
71 |
72 | server = new Jinkies.Server(Options.server)
73 |
74 | switch Options.mode
75 | when "all"
76 | server.jobs (err, jobs) ->
77 | for job in jobs
78 | do (job) ->
79 | job.color = "green" if job.color == "blue"
80 | console.log("#{Color.set(job.name, job.color)} - #{job.url}")
81 | when "job"
82 | job = server.job_for Options.job
83 | job.info (err, info) ->
84 | info.color = "green" if info.color == "blue"
85 | console.log("#{Color.set(info.name, info.color)} - #{info.url}")
86 | job.build_for info.lastSuccessfulBuild.number, (err, build) ->
87 | console.log(" Last Successful Build - ##{build.number} - #{build.branch} - #{build.sha1}")
88 | if info.lastFailedBuild
89 | job.build_for info.lastFailedBuild.number, (err, build) ->
90 | console.log(" Last Failed Build - ##{build.number} - #{build.branch} - #{build.sha1}")
91 | else
92 | console.log(" Last Failed Build - never")
93 | when "build"
94 | job = server.job_for Options.job
95 | job.triggerBuild Options.branch, "{}", (err, data) ->
96 | status = Color.set("failed", "red")
97 | status = Color.set("succeeded", "green") if data.status == true
98 | console.log "Build request for #{job.name}: #{status}"
99 |
100 | when "express"
101 | app = Jinkies.ExpressApp
102 | app.jinkies = new Jinkies.Server(Options.server)
103 | app.listen(Options.port)
104 | process.on 'uncaughtException', (err) ->
105 | console.log('Caught exception: ' + err)
106 | console.log err.stack
107 |
108 | when "robot"
109 | robot = new Jinkies.Robot(Options.server)
110 | robot.on "build", (err, build, notification) ->
111 | console.log(notification)
112 | robot.run()
113 | process.on 'uncaughtException', (err) ->
114 | console.log('Caught exception: ' + err)
115 | console.log err.stack
116 | when "campfire"
117 | campfireOptions =
118 | room: process.env.JENKINS_CAMPFIRE_ROOM
119 | token: process.env.JENKINS_CAMPFIRE_TOKEN
120 | account: process.env.JENKINS_CAMPFIRE_ACCOUNT
121 |
122 | robot = new Jinkies.CampfireRobot(Options.server, campfireOptions)
123 | robot.run()
124 | process.on 'uncaughtException', (err) ->
125 | console.log('Caught exception: ' + err)
126 | console.log err.stack
127 |
128 | #console.log(Options)
129 |
130 | # vim:ft=coffee
131 |
--------------------------------------------------------------------------------
/src/app.coffee:
--------------------------------------------------------------------------------
1 | Fs = require "fs"
2 | Auth = require "connect-auth"
3 | Path = require "path"
4 | User = require "user"
5 | Express = require "express"
6 |
7 | app = Express.createServer()
8 |
9 | Config =
10 | scope: "email,offline_access"
11 | appId: process.env.GITHUB_CLIENT_ID
12 | appSecret: process.env.GITHUB_CLIENT_SECRET
13 | callback: process.env.GITHUB_CALLBACK
14 | sessionSecret: process.env.GITHUB_SESSION_SECRET || "unknown"
15 | apiPassword: process.env.JINKIES_API_PASSWORD || "password"
16 | organization: process.env.JINKIES_ORGANIZATION || "github"
17 |
18 | apiUserPasswordFunction = (username, password, successCallback, failureCallback) ->
19 | if username == "api" && password == Config.apiPassword
20 | successCallback()
21 | else
22 | failureCallback()
23 |
24 | app.configure ->
25 | app.set "root", Path.join(__filename, "..", "..")
26 | app.set "view engine", "ejs"
27 | app.set "views", Path.join(__filename, "..", "..", "views")
28 | app.use Express.logger()
29 | app.use Express.methodOverride()
30 | app.use Express.errorHandler({ showStack: true, dumpExceptions: true })
31 | app.use Express.static Path.join(__filename, "..", "..", "public")
32 | app.use Express.cookieParser()
33 | app.use Express.session { secret: Config.sessionSecret, lifetime: 150000, reapInterval: 10000 }
34 | app.use(Auth([ Auth.Anonymous(), Auth.Basic({validatePassword: apiUserPasswordFunction}), Auth.Github(Config) ]))
35 | app.use Express.bodyParser()
36 |
37 | # Oauth related Callbacks
38 | app.get "/auth/github/callback", (req, res) ->
39 | req.authenticate ["github"], (err, success) ->
40 | if success
41 | user = new User.User req.getAuthDetails().user
42 | user.memberOf Config.organization, (err, isMember) ->
43 | if isMember
44 | res.redirect "/jobs"
45 | else
46 | res.redirect "/auth/failure"
47 | else
48 | res.redirect "/auth/failure"
49 |
50 | app.get "/auth/failure", (req, res) ->
51 | res.render "failure", { title: "Unable to Authenticate, bummer." }
52 |
53 | app.get "/auth/login", (req, res) ->
54 | res.render "login", { title: "Login to our Janky CI Server with GitHub!" }
55 |
56 | app.get "/auth/logout", (req, res) ->
57 | req.logout()
58 | res.redirect("/jobs", 303)
59 |
60 | # GitHub post-receives land here
61 | app.post "/", (req, res) ->
62 | info = JSON.parse(req.body.payload)
63 | owner = info.repository.owner.name
64 | branch = info["ref"].split("/")[2]
65 |
66 | project = "#{owner}-#{info.repository.name}"
67 |
68 | if info.deleted or (info.created and info.commits.length == 0)
69 | # ignore branch deletions, and new branches with no commits
70 | else
71 | job = app.jinkies.job_for project
72 | job.triggerBuild branch, req.body.payload, (err, data) ->
73 | res.send data, {"Content-Type": "application/json"}, 200
74 |
75 | # Everything else requires Basic-Auth or Oauth
76 | app.all "*", (req, res, next) ->
77 | if req.headers.accept == "application/json"
78 | if req.socket.remoteAddress == '127.0.0.1'
79 | next()
80 | else
81 | req.authenticate ["basic"], (err, success) ->
82 | if success
83 | next()
84 | else
85 | res.send "Unauthorized", 401
86 | else
87 | if req.isAuthenticated()
88 | next()
89 | else
90 | res.redirect "/auth/login"
91 |
92 | # API for jobs info
93 | app.get "/jobs", (req, res) ->
94 | app.jinkies.jobs_info (err, jobs) ->
95 | if req.headers.accept == "application/json"
96 | res.send jobs, {"Content-Type": "application/json"}, 200
97 | else
98 | res.render "index", { title: "Janky CI Server", jobs: jobs }
99 |
100 | app.get "/jobs/:job", (req, res) ->
101 | job = app.jinkies.job_for req.params.job
102 | job.info (err, info) ->
103 | res.send info, {"Content-Type": "application/json"}, 200
104 |
105 | app.post "/jobs/:job/builds", (req, res) ->
106 | job = app.jinkies.job_for req.params.job
107 | branch_name = req.body.branch || "master"
108 | job.triggerBuild branch_name, "{}", (err, data) ->
109 | res.send data, {"Content-Type": "application/json"}, 200
110 |
111 | app.get "/jobs/:job/builds/:build", (req, res) ->
112 | job = app.jinkies.job_for req.params.job
113 | job.build_for req.params.build, (err, build) ->
114 | res.send build, {"Content-Type": "application/json"}, 200
115 |
116 | app.get "/jobs/:job/builds/:build/console", (req, res) ->
117 | name = req.params.job
118 | number = req.params.build
119 | job = app.jinkies.job_for name
120 | job.build_for number, (err, build) ->
121 | title = "Build output for #{name} # #{number}"
122 | res.render "console", { title: title, output: build.consoleText }
123 |
124 | app.get "/jobs/:job/branches/:branch", (req, res) ->
125 | job = app.jinkies.job_for req.params.job
126 | job.branches_for req.params.branch, (err, branches) ->
127 | res.send branches, {"Content-Type": "application/json"}, 200
128 |
129 | exports.App = app
130 |
--------------------------------------------------------------------------------
/public/css/base.css:
--------------------------------------------------------------------------------
1 | /*------------------------------------------------------------------------------------
2 | @group Global Reset
3 | ------------------------------------------------------------------------------------*/
4 | * {
5 | padding:0;
6 | margin:0;
7 | }
8 | h1, h2, h3, h4, h5, h6, p, pre, blockquote, label, ul, ol, dl, fieldset, address { margin:1em 0; }
9 | li, dd { margin-left:5%; }
10 | fieldset { padding: .5em; }
11 | select option{ padding:0 5px; }
12 |
13 | .access{ display:none; } /* For accessibility related elements */
14 | .clear{ clear:both; height:0px; font-size:0px; line-height:0px; overflow:hidden; }
15 | a{ outline:none; }
16 | a img{ border:none; }
17 |
18 | .clearfix:after {
19 | content: ".";
20 | display: block;
21 | height: 0;
22 | clear: both;
23 | visibility: hidden;
24 | }
25 | * html .clearfix {height: 1%;}
26 | .clearfix {display:inline-block;}
27 | .clearfix {display: block;}
28 |
29 | /* @end */
30 |
31 | /*----------------------------------------------------------------------------
32 | @group Base Layout
33 | ----------------------------------------------------------------------------*/
34 |
35 | body{
36 | margin:0;
37 | padding:0;
38 | font-size:14px;
39 | line-height:1.6;
40 | font-family:Helvetica, Arial, sans-serif;
41 | background:#fff;
42 | }
43 |
44 | #wrapper{
45 | margin:0 auto;
46 | width:600px;
47 | }
48 | .wide #wrapper{
49 | width:1000px;
50 | }
51 |
52 | h2#logo{
53 | width:600px;
54 | margin:0 auto 25px auto;
55 | }
56 | h2#logo a{
57 | display:block;
58 | height:156px;
59 | text-indent:-9999px;
60 | text-decoration:none;
61 | background:url(../images/logo.png);
62 | }
63 |
64 | .content{
65 | padding:5px;
66 | background:#ededed;
67 | border-radius:4px;
68 | }
69 | .content > .inside{
70 | border:1px solid #ddd;
71 | background:#fff;
72 | border-radius:3px;
73 | }
74 |
75 | /* @end */
76 |
77 | /*----------------------------------------------------------------------------
78 | @group Builds
79 | ----------------------------------------------------------------------------*/
80 |
81 | ul.builds{
82 | margin:0;
83 | }
84 |
85 | ul.builds li{
86 | list-style-type:none;
87 | margin:0;
88 | padding:12px 10px;
89 | border-bottom:1px solid #e5e5e5;
90 | border-top:1px solid #fff;
91 | background:-webkit-gradient(linear, left top, left bottom, from(#fdfdfd), to(#f2f2f2));
92 | background:-moz-linear-gradient(top, #fdfdfd, #f2f2f2);
93 | }
94 | ul.builds li:first-child{
95 | border-top:none;
96 | border-top-left-radius: 3px;
97 | border-top-right-radius: 3px;
98 | }
99 | ul.builds li:last-child{
100 | border-bottom:none;
101 | border-bottom-left-radius: 3px;
102 | border-bottom-right-radius: 3px;
103 | }
104 | ul.builds li:hover{
105 | background:-webkit-gradient(linear, left top, left bottom, from(#f5f9fb), to(#e9eef0));
106 | background:-moz-linear-gradient(top, #f5f9fb, #e9eef0);
107 | }
108 | ul.builds li.building:hover{
109 | background:-webkit-gradient(linear, left top, left bottom, from(#fdfdfd), to(#f2f2f2));
110 | background:-moz-linear-gradient(top, #fdfdfd, #f2f2f2);
111 | }
112 |
113 | ul.builds a{
114 | display:block;
115 | text-decoration:none;
116 | background:url(../images/disclosure-arrow.png) 100% 10px no-repeat;
117 | }
118 | ul.builds li:hover a{
119 | background-position:100% -90px;
120 | }
121 | ul.builds .building a{
122 | background:none;
123 | cursor:default;
124 | }
125 |
126 | ul.builds .status{
127 | float:left;
128 | margin-top:5px;
129 | margin-right:10px;
130 | width:37px;
131 | height:34px;
132 | background:url(../images/robawt-status.gif) 0 0 no-repeat;
133 | }
134 | ul.builds .building .status{
135 | background:url(../images/building-bot.gif);
136 | }
137 | ul.builds .janky .status{
138 | background-position:0 -200px;
139 | }
140 |
141 | ul.builds h2{
142 | margin:0;
143 | font-size:16px;
144 | text-shadow:0 1px #fff;
145 | }
146 | ul.builds .good a h2{
147 | color:#358c00;
148 | }
149 | ul.builds .building a h2{
150 | color:#e59741;
151 | }
152 | ul.builds .janky a h2{
153 | color:#ae0000;
154 | }
155 |
156 | ul.builds p{
157 | margin:-2px 0 0 0;
158 | font-size:13px;
159 | font-weight:200;
160 | color:#666;
161 | text-shadow:0 1px #fff;
162 | }
163 | ul.builds .building p{
164 | color:#999;
165 | }
166 |
167 | /* @end */
168 |
169 | /*----------------------------------------------------------------------------
170 | @group Text Styles
171 | ----------------------------------------------------------------------------*/
172 |
173 | pre{
174 | margin:10px;
175 | font-size:12px;
176 | overflow:auto;
177 | }
178 | pre::-webkit-scrollbar {
179 | height: 8px;
180 | width: 8px;
181 | }
182 | pre::-webkit-scrollbar-track-piece{
183 | margin-bottom:10px;
184 | background-color: #e5e5e5;
185 | border-bottom-left-radius: 4px 4px;
186 | border-bottom-right-radius: 4px 4px;
187 | border-top-left-radius: 4px 4px;
188 | border-top-right-radius: 4px 4px;
189 | }
190 |
191 | pre::-webkit-scrollbar-thumb:vertical{
192 | height: 25px;
193 | background-color: #ccc;
194 | -webkit-border-radius: 4px;
195 | -webkit-box-shadow: 0 1px 1px rgba(255,255,255,1);
196 | }
197 | pre::-webkit-scrollbar-thumb:horizontal{
198 | width: 25px;
199 | background-color: #ccc;
200 | -webkit-border-radius: 4px;
201 | }
202 |
203 | /* @end */
--------------------------------------------------------------------------------