DEV Community

Play Button Pause Button
Matt Hamilton
Matt Hamilton

Posted on

Using IBM Cloud Functions for Github Hooks

So you want to get Github to "do something" on a commit? Well, serverless technologies like IBM Cloud Functions (based on Apache Openwhisk) could be the answer for you.

A 1080p version of this video is on Cinnamon here.

This is part two of a live coding session in which I show how to create an IBM Cloud Function in Python to look up a user's PayID and resolve their payment address to pay them some XRP on commit. Part one is here:

Once again, I was joined by my colleague, Si Metson, for the show and we were working on this together with a view to submit it to the PayID hackathon currently running.

Recap of the session

I'm going to run through the basic steps we covered in both part I and II of this session. I will be taking examples from the code we wrote during the session, although may omit or simplify some bits for focus in this post.

Setting up Cloud Functions

Firstly you will need an account on IBM Cloud, which you can get here.

You will need to download the IBM Cloud CLI, login, install the cloud functions plugin, create and target a namespace:

$ ibmcloud login -a  
$ ibmcloud plugin install cloud-functions
$ ibmcloud namespace create mynamespace
$ ibmcloud fn property set --namespace mynamespace
Enter fullscreen mode Exit fullscreen mode

Code For Our Webhook

We then need to create our code to be called. I've simplified this a bit from the video to make it a bit easier to blog about. But there are three main functions:

A function pay_xrp_testnet that takes a wallet seed (secret key) for a wallet on the XRP Ledger testnet and will make a payment to a destination address of a certain amount using the Xpring Python API. For the sake of brevity this function is not waiting to confirm that the transaction was successful.

def pay_xrp_testnet(wallet_seed, address, amount):
    # Create a wallet instance using the seed
    wallet = xpring.Wallet.from_seed(wallet_seed)

    # The XRP testnet
    url = 'test.xrp.xpring.io:50051'
    client = xpring.Client.from_url(url)

    # Create a transaction
    txn = client.send(wallet, address, str(amount))

    # Submit txn to the network
    res = client.submit(txn)

    return res
Enter fullscreen mode Exit fullscreen mode

We then have a function, get_address_from_payid, that given a PayID will form an HTTP request to fetch the contents of the PayID and parse it for the address for the network and environment we want:

def get_address_from_payid(payid, network, environment):
    # Convert the PayID into a URL e.g.
    # pay$username.github.io -> https://username.github.io/pay
    local_part, domain = payid.split('$')
    url = f'https://{domain}/{local_part}'
    response = requests.get(url)
    response.raise_for_status()

    data = response.json()

    # Look for an address that matches the network
    # and environment we want to use
    for address in data['addresses']:
        if address['paymentNetwork'] == network and \
           address['environment'] == environment:
            return address['addressDetails']['address']
Enter fullscreen mode Exit fullscreen mode

Again for the sake of brevity here, we are not handling errors if we can't resolve or parse the PayID.

And finally, we have our main function that is actually called by the webhook:

def main(args):
    # The wallet seed (secret key) is passed in, as a bound
    # parameter to this function
    xrp_wallet_seed = args['xrp_wallet_seed']

    # Extract the username of the pusher from the
    # Github hook payload
    pusher = args['pusher']['name']

    # Assume a PayID on Github of this form
    payid = f'pay${pusher}.github.io'

    # Calculate the amount based on number of commits
    # this is just an example and could be any metric
    try:
        num_commits = len(args['commits'])
    except KeyError:
        num_commits = 1
    amount = 1000 * num_commits

    # Get the address from the PayID and make payment
    address = get_address_from_payid(payid, 'XRPL', 'TESTNET')
    res = pay_xrp_testnet(xrp_wallet_seed, address, amount)

    return {
        'address': address,
        'amount': amount,
    }
Enter fullscreen mode Exit fullscreen mode

In this version we are just assuming that a Github user with username foo will have a PayID setup of pay$foo.github.io. But in the future we'd like to parse the PayID from the commit message itself.

We calculate how much to pay the contributor based on the number of commits in this push multiplied by 1000 drops. There are 1e6 drops in 1 XRP.

I've detailed in another post how to setup a PayID using Github Pages:

Creating a Cloud Function

Now how to get create the Cloud function? Well this was slightly complicated by the fact we need to have access to the xpring library, which is not included by default. So we created our own Docker image, the Dockerfile being:

FROM ibmfunctions/action-python-v3.7

RUN pip install --upgrade pip
RUN pip install -r requirements.txt

RUN pip install xpring[py] requests
Enter fullscreen mode Exit fullscreen mode

This bases off the original docker image that IBM Cloud Functions uses for Python functions and installs the xpring library to it.

I then built and pushed the Docker image to Dockerhub:

$ docker build -t hammertoe/twitch_payid:0.1 . 
$ docker push hammertoe/twitch_payid:0.1
Enter fullscreen mode Exit fullscreen mode

I can then refer to it when I create my cloud function. I also bind a value of xrp_wallet_seed to the function with the secret key of my wallet. Obviously take care with this secret as with it anyone can empty your account (in this case it is just the testnet, so no issue). By passing --web true we allow this function to be accessed from the web.

$ ic fn action create webhook webhook.py --docker hammertoe/twitch_payid:0.1 --param xrp_wallet_seed snzBUmvTTAzCCRwGvGfKeA6Zqn4Yf --web true 
ok: created action github_pay
Enter fullscreen mode Exit fullscreen mode

Configure the webhook in Github

To configure the webhook in Github, you need to first get the URL of the function we just created:

$ ic fn action get github_pay --url
ok: got action github_pay
https://eu-gb.functions.cloud.ibm.com/api/v1/web/2155dac7-8c22-4c44-8241-987ae2557df6/default/github_pay
Enter fullscreen mode Exit fullscreen mode

The can then put that URL with .json on the end into our Github hook settings. Also note we are not setting a secret here, which we would want to configure in a real world system to ensure our Cloud Function can only be called by Github and not someone else randomly.

Screenshot of setting up Github hook

Results

Here is an animated gif showing a commit being pushed and the payment arriving a few seconds later in my XRP wallet:

Animated gif of github commit being paid in XRP

The next steps will be to take this and expand it to be a bit more generic and secure to run in the real world.

The full code to this session is, as always, in the following Github Repo:

GitHub logo IBMDeveloperUK / ML-For-Everyone

Resources, notebooks, assets for ML for Everyone Twitch stream

ML for Everyone

ML For everyone is a live show broadcast on the IBM Developer Twitch channel every week at 2pm UK time.

The show is presented by Matt Hamilton and focusses on topics in and around machine learning for developers, with the odd smattering of random Python topics now and then.

Preview of show

Past shows

I hope you enjoyed the video, if you want to catch them live, I stream each week at 2pm UK time on the IBM Developer Twitch channel:

https://developer.ibm.com/livestream

Top comments (0)