Skip to content

Commit a60da57

Browse files
authored
Refactor action: migrate deployment logic from Bash script to Python (#3)
* Migrate deployment logic from Bash script to Python * Update README.md
1 parent 3b78755 commit a60da57

File tree

11 files changed

+246
-66
lines changed

11 files changed

+246
-66
lines changed

.dockerignore

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
__pycache__/
2+
*.pyc
3+
*.pyo
4+
*.egg-info/
5+
*.egg
6+
*.log
7+
*.tmp
8+
*.swp
9+
venv/
10+
.env
11+
.git
12+
.gitignore
13+
.DS_Store
14+
node_modules/
15+
tests/
16+
test/
17+
README.md

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
__pycache__
2+
*.pyc
3+
*.egg-info
4+
venv
5+
.env
6+
*.log
7+
*.tmp

Dockerfile

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
FROM ubuntu:22.04
1+
FROM python:3.11-slim
22

3-
RUN apt-get update && \
4-
apt-get install -y openssh-client sshpass git python3 python3-pip
3+
WORKDIR /app
4+
ENV PYTHONPATH="/app"
55

6-
COPY deploy.sh /deploy.sh
7-
RUN chmod +x /deploy.sh
6+
COPY requirements.txt .
7+
RUN pip install --no-cache-dir -r requirements.txt
8+
9+
COPY deploy/ ./deploy/
10+
11+
CMD ["python", "-m", "deploy.main"]
812

9-
ENTRYPOINT ["/deploy.sh"]

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Before using this action, make sure that:
1919
- Pulls the latest code from the `main` branch
2020
- Installs Python dependencies with `pip`
2121
- Runs `collectstatic` and `migrate`
22+
- Runs tests
2223
- Restarts the web server by touching the `WSGI` file
2324

2425

@@ -65,7 +66,7 @@ jobs:
6566
# Required: path to your WSGI file
6667
wsgi_file: /var/www/webapp_name_wsgi.py
6768
```
68-
-💡 Use `ssh.pythonanywhere.com` for US accounts or `ssh.eu.pythonanywhere.com` for EU-based accounts.
69+
> 💡 Use `ssh.pythonanywhere.com` for US accounts or `ssh.eu.pythonanywhere.com` for EU-based accounts.
6970
🔐 You must provide **at least one** of the following: `password` or `ssh_private_key`
7071

7172

action.yml

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: PythonAnywhere Redeploy Action
2-
description: Redeploy a Django app to PythonAnywhere via SSH using Docker.
2+
description: Redeploy a Django app to PythonAnywhere via SSH.
33
author: MiguelRizzi
44

55
branding:
@@ -33,11 +33,11 @@ inputs:
3333
runs:
3434
using: docker
3535
image: Dockerfile
36-
args:
37-
- ${{ inputs.ssh_host }}
38-
- ${{ inputs.username }}
39-
- ${{ inputs.password }}
40-
- ${{ inputs.ssh_private_key }}
41-
- ${{ inputs.working_directory }}
42-
- ${{ inputs.venv_directory }}
43-
- ${{ inputs.wsgi_file }}
36+
env:
37+
SSH_HOST: ${{ inputs.ssh_host }}
38+
USERNAME: ${{ inputs.username }}
39+
PASSWORD: ${{ inputs.password }}
40+
SSH_PRIVATE_KEY: ${{ inputs.ssh_private_key }}
41+
WORKING_DIRECTORY: ${{ inputs.working_directory }}
42+
VENV_DIRECTORY: ${{ inputs.venv_directory }}
43+
WSGI_FILE: ${{ inputs.wsgi_file }}

deploy.sh

Lines changed: 0 additions & 50 deletions
This file was deleted.

deploy/__init__ .py

Whitespace-only changes.

deploy/deploy_manager.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
from .logger import logger
2+
3+
class DeployManager:
4+
def __init__(self, ssh, workdir, venvdir, wsgi):
5+
self.ssh = ssh
6+
self.workdir = workdir
7+
self.venv = f"source {venvdir}/bin/activate"
8+
self.wsgi = wsgi
9+
self.prev_commit = ""
10+
11+
def post_pull_tasks(self, changes):
12+
def changed(path): return any(path in f for f in changes)
13+
14+
if changed("requirements.txt"):
15+
logger.info("requirements.txt changed. Installing dependencies...")
16+
self.ssh.exec(f"cd {self.workdir} && {self.venv} && pip install -r requirements.txt")
17+
18+
if any("models.py" in f or f.startswith("migrations/") for f in changes):
19+
logger.info("Model or migration changes detected. Running migrations...")
20+
self.ssh.exec(f"cd {self.workdir} && {self.venv} && python manage.py makemigrations")
21+
self.ssh.exec(f"cd {self.workdir} && {self.venv} && python manage.py migrate")
22+
23+
if any("static" in f for f in changes):
24+
logger.info("Static files changed. Running collectstatic...")
25+
self.ssh.exec(f"cd {self.workdir} && {self.venv} && python manage.py collectstatic --noinput")
26+
27+
logger.info("Running tests...")
28+
out, err = self.ssh.exec(f"cd {self.workdir} && {self.venv} && python manage.py test")
29+
if err and not "Ran 0 tests" in err:
30+
logger.error("Tests failed")
31+
raise Exception(err)
32+
else:
33+
logger.info("Tests passed successfully.")
34+
35+
def get_changed_files(self):
36+
out, _ = self.ssh.exec(f"cd {self.workdir} && git rev-parse HEAD")
37+
self.prev_commit = out.strip()
38+
logger.info(f"Previous commit: {self.prev_commit}")
39+
40+
logger.info("Checking for changes in the remote repository...")
41+
self.ssh.exec(f"cd {self.workdir} && git fetch origin")
42+
out, _ = self.ssh.exec(f"cd {self.workdir} && git diff --name-only origin/main..HEAD")
43+
return out.splitlines()
44+
45+
def rollback_to_previous_commit(self):
46+
logger.warning("Rolling back to previous commit...")
47+
self.ssh.exec(f"cd {self.workdir} && git reset --hard {self.prev_commit}")
48+
49+
def deploy(self):
50+
try:
51+
changes = self.get_changed_files()
52+
if changes:
53+
logger.info("Changed files:\n " + "\n ".join(changes))
54+
self.ssh.exec(f"cd {self.workdir} && git pull origin main")
55+
logger.info("Pulled latest changes from remote repository.")
56+
try:
57+
self.post_pull_tasks(changes)
58+
except Exception:
59+
logger.exception("Error during post-pull tasks")
60+
self.rollback_to_previous_commit()
61+
logger.warning("Rolled back due to test failure.")
62+
raise
63+
else:
64+
logger.info("No files changed.")
65+
66+
logger.info("Reloading application (touch WSGI)...")
67+
self.ssh.exec(f"touch {self.wsgi}")
68+
logger.info("Deployment completed successfully!")
69+
70+
except Exception:
71+
logger.exception("Error during deployment")
72+
self.rollback_to_previous_commit()
73+
raise

deploy/logger.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import logging
2+
3+
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(funcName)s - %(message)s')
4+
handler = logging.StreamHandler()
5+
handler.setFormatter(formatter)
6+
7+
logger = logging.getLogger(__name__)
8+
logger.addHandler(handler)
9+
logger.setLevel(logging.INFO)

deploy/main.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import os
2+
3+
from .logger import logger
4+
from .ssh_connector import SSHConnector
5+
from .deploy_manager import DeployManager
6+
7+
def verify_env_variables():
8+
required_vars = [
9+
"SSH_HOST", "USERNAME", "WORKING_DIRECTORY", "VENV_DIRECTORY", "WSGI_FILE"
10+
]
11+
12+
missing = [var for var in required_vars if not os.environ.get(var)]
13+
if missing:
14+
msg = f"Missing environment variables: {', '.join(missing)}"
15+
logger.critical(msg)
16+
raise EnvironmentError(msg)
17+
18+
if not (os.environ.get("PASSWORD") or os.environ.get("SSH_PRIVATE_KEY")):
19+
msg = "Neither PASSWORD nor SSH_PRIVATE_KEY is set"
20+
logger.error(msg)
21+
raise EnvironmentError(msg)
22+
23+
24+
def main():
25+
verify_env_variables()
26+
ssh = SSHConnector(
27+
hostname=os.environ.get("SSH_HOST"),
28+
port=22,
29+
username=os.environ.get("USERNAME"),
30+
password=os.environ.get("PASSWORD"),
31+
key_filename=None,
32+
private_key_str=os.environ.get("SSH_PRIVATE_KEY"),
33+
)
34+
try:
35+
ssh.connect()
36+
if ssh.client:
37+
manager = DeployManager(
38+
ssh,
39+
workdir=os.environ.get("WORKING_DIRECTORY"),
40+
venvdir=os.environ.get("VENV_DIRECTORY"),
41+
wsgi=os.environ.get("WSGI_FILE")
42+
)
43+
manager.deploy()
44+
except Exception:
45+
logger.exception("Fatal error during deployment")
46+
finally:
47+
ssh.disconnect()
48+
49+
if __name__ == "__main__":
50+
main()

0 commit comments

Comments
 (0)