Adding Continuous Deployment to your CI

CI what now?! Continuous Integration is the practice of automating your testing so you can frequently merge your changes to your main branch. The smaller and more frequent the changes the better. But this is only half of the story. Having your GitHub repo up to date is great but what if you could also have those changes deployed automatically? Welcome to Continuous Deployment!

GitHub Actions

We have our example GitHub Action which checks out our code and runs some basic testing

name: Example Pipeline

on:
  push:
    branches: [ main ]
  pull_request:
  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:

jobs:
  Testing:
    runs-on: ubuntu
    steps:
      - name: Checkout Code
        uses: actions/checkout@v3
      
      - name: Set up Python 3.10
        uses: actions/setup-python@v4
        with:
          python-version: '3.10'

      - name: Install Pipenv and run PyLint
        run: |
          pipenv install
          pipenv run pylint *.py

      - name: PyTesting
        run: pipenv run pytest -v tests/

This would live in your .github/workflow/example_pipeline.yml file. If you are unfamiliar spend some time reading the docs: https://docs.github.com/en/actions

Creating our Tornado Script

On our running server we are going to create a simple tornado script which will listen for a webhook request. Upon receiving the webhook request it will update our repo.

#!/usr/bin/env python

from pathlib import Path
import json
from tornado.web import Application, RequestHandler
from tornado.ioloop import IOLoop

class Main(RequestHandler):
  def post(self) -> None:
    body = json.loads(self.request.body.decode('utf-8'))
    if 'secret' not in body.keys():
      # Fail anything that doesn't provide our secret
      logging.warning('%s failed to provide an API key', remote_ip)
      self.set_status(404)
      self.write({'message':'API Key not specified'})
      return
    if body['secret'] == 'YoUrReAlLyHorrIbLeSecrEtKey!':
      # Only if our secret matches
      Path(f"{envs['base']}/{body['repo']}/.update").touch()
      self.set_status(200)
      self.write({'message': 'Thank you for request'})

def app():
  urls = [
    ("/api/", Main)
  ]
  return Application(urls, debug=True)

app = make_app()
app.listen(3000)
IOLoop.instance().start()

Our script takes a message body of {'secret': 'Your Key', 'repo': 'Your repo name'}. This allows us to authenticate the request and to control the scope of what our tornado script can do should a malicous person guess our super difficult secret. Its important whenever we are exposing things on the internet that we think about attack vectors.

The cron job and the update

Our cron is going to be super simple for this example as will the bash script. I would encourage you to write something more defensive that will look for possible errors and alert to problems.

# Every 5 minutes call our update script
*/5 * * * * ubuntu /bin/bash ${HOME}/MyRepo/update.sh

Our Update script
#!/bin/bash

if [[ -f "${HOME}/MyRepo/.update" ]]; then
  cd "${HOME}/MyRepo/" || exit 1
  rm .update || exit 1
  git checkout main && git pull
  echo "$(date)" > .last_updated
fi

Putting it all together

We should now have our tornado script running on our server listening for the webhook. Upon receiving the webhook a .update file will be recreated. That in turn will be picked up by our cron’d script running every 5 minutes resulting in the repo being updated and the .last_updated file recording the datetime.

The last step is to add the following to the bottom of our GitHub Action

      - name: PyTesting
        run: pipenv run pytest -v tests/
      - name: Deployment
        # Only deploy if on branch == main
        if: github.ref == 'refs/heads/master' 
        run: curl -X POST http://<your home IP>:3000 -d '{'secret': 'YoUrReAlLyHorrIbLeSecrEtKey!', 'repo': 'MyRepo'}

If you have a dynamic IP address and want to use a static domain name have a look at services like https://www.noip.com/.

And that’s it! You now have Continuous Deployment to your Continuous Integration!

Leave a Reply0

Your email address will not be published. Required fields are marked *


This site uses Akismet to reduce spam. Learn how your comment data is processed.