Skip to content

Commit

Permalink
Merge branch 'start'
Browse files Browse the repository at this point in the history
  • Loading branch information
mike-north committed Dec 12, 2019
2 parents a43abb5 + 92ceb76 commit 2c2e6d1
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 62 deletions.
45 changes: 45 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
WEBHOOK_PROXY_URL=https://smee.io/--------
# GitHub API endpoint
API_BASE_URL="https://api.github.com"

#
# GitHub User's USERNAME
#
GH_USER=""

#
# GitHub User's PERSONAL ACCESS TOKEN which can be created here:
#
# https://github.com/settings/tokens/new?scopes=public_repo,read:user
#
GH_USER_TOKEN=""

#
# Once you've created a GITHUB APP here:
#
# https://github.com/settings/apps/new
#
# you'll see an "app id", "client id" and "client secret" at the top of the
# settings page
#
# https://github.com/settings/apps/YOUR_APP_NAME#private-key
#
# for your app. Fill in those values below
GH_APP_ID=""
GH_APP_CLIENT_ID=""
GH_APP_CLIENT_SECRET=""

#
# At the very bottom of your app's settings page
#
# https://github.com/settings/apps/YOUR_APP_NAME#private-key
#
# you'll find a "Private Keys"section, with a "Generate Private Key" button.
# Clicking it will download a file to your computer.
#
# (1) Place that file in the `./private` folder so that it will be ignored by Git.
# (2) Ensure that the GH_APP_PRIVATE_KEY_PATH variable leads to the relative path
# of that file
#
#
GH_APP_PRIVATE_KEY_PATH="./private/gh-app.key"
41 changes: 31 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,41 @@
# Python GitHub App

This app is meant to serve as an application to help you onboard to the GitHub ecosystem and start using GitHub Apps.
This app is meant to serve as an application to help you onboard to the GitHub ecosystem and start using GitHub Webhooks & Apps.

## How to set it up and use it
## Getting Started

### Initial Project Setup

- Clone/Fork this repo
- Generate your virutal environment - `python3 -m venv venv`
- Activate your environment - `source venv/bin/activate`
- Install dependencies - `pip3 install -r requirements.txt`
- Run the app - `flask run`
- Generate your virutal environment

```sh
python3 -m venv venv
```

- Activate your environment

```sh
source venv/bin/activate
```

- Install dependencies

```sh
pip3 install -r requirements.txt
```

- Run the app

```
flask run
```

## How to use setup and install the app
### Connecting to GitHub

- Create a new repository at https://github.com/li-playground/
- Create a new public repository at https://github.com/new
- Visit https://smee.io/ and click on `Start a new Channel` and note the URL
- Create a new GitHub App - https://github.com/organizations/li-playground/settings/apps/new
- Create a new GitHub App - https://github.com/settings/apps/new
- Give it a distinct name and description (prefix your LDAP)
- Set `Homepage URL` = `http://localhost:5000/`
- Set `User authorization callback URL` = `http://localhost:5000/authenticate/`
Expand All @@ -23,7 +44,7 @@ This app is meant to serve as an application to help you onboard to the GitHub e
- Select the Radio Button for `Enable SSL verification`
- Under permissions, give `Read & Write` permissions for `Pull Requests`
- Under `Subscribe to Events`, check `Pull Request`
- Generate and Download the `Private key`, move it to `private` folder in your app on local machine and name it `gh-app.key`
- Generate and Download the `Private key`, move it to your app folder on local machine and name it `./private/gh-app.key`
- Hit `Save Changes`

Now you should be redirected to the App Settings -
Expand Down
31 changes: 16 additions & 15 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from bot_config import API_BASE_URL, validate_env_variables
from gh_token import get_token, store_token
# from gh_oauth_token import get_token, store_token
from gh_utils import make_github_rest_api_call
from webhook_handlers import add_pr_comment

Expand All @@ -12,7 +12,6 @@
import markdown2

from flask import Flask, request, redirect, render_template
from flask_apscheduler import APScheduler
from objectify_json import ObjectifyJSON

log = logging.getLogger(__name__)
Expand All @@ -38,22 +37,23 @@ def welcome():
============
Dynamic routes that are needed to facilitate the authentication flow
"""
We will let you know when it's appropriate to un-comment this
"""

@app.route("/authenticate/<app_id>", methods=["GET"])
def authenticate(app_id):
"""Incoming Installation Request. Accept and get a new token."""
try:
app_id = str(app_id)
installation_id = request.args.get('installation_id')
store_token(get_token(app_id, installation_id))
# @app.route("/authenticate/<app_id>", methods=["GET"])
# def authenticate(app_id):
# """Incoming Installation Request. Accept and get a new token."""
# try:
# app_id = str(app_id)
# installation_id = request.args.get('installation_id')
# store_token(get_token(app_id, installation_id))

except Exception:
log.error("Unable to get and store token.")
traceback.print_exc(file=sys.stderr)
# except Exception:
# log.error("Unable to get and store token.")
# traceback.print_exc(file=sys.stderr)

return redirect("https://www.github.com", code=302)
# return redirect("https://www.github.com", code=302)


@app.route('/webhook', methods=['POST'])
Expand All @@ -74,8 +74,9 @@ def process_message():
- Is github SENDING webhooks to the same https://smee.io URL you're RECEIVING from?
"""
log.info('Incoming webhook')
webhook = ObjectifyJSON(request.json)
log.info(
f'Incoming webhook [{webhook.action}]: {json.dumps(webhook, sort_keys=True, indent=4)}')

# Let's react only when a new Pull Requests has been opened.
if request.headers['X-Github-Event'] == 'pull_request' and str(webhook.action).lower() == 'opened':
Expand Down
26 changes: 26 additions & 0 deletions bin/start_smee
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/bin/bash
test ! -f .env && \
echo "Could not find a .env file. You may want to run:" && \
echo "" && \
echo " cp .env.example .env" && \
echo "" && exit 1

# setup environment based on .env
export $(egrep -v '^#' .env | xargs)

# check for the presence of WEBHOOK_PROXY_URL
eval val=\""\$WEBHOOK_PROXY_URL"\"
if [[ -z "${val}" ]]; then
echo "🛑 WEBHOOK_PROXY_URL has not been set. Please provide a value in your .env file"
echo ""
exit 1
else
echo "✅ Starting webhook proxy w/ WEBHOOK_PROXY_URL=$WEBHOOK_PROXY_URL"
echo ""
fi

# allow for the detection of the running webhook proxy
export WEBHOOK_PROXY_IS_RUNNING=1
pysmee forward $WEBHOOK_PROXY_URL http://localhost:5000/webhook
# indicate that the webhook proxy has been shut down
unset WEBHOOK_PROXY_IS_RUNNING
31 changes: 23 additions & 8 deletions bot_config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
import sys
import logging
from functools import reduce

logging.basicConfig(stream=sys.stdout, level=logging.INFO,
format='[%(levelname)8s] %(process)s:%(name)s\t%(message)s')
Expand All @@ -18,15 +19,29 @@
"""


GITHUB_USER = os.getenv("GITHUB_USER", -1)
GITHUB_TOKEN = os.getenv("GITHUB_TOKEN", -1)
GH_USER = os.getenv("GH_USER", -1)
GH_USER_TOKEN = os.getenv("GH_USER_TOKEN", -1)
API_BASE_URL = os.getenv("API_BASE_URL", -1)
GH_APP_ID = os.getenv("GH_APP_ID", -1)
GH_APP_CLIENT_ID = os.getenv("GH_APP_CLIENT_ID", -1)
GH_APP_CLIENT_SECRET = os.getenv("GH_APP_CLIENT_SECRET", -1)
GH_APP_PRIVATE_KEY_PATH = os.getenv("GH_APP_PRIVATE_KEY_PATH", -1)


def validate_env_variables():
if (GITHUB_USER == -1 or GITHUB_TOKEN.count == 0):
log.warn("GITHUB_USER env variable has not yet been set")
if (GITHUB_TOKEN == -1 or GITHUB_TOKEN.count == 0):
log.warn("GITHUB_TOKEN env variable has not yet been set")
if (API_BASE_URL == -1 or API_BASE_URL.count == 0):
log.warn("API_BASE_URL env variable has not yet been set")
env_vars = {
"GH_USER": GH_USER,
"GH_USER_TOKEN": GH_USER_TOKEN,
"API_BASE_URL": API_BASE_URL,
"GH_APP_ID": GH_APP_ID,
"GH_APP_CLIENT_ID": GH_APP_CLIENT_ID,
"GH_APP_CLIENT_SECRET": GH_APP_CLIENT_SECRET,
"GH_APP_PRIVATE_KEY_PATH": GH_APP_PRIVATE_KEY_PATH
}

blanks_msg = reduce(lambda acc, item:
acc + "\n\t\t\t\t" + item[0] if (item[1] == -1 or item[1] == "") else acc, env_vars.items(), "")
log.warn(
"The following environment variables were found to be empty:\n"
+ blanks_msg
+ "\n\n\t\t\t\tThis may be fine, depending on which exercise you're currently working on")
33 changes: 24 additions & 9 deletions gh_token.py → gh_oauth_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,20 @@

log = logging.getLogger(__name__)

"""
TO WORKSHOP ATTENDEES:
======================
You should not have to touch anything in this file. It deals with
building and signing the JWT necessary to facilitate OAuth 2.0
authentication and authorization w/ GitHub.
"""

# The paths of two things that should never be checked into git
_token_storage_path = f'private/.secret'
_private_key_path = f'private/gh-app.key'


def get_token(app_id, installation_id):
"""Get a token from GitHub."""
Expand All @@ -33,7 +47,8 @@ def get_token(app_id, installation_id):
encoded = jwt.encode(params, private_key,
algorithm='RS256').decode("utf-8")
headers = {'Accept': 'application/vnd.github.machine-man-preview+json',
'Authorization': f'Bearer {encoded}'}
'Authorization': f'Bearer {encoded}' # OAuth 2.0
}

# Send request to GitHub.
response = requests.post(token_url, headers=headers)
Expand All @@ -54,27 +69,27 @@ def get_token(app_id, installation_id):
def store_token(token_json):
if token_json:
try:
if os.path.exists(f".secret"):
os.unlink(f".secret")
if os.path.exists(_token_storage_path):
os.unlink(_token_storage_path)

with open(f".secret", 'w') as secret_file:
with open(_token_storage_path, 'w') as secret_file:
secret_file.write(json.dumps(token_json))

except Exception as exc:
log.error(f'Could not write secret file.\n{exc}')
traceback.print_exc(file=sys.stderr)

else:
log.error("Invalid token for app")
log.error("Invalid (empty) token for app")


def peek_app_token():
"""Peek on secret file that has the token, deserialize it and return the dict."""
if not os.path.exists(f".secret"):
if not os.path.exists(_token_storage_path):
return None

try:
with open(f".secret") as secret_file:
with open(_token_storage_path) as secret_file:
return json.loads(secret_file.read())

except Exception as exc:
Expand Down Expand Up @@ -125,11 +140,11 @@ def retrieve_token():

def get_private_key():
"""Read private key from hidden file and return it."""
if not os.path.exists(".private-key"):
if not os.path.exists(_private_key_path):
return None

try:
with open(".private-key") as secret_file:
with open(_private_key_path) as secret_file:
return secret_file.read()

except Exception as exc:
Expand Down
Loading

0 comments on commit 2c2e6d1

Please sign in to comment.