joewiz
5/10/2015 - 7:23 AM

Receive payloads from BitBucket.org POST hooks with eXist

Receive payloads from BitBucket.org POST hooks with eXist

xquery version "3.1";

(: 
Receive payloads from BitBucket.org POST hooks with eXist

Prerequisites:

1. A post-January 2015 build of the develop branch of eXist, which adds support 
for XQuery 3.1's JSON parsing and serialization features. 

a) Check out and `build.sh` from:

  https://github.com/exist-db/exist
  
b) Or install a nightly from http://static.adamretter.org.uk/exist-nightly/ 
(these seem to have stopped in February, but that should still do.)

2. A BitBucket account and repository.

Setup:

1. Save this script as /db/apps/bbxq/payload.xq in your eXist database. Set 
execute permissions on the script to world-executable, because BitBucket will 
need to call this service via HTTP POST request.  (Assuming you're using eXide,
do this via File > Manager, select payload.xq, select the "i" (info) icon, 
and check all of the "execute" checkboxes. Then select the Apply button, then 
the Close button.)

Now the script should be accessible in your browser via:

  http://localhost:8080/exist/apps/bbxq/payload.xq

You should see this message:

  <response status="fail">
    <message>No post data received</message>
  </response>

This is to be expected.

2. Open monex to view the console logging output

  http://localhost:8080/exist/apps/monex/console.html

Reload payload.xq in the browser, and you should see some logging of your 
request's headers and the the same <response> as before in the console window.
  
3. You need a public URL for your eXist server. If you don't have one, install
ngrok from https://ngrok.com/. This gives you a publicly reachable domain name, 
that tunnels to your computer on a port you determine. The ngrok command for 
opening port 8080 is

  ngrok http 8080

This command then displays the URL that ngrok has assigned you. Now pull up 
payload.xq via this URL:

  https://YOUR-NGROK-ID.ngrok.io/exist/apps/bbxq/payload.xq

Confirm that you see the same results as in steps 1 and 2 above.

4. Configure a POST hook on your BitBucket repository, using these directions:

  https://confluence.atlassian.com/display/BITBUCKET/POST+hook+management

When prompted for the URL where Bitbucket should send its update messages,
enter the publicly reachable URL for your payload.xq.

5. Push a commit to your repository. You should see the following console logs:

  <response status="success">
    <message>Payload received from Bitbucket.org.</message>
  </response>

  {
    "repository": {
        "website": "",
        "fork": false,
        "name": "test",
        "scm": "git",
        "owner": "joewiz",
        "absolute_url": "/joewiz/test/",
        "slug": "test",
        "is_private": true
    },
    "truncated": false,
    "commits": [{
        "node": "fb2a7271e09a",
        "files": [{
            "type": "modified",
            "file": "README.md"
        }],
        "raw_author": "Joe Wicentowski <joewiz@gmail.com>",
        "utctimestamp": "2015-05-10 04:17:18+00:00",
        "author": "joewiz",
        "timestamp": "2015-05-10 06:17:18",
        "raw_node": "fb2a7271e09a4e393213c770330daf3d648423a1",
        "parents": ["f809e677d8e7"],
        "branch": "master",
        "message": "README.md edited online with Bitbucket",
        "revision": null,
        "size": -1
    }],
    "canon_url": "https://bitbucket.org",
    "user": "joewiz"
  }

6. That's it! With this info, you can then use BitBucket's REST API to fetch
the changed files.

:)

import module namespace console="http://exist-db.org/xquery/console";

declare namespace output = "http://www.w3.org/2010/xslt-xquery-serialization";

let $post-data := request:get-data()
let $log := console:log(string-join(request:get-header-names() ! concat(., ': ', request:get-header(.)), '  --  ')) 
let $json-serialization-parameters := 
    <output:serialization-parameters>
        <output:method>json</output:method>
        <output:indent>yes</output:indent>
    </output:serialization-parameters>
let $response-and-data := 
    if (not(empty($post-data))) then
        if (request:get-header('Content-Type') = 'application/x-www-form-urlencoded') then
            if (request:get-parameter-names() = 'payload') then
                try {
                    let $payload := request:get-parameter('payload', '')
                    let $json-data := parse-json($payload)
                    return
                        if (starts-with(request:get-header('User-Agent'), 'Bitbucket.org')) then
                            (
                            <response status="success">
                                <message>Payload received from Bitbucket.org.</message>
                            </response>
                            ,
                            serialize($json-data, $json-serialization-parameters)
                            )
                        else 
                            <response status="fail">
                                <message>Bitbucket User-Agent check failed: {request:get-header('User-Agent')}.</message>
                            </response>
                } catch * {
                    <response status="fail">
                        <message>There was an unexpected problem. {concat($err:code, ": ", $err:description, ' (', $err:module, ' ', $err:line-number, ':', $err:column-number, ')')}</message>
                    </response>
                }
            else 
                <response status="fail">
                    <message>Expected a POST Parameter named 'payload', but only received '{string-join(request:get-parameter-names(), ', ')}'.</message>
                </response>
        else 
            <response status="fail">
                <message>Expected a Content-Type header of 'application/x-www-form-urlencoded', but received '{request:get-header('Content-Type')}'.</message>
            </response>
     else    
        <response status="fail">
            <message>No post data received</message>
        </response>
return
    (
    $response-and-data[1], 
    console:log($response-and-data)[1],
    console:log($response-and-data)[2]
    )