A library to implement GitHub Apps in Clojure.
[nubank/clj-github-app "0.2.0"]
Includes:
- Webhook payload signature checker with secure comparison.
- API client with HTTP connection pool.
- Access token manager with caching (Authenticating with GitHub Apps is tricky).
When implementing a webhook handler, it is recommended to check the webhook request signature before processing it. Please read the official documentation first.
Imagine you have a webhook handler:
(ns your-project.webhooks
(:require [clj-github-app.webhook-signature :as webhook-signature]))
(def GITHUB_WEBHOOK_SECRET (System/getenv "GITHUB_WEBHOOK_SECRET"))
(defn post-github
"Checks if the webhook is valid and handles it."
[request]
(let [{:strs [x-github-delivery x-github-event x-hub-signature-256]} (:headers request)
payload (slurp (:body request))]
(case (webhook-signature/check-payload-signature-256 GITHUB_WEBHOOK_SECRET x-hub-signature-256 payload)
::webhook-signature/missing-signature {:status 400 :body "x-hub-signature-256 header is missing"}
::webhook-signature/wrong-signature {:status 401 :body "x-hub-signature-256 does not match"}
(let [parsed-payload (json/parse-string payload keyword)]
;; process your webhook here
{:status 200 :body "This is fine."}))))
The key part here is the call to check-payload-signature-256
. It takes 3 arguments:
webhook-secret
— the exact secret string that you set when configuring webhook for your repo.
If this argument is blank or nil,check-payload-signature-256
will do nothing and return:clj-github-app.webhook-signature/not-checked
.x-hub-signature-256
— contents of "X-Hub-Signature-256" request header.payload
— request body as a string.
Possible return values:
:clj-github-app.webhook-signature/ok
— signature matches the payload.:clj-github-app.webhook-signature/wrong-signature
— signature does not match the payload.:clj-github-app.webhook-signature/missing-signature
—x-hub-signature
parameter was blank or nil.:clj-github-app.webhook-signature/not-checked
— no check was done becausewebhook-secret
parameter was blank or nil.
Please read Authenticating with GitHub Apps official documentation first.
Example (uses mount-lite):
(ns your-project.external.github
(:require [mount.lite :as m]
[clj-github-app.client :as client]))
(def GITHUB_API_URL "https://api.github.com")
(def GITHUB_APP_ID (System/getenv "GITHUB_APP_ID"))
(def GITHUB_APP_PRIVATE_KEY_PEM (System/getenv "GITHUB_APP_PRIVATE_KEY_PEM"))
(m/defstate client
:start (client/make-app-client GITHUB_API_URL GITHUB_APP_ID GITHUB_APP_PRIVATE_KEY_PEM {})
:stop (.close @client))
clj-github-app.client/make-app-client
takes 4 parameters:
github-api-url
— Base URL of GitHub API. Usuallyhttps://api.github.com
or something likehttps://github.example.com/api/v3
for GHE.github-app-id
— GitHub App ID as string (can be found on the app settings page).private-key-pem-str
— String contents of the private key file that you generated when configuring the app.connection-pool-opts
— clj-http connection pool parameters. Can be set to{}
to use all defaults.
It returns an object that implements AutoCloseable
interface and AppClient
protocol, which has the following functions:
request*
— to authenticate as an installation. Giveninstallation-id
andopts
, makes an HTTP request to GitHub API, automatically retrieving an access token.
Uses clj-http,opts
argument is given torequest
function as described here.opts
is supposed to include:method
and:url
keys. This function is the main workhorse.request
— same asrequest*
, but has separate arguments for method and URL.app-request*
— to authenticate as a GitHub App. This is only useful for querying app metadata.app-request
— same asapp-request*
, but has separate arguments for method and URL.
You can authenticate as a GitHub App:
(client/app-request* @client {:method :get :url "/app" :accept "application/vnd.github.machine-man-preview+json"})
(client/app-request @client :get "/app" {:accept "application/vnd.github.machine-man-preview+json"})
You can also authenticate as an installation. For this you need Installation ID, (which is usually given to you in webhook payloads):
(client/request* @client 42 {:method :get :url "/repos/myname/myrepo/issues/123/comments")
(client/request @client 42 :get "/repos/myname/myrepo/issues/123/comments" {})
All these functions can accept either a full URL or just a relative path, which will be automatically appended to the base
GitHub API URL, given earlier to make-app-client
.
The "path only" mode is useful when you are constructing the URL yourself and don't want to repeat the base API URL there.
The path can start with a /
or not, which makes no difference, both cases are handled the same way.
The "full URL" mode is useful when you use a URL extracted from a webhook payload
and don't want to strip the base URL part from there.
;; Use github-api-url (provided earlier to make-app-client) as base API URL
(client/app-request @client :get "foo" {})
(client/app-request @client :get "/foo" {})
;; The same call, but without relying on github-api-url
(client/app-request @client :get "https://api.github.com/foo" {})
This library does not provide any wrappers like
(list-issue-comments "owner" "repo" "123" {:since "2018-01-01"})
Such wrappers are really easy to implement on your own:
(defn create-list-issue-comments-request [owner repo issue-number params]
{:method :get
:url (format "/repos/%s/%s/issues/%s/comments" owner repo issue-number)
:query-params params})
and then use like this:
(client/request @client 42 (create-list-issue-comments-request "owner" "repo" "123" {:since "2018-01-01"}))
Full GitHub API reference can be found here.
With every commit, add important changes from it to the "Unreleased" section of CHANGELOG.md.
Run lein release
as described below, depending on how much changes are made since previous release.
Library version will be updated in project.clj and README.md automatically after calling lein release
.
## Unreleased
section int CHANGELOG.md will be automatically changed into the version being released.
lein release :patch
# or
lein release :minor
# or
lein release :major
Copyright © 2018 Dmitrii Balakhonskii
Distributed under the Eclipse Public License version 1.0.