Skip to content

nubank/clj-github-app

Repository files navigation

clj-github-app

Build Status codecov Clojars Project

A library to implement GitHub Apps in Clojure.

[nubank/clj-github-app "0.2.0"]

Includes:

Usage

Checking webhook signatures

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-signaturex-hub-signature parameter was blank or nil.
  • :clj-github-app.webhook-signature/not-checked — no check was done because webhook-secret parameter was blank or nil.

Authenticating as a GitHub App

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. Usually https://api.github.com or something like https://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-optsclj-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. Given installation-id and opts, makes an HTTP request to GitHub API, automatically retrieving an access token.
    Uses clj-http, opts argument is given to request function as described here. opts is supposed to include :method and :url keys. This function is the main workhorse.
  • request — same as request*, 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 as app-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" {})

Convenience wrappers for API endpoints

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.

Development

With every commit, add important changes from it to the "Unreleased" section of CHANGELOG.md.

Release procedure

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

License

Copyright © 2018 Dmitrii Balakhonskii

Distributed under the Eclipse Public License version 1.0.