Skip to content

Commit 78d16a8

Browse files
committed
Other dep updates and set up CD
1 parent 1ab16ae commit 78d16a8

File tree

6 files changed

+329
-29
lines changed

6 files changed

+329
-29
lines changed

.cpanel.yml

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
deployment:
3+
tasks:
4+
- /bin/bash /home/werewolf/repositories/paste/deploy2.sh

.github/workflows/deploy.yml

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: Deploy to production
2+
on:
3+
push:
4+
branches:
5+
- master
6+
7+
jobs:
8+
deploy:
9+
name: Deploy
10+
runs-on: ubuntu-latest
11+
steps:
12+
- name: Checkout
13+
uses: actions/checkout@v2
14+
- name: Setup python
15+
uses: actions/setup-python@v2
16+
with:
17+
python-version: 3.7
18+
- name: Install dependencies
19+
run: pip install -r requirements.txt
20+
- name: Run deployment
21+
run: python deploy1.py
22+
env:
23+
DEBUG: 0
24+
CPANEL_ENV: production
25+
CPANEL_TOKEN: ${{ secrets.CPANEL_TOKEN }}
26+
CPANEL_API_URL: "https://werewolf.chat:2083"
27+
CPANEL_API_USER: werewolf
28+
CPANEL_REPO_PATTERN: "/home/werewolf/repositories/{repo}"
29+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
30+
GITHUB_REPO: ${{ github.repository }}
31+
GITHUB_OWNER: ${{ github.repository_owner }}
32+
GITHUB_COMMIT: ${{ github.sha }}
33+
GITHUB_RUN_ID: ${{ github.run_id }}

app.py

+4-5
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@
55
from flask import Flask, request, redirect, jsonify, abort, render_template
66
from flask_talisman import Talisman
77
from pygments import highlight
8-
from pygments.lexers import Python3TracebackLexer
8+
from pygments.lexers import PythonTracebackLexer
99
from pygments.formatters import HtmlFormatter
1010
import db
1111
import config
1212
import webhook
13-
from style import SimpleStyle
1413

1514
app = Flask(__name__)
1615
Talisman(app, content_security_policy={"default-src": "'self' 'unsafe-inline'"})
@@ -89,9 +88,9 @@ def get_paste(slug):
8988
if _wants_json():
9089
return jsonify({"status": "success", "data": data["paste_content"], "expires": data["paste_expires"]})
9190
output = highlight(data["paste_content"],
92-
Python3TracebackLexer(),
93-
HtmlFormatter(full=True, linenos="table", lineanchors="l",
94-
anchorlinenos=True, wrapcode=True))
91+
PythonTracebackLexer(),
92+
HtmlFormatter(full=True, linenos="table", lineanchors="l",
93+
anchorlinenos=True, wrapcode=True))
9594
# get rid of red boxes. Tried to do this the "official" way but pygments hates me
9695
return output.replace(r'class="err"', "")
9796

deploy1.py

+186
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import json
2+
import requests
3+
import sys
4+
import logging
5+
import pprint
6+
import time
7+
import os
8+
import functools
9+
10+
# this script is executed in a github actions container
11+
12+
DEBUG_MODE = int(os.environ["DEBUG"])
13+
CLEANUP = []
14+
15+
def run():
16+
gh_owner = os.environ["GITHUB_OWNER"]
17+
gh_owner_repo = os.environ["GITHUB_REPO"]
18+
gh_repo = gh_owner_repo[len(gh_owner)+1:]
19+
20+
cp_repo = os.environ["CPANEL_REPO_PATTERN"].format(owner=gh_owner, repo=gh_repo)
21+
gh_deploy = "repos/{}/deployments".format(gh_owner_repo)
22+
23+
# add some logging about web requests that we make
24+
if DEBUG_MODE:
25+
logging.basicConfig(level=logging.DEBUG)
26+
27+
# make a new deployment in github
28+
res = github_api("POST", gh_deploy, {
29+
"ref": os.environ["GITHUB_SHA"],
30+
"task": "deploy",
31+
"required_contexts": [],
32+
"auto_merge": False,
33+
"environment": os.environ["CPANEL_ENV"]
34+
})
35+
36+
gh_deploy_status = "{}/{}/statuses".format(gh_deploy, res["id"])
37+
gh_status_url = "https://github.com/{}/actions/runs/{}".format(gh_owner_repo, os.environ["GITHUB_RUN_ID"])
38+
res = github_api("POST", gh_deploy_status, {
39+
"state": "pending",
40+
"target_url": gh_status_url,
41+
"description": "Initializing deployment"
42+
})
43+
44+
CLEANUP.append(functools.partial(github_api, "POST", gh_deploy_status, {
45+
"state": "error",
46+
"target_url": gh_status_url,
47+
"description": "Aborted due to error in GitHub Actions run"
48+
}))
49+
50+
# Updates the repo to the latest master revision
51+
res = cpanel_api("VersionControl", "update", {
52+
"repository_root": cp_repo,
53+
"branch": "master"
54+
})
55+
56+
# check if we can execute a deploy
57+
# if this fails, it's because the repo on the webserver has changes
58+
# or is missing a .cpanel.yml file with deployment steps
59+
if not res["data"]["deployable"]:
60+
raise RuntimeError("Repository is not deployable, aborting!")
61+
62+
# create a deployment
63+
res = cpanel_api("VersionControlDeployment", "create", {
64+
"repository_root": cp_repo
65+
})
66+
cp_deploy_id = res["data"]["deploy_id"]
67+
68+
# mark deployment as queued
69+
res = github_api("POST", gh_deploy_status, {
70+
"state": "queued",
71+
"target_url": gh_status_url,
72+
"description": "Deployment {} queued at {} (task {})".format(res["data"]["deploy_id"],
73+
res["data"]["timestamps"]["queued"],
74+
res["data"]["task_id"])
75+
})
76+
77+
# fetch deployment status (note: uses a custom API module since built-in
78+
# one doesn't let us filter on a specific deployment id)
79+
# Custom API module source code is available at:
80+
# https://gist.github.com/skizzerz/85a0a2d4a6176ffa67004cd22c507799
81+
cur_state = "queued"
82+
success = False
83+
i = 0
84+
while True:
85+
i += 1
86+
# if the deployment is taking forever, give up on checking status
87+
if i > 60:
88+
res = github_api("POST", gh_deploy_status, {
89+
"state": "error",
90+
"target_url": gh_status_url,
91+
"description": "No deployment response after 5 minutes, aborting action"
92+
})
93+
break
94+
print("Sleeping for 5 seconds then checking status", flush=True)
95+
time.sleep(5)
96+
res = cpanel_api("VCDeployStatus", "retrieve", query={
97+
"deploy_id": cp_deploy_id
98+
})
99+
100+
if res["data"]["timestamps"].get("succeeded", None):
101+
res = github_api("POST", gh_deploy_status, {
102+
"state": "success",
103+
"auto_inactive": True,
104+
"target_url": gh_status_url,
105+
"description": "Deployment {} succeeded at {} (task {})".format(res["data"]["deploy_id"],
106+
res["data"]["timestamps"]["succeeded"],
107+
res["data"]["task_id"])
108+
})
109+
success = True
110+
break
111+
if res["data"]["timestamps"].get("failed", None):
112+
res = github_api("POST", gh_deploy_status, {
113+
"state": "failure",
114+
"target_url": gh_status_url,
115+
"description": "Deployment {} FAILED at {} (task {})".format(res["data"]["deploy_id"],
116+
res["data"]["timestamps"]["failed"],
117+
res["data"]["task_id"])
118+
})
119+
break
120+
if res["data"]["timestamps"].get("canceled", None):
121+
res = github_api("POST", gh_deploy_status, {
122+
"state": "failure",
123+
"target_url": gh_status_url,
124+
"description": "Deployment {} CANCELED at {} (task {})".format(res["data"]["deploy_id"],
125+
res["data"]["timestamps"]["canceled"],
126+
res["data"]["task_id"])
127+
})
128+
break
129+
if cur_state == "queued" and res["data"]["timestamps"].get("active", None):
130+
res = github_api("POST", gh_deploy_status, {
131+
"state": "in_progress",
132+
"target_url": gh_status_url,
133+
"description": "Deployment {} began running at {} (task {})".format(res["data"]["deploy_id"],
134+
res["data"]["timestamps"]["active"],
135+
res["data"]["task_id"])
136+
})
137+
cur_state = "in_progress"
138+
else:
139+
print("No new deployment status updates", flush=True)
140+
141+
CLEANUP.clear()
142+
return success
143+
144+
def cpanel_api(module, endpoint, body=None, query=None):
145+
headers = {
146+
"Authorization": "cpanel {}:{}".format(os.environ["CPANEL_API_USER"], os.environ["CPANEL_TOKEN"])
147+
}
148+
149+
url = "{}/execute/{}/{}".format(os.environ["CPANEL_API_URL"], module, endpoint)
150+
method = "POST" if body else "GET"
151+
print(method, url, flush=True)
152+
r = requests.request(method, url, headers=headers, params=query, data=body)
153+
r.raise_for_status()
154+
data = r.json()
155+
if DEBUG_MODE or not data["status"]:
156+
pprint.pprint(data)
157+
print("", flush=True)
158+
if not data["status"]:
159+
raise RuntimeError("API call was unsuccessful, aborting!")
160+
return data
161+
162+
def github_api(method, endpoint, body=None):
163+
headers = {
164+
"Authorization": "Bearer {}".format(os.environ["GITHUB_TOKEN"]),
165+
# opt into preview letting us set additional deployment status types
166+
# remove this line once it hits stable API
167+
"Accept": "application/vnd.github.flash-preview+json, aplication/vnd.github.ant-man-preview+json"
168+
}
169+
170+
url = "https://api.github.com/{}".format(endpoint)
171+
print(method, url, flush=True)
172+
r = requests.request(method, url, headers=headers, json=body)
173+
data = r.json()
174+
if DEBUG_MODE or r.status_code >= 400:
175+
pprint.pprint(data)
176+
print("", flush=True)
177+
r.raise_for_status()
178+
return data
179+
180+
if __name__ == "__main__":
181+
try:
182+
if not run():
183+
sys.exit(1)
184+
finally:
185+
for callback in CLEANUP:
186+
callback()

deploy2.sh

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
#!/bin/bash
2+
3+
# this script is executed on the webserver
4+
5+
# there's a bunch of sleep commands to guard things that don't necessarily
6+
# complete in realtime; should evaluate if they're actually necessary
7+
# and if they are how small we can make them
8+
9+
##########
10+
# set up correct version of python/pip
11+
##########
12+
echo "Initializing deployment"
13+
export VIRTUAL_ENV_DISABLE_PROMPT=1
14+
source /home/werewolf/virtualenv/paste/3.7/bin/activate
15+
16+
SOURCEPATH=/home/werewolf/repositories/paste
17+
DESTPATH=/home/werewolf/paste
18+
BAKPATH=/home/werewolf/paste.bak
19+
20+
##########
21+
# make a backup
22+
##########
23+
echo "Creating backup"
24+
rm -rf $BAKPATH
25+
mv $DESTPATH $BAKPATH
26+
27+
##########
28+
# ensure dependencies are up to date
29+
##########
30+
echo "Ensuring updated dependencies"
31+
cd $SOURCEPATH
32+
pip install --upgrade -r requirements.txt
33+
34+
##########
35+
# copy files
36+
##########
37+
echo "Copying files"
38+
mkdir $DESTPATH
39+
mkdir $DESTPATH/tmp
40+
cp -r . $DESTPATH
41+
42+
##########
43+
# cleanup unnecessary files
44+
##########
45+
rm -rf $DESTPATH/.git
46+
rm -rf $DESTPATH/.github
47+
rm -f $DESTPATH/.gitignore
48+
rm -f $DESTPATH/.cpanel.yml
49+
rm -f $DESTPATH/deploy*
50+
51+
##########
52+
# restart app
53+
##########
54+
echo "Restarting webapp"
55+
touch $DESTPATH/tmp/restart.txt
56+
sleep 5
57+
58+
##########
59+
# test if app runs successfully
60+
##########
61+
SUCCESS=0
62+
curl -f https://ww.chat > /dev/null
63+
if [ $? -ne 0 ]; then
64+
SUCCESS=1
65+
fi
66+
67+
sleep 5
68+
if [ -s $DESTPATH/stderr.log ]; then
69+
SUCCESS=1
70+
fi
71+
72+
##########
73+
# restore backup if app didn't run successfully
74+
##########
75+
if [ SUCCESS -eq 1 ]; then
76+
echo "Webapp failed, restoring backup. Below is what was found in the log:"
77+
cat $DESTPATH/stderr.log
78+
rm -rf $DESTPATH
79+
mv $BAKPATH $DESTPATH
80+
touch $DESTPATH/tmp/restart.txt
81+
sleep 5
82+
curl -f https://ww.chat > /dev/null
83+
echo "Backup restored"
84+
fi
85+
86+
echo "Deployment complete"
87+
exit $SUCCESS

requirements.txt

+15-24
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,19 @@
1-
asn1crypto==1.2.0
2-
certifi==2019.9.11
3-
cffi==1.13.2
1+
certifi==2020.6.20
2+
cffi==1.14.3
43
chardet==3.0.4
5-
Click==7.0
6-
cryptography==3.2
7-
elasticsearch==7.1.0
8-
Flask==1.1.1
9-
flask-mwoauth==0.4.75
4+
click==7.1.2
5+
cryptography==3.2.1
6+
Flask==1.1.2
107
flask-talisman==0.7.0
11-
future==0.18.2
12-
idna==2.8
8+
idna==2.10
139
itsdangerous==1.1.0
14-
Jinja2==2.10.3
10+
Jinja2==2.11.2
1511
MarkupSafe==1.1.1
16-
mwoauth==0.3.7
17-
mysql-connector-python==8.0.18
18-
oauthlib==3.1.0
19-
protobuf==3.10.0
20-
pycparser==2.19
21-
Pygments==2.4.2
22-
PyJWT==1.7.1
23-
python-dateutil==2.8.1
24-
requests==2.22.0
25-
requests-oauthlib==1.3.0
26-
six==1.13.0
27-
urllib3==1.25.7
28-
Werkzeug==0.16.0
12+
mysql-connector-python==8.0.22
13+
protobuf==3.13.0
14+
pycparser==2.20
15+
Pygments==2.7.2
16+
requests==2.24.0
17+
six==1.15.0
18+
urllib3==1.25.11
19+
Werkzeug==1.0.1

0 commit comments

Comments
 (0)