mirror of
https://github.com/MaxenceG2M/github-release-notifier.git
synced 2025-12-08 22:03:24 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8dba474e23 | |||
| 07dccee235 | |||
| fd7e6b73d1 | |||
| 0ee46a5d9e | |||
| 9709947c28 | |||
| 89e88c8a1b | |||
| 148c647ac7 | |||
| 02fad7767c | |||
| 74e6488044 | |||
| f3380139ed | |||
| 92953dbb5c | |||
| 11834803c8 | |||
| bd5bf11661 |
2
.git-blame-ignore-revs
Normal file
2
.git-blame-ignore-revs
Normal file
@@ -0,0 +1,2 @@
|
||||
# Black format commit
|
||||
89e88c8a1b26e2c6ca242d0bba6cdba8da35c3ae
|
||||
28
.pre-commit-config.yaml
Normal file
28
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,28 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.5.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: check-toml
|
||||
- id: check-added-large-files
|
||||
- id: check-merge-conflict
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.11.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.12.0
|
||||
hooks:
|
||||
- id: isort
|
||||
name: isort
|
||||
- repo: https://github.com/PyCQA/autoflake
|
||||
rev: v2.1.1
|
||||
hooks:
|
||||
- id: autoflake
|
||||
- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
|
||||
rev: v2.11.0
|
||||
hooks:
|
||||
- id: pretty-format-yaml
|
||||
args: [--autofix, --indent, '2', --offset, '2']
|
||||
10
Dockerfile
Normal file
10
Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
||||
FROM python:3.10-alpine3.18
|
||||
|
||||
RUN pip install requests
|
||||
WORKDIR /app
|
||||
COPY notifier.py template.html /app/
|
||||
|
||||
# TODO Dev purporse
|
||||
COPY conf.ini /app/conf.ini
|
||||
|
||||
ENTRYPOINT ["python3", "/app/notifier.py"]
|
||||
57
Justfile
Normal file
57
Justfile
Normal file
@@ -0,0 +1,57 @@
|
||||
# https://github.com/casey/just
|
||||
|
||||
vbin := "./venv/bin"
|
||||
pip := vbin / "pip"
|
||||
python := vbin / "python"
|
||||
|
||||
last_commit_sha1 := `git rev-parse --short HEAD`
|
||||
remote_image_name := "gitea.gdemontauzan.fr/maxenceg2m/github-release-notifier"
|
||||
remote_build_image := remote_image_name + ":" + last_commit_sha1
|
||||
|
||||
# Run the script
|
||||
run: init
|
||||
{{ python }} notifier.py
|
||||
|
||||
# Init python virtual env
|
||||
init:
|
||||
python3 -m venv venv
|
||||
{{ pip }} install --requirement requirements.txt
|
||||
|
||||
# Clean workspace - remove venv - and init
|
||||
reinit: hclean init
|
||||
|
||||
# Remove virtual env (venv)
|
||||
hclean:
|
||||
rm -fr venv
|
||||
|
||||
# Run docker compose then show logs
|
||||
dup: dbuild
|
||||
docker compose up -d
|
||||
docker compose logs
|
||||
|
||||
# Build with docker compose
|
||||
dbuild:
|
||||
docker compose build
|
||||
|
||||
# Down docker compose then build
|
||||
drebuild: ddown dbuild
|
||||
|
||||
# Down docker compose
|
||||
ddown:
|
||||
docker compose down
|
||||
|
||||
# Docker build without cache
|
||||
dforce-build:
|
||||
docker compose build --no-cache
|
||||
|
||||
# Push a working images on registry, tagged with commit-sha1
|
||||
dpush: dbuild
|
||||
docker tag github-release-notifier {{ remote_build_image }}
|
||||
docker push {{ remote_build_image }}
|
||||
echo "To push a tagged version, do 'just release <version>'"
|
||||
|
||||
# Release a version: create a tagged images, push it and create a git tag
|
||||
release version: dbuild
|
||||
docker tag github-release-notifier {{ remote_image_name }}:{{ version }}
|
||||
docker push {{ remote_image_name }}:{{ version }}
|
||||
git tag -a v{{ version }} -m ""
|
||||
@@ -37,6 +37,9 @@ I wrote this script really quickly, certainly faster than this README. So I alre
|
||||
* The script sends mail even if no new projets or release has been detected
|
||||
* The biggest, you have to edit script to specify absolute path...
|
||||
* A lot of other little problems, like the code that's disgusting and so on.
|
||||
|
||||
For who's asking: yes, it's normal that I have put all code in one main function [like a blind gunner.](https://media.giphy.com/media/1yMexL5rkwYhuiVEmZ/giphy.gif). Really quickly I said!
|
||||
|
||||
But overall, the script works and sends mail!
|
||||
|
||||
Hey boy, what is the `pit.db` file?
|
||||
@@ -44,5 +47,3 @@ Hey boy, what is the `pit.db` file?
|
||||
Oh, just for fun, and because I love this project, I use [pit by michaeldv](https://github.com/michaeldv/pit) to follow my task etc.
|
||||
|
||||
It makes me think I should push my python version of this project on occasion when I will take the time to do...
|
||||
|
||||
|
||||
|
||||
4
conf.ini
4
conf.ini
@@ -1,7 +1,7 @@
|
||||
[config]
|
||||
github_api_url = https://api.github.com/repos/
|
||||
smtp_port = 25
|
||||
smtp_server = localhost
|
||||
smtp_port = 1025
|
||||
smtp_server = mailhog
|
||||
sender_email = sender@host.eu
|
||||
receiver_email = receiver@anotherhost.eu
|
||||
|
||||
|
||||
24
docker-compose.yml
Normal file
24
docker-compose.yml
Normal file
@@ -0,0 +1,24 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
notifier:
|
||||
build: .
|
||||
image: github-release-notifier
|
||||
container_name: github-release-notifier
|
||||
volumes:
|
||||
- ./conf.ini:/app/conf.ini
|
||||
|
||||
mailhog:
|
||||
image: mailhog/mailhog:v1.0.1
|
||||
ports:
|
||||
- "8025:8025"
|
||||
- "1025:1025"
|
||||
|
||||
ofelia:
|
||||
image: mcuadros/ofelia:latest
|
||||
depends_on:
|
||||
- notifier
|
||||
command: daemon --config=/opt/config.ini
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- ./ofelia.ini:/opt/config.ini
|
||||
158
notifier.py
158
notifier.py
@@ -1,103 +1,129 @@
|
||||
from configparser import ConfigParser
|
||||
import json
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
|
||||
import configparser
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import smtplib
|
||||
import sys
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
import requests
|
||||
|
||||
SMTP_PORT = 0
|
||||
SMTP_SERVER = 'null'
|
||||
SENDER_EMAIL = 'a@b.c'
|
||||
RECEIVER_EMAIL = 'd@e.f'
|
||||
|
||||
class EnvInjection(configparser.Interpolation):
|
||||
"""
|
||||
Derived interpolation to take env variable before file variable.
|
||||
Permit to keep the ini file for local / traditionnal use
|
||||
And use env variable to overload configuration in a Docker usage.
|
||||
"""
|
||||
|
||||
def before_get(self, parser, section, option, value, defaults):
|
||||
file_value = super().before_get(parser, section, option, value, defaults)
|
||||
if section != parser.default_section:
|
||||
return file_value
|
||||
|
||||
env_value = os.getenv(option.upper())
|
||||
return env_value if env_value else file_value
|
||||
|
||||
|
||||
def main():
|
||||
global SMTP_PORT, SMTP_SERVER, SENDER_EMAIL, RECEIVER_EMAIL
|
||||
parser = ConfigParser()
|
||||
parser.read('conf.ini')
|
||||
script_dir = os.path.dirname(__file__)
|
||||
conf_file = os.path.join(script_dir, "conf.ini")
|
||||
template_file = os.path.join(script_dir, "template.html")
|
||||
|
||||
SMTP_PORT = parser.get('config', 'smtp_port')
|
||||
SMTP_SERVER = parser.get('config', 'smtp_server')
|
||||
SENDER_EMAIL = parser.get('config', 'sender_email')
|
||||
RECEIVER_EMAIL = parser.get('config', 'receiver_email')
|
||||
parser = configparser.ConfigParser(
|
||||
default_section="config", interpolation=EnvInjection()
|
||||
)
|
||||
parser.read(conf_file)
|
||||
default_config = parser["config"]
|
||||
|
||||
try:
|
||||
projects = json.loads(parser.get("projects", "projects"))
|
||||
except json.decoder.JSONDecodeError as jse:
|
||||
print("ERROR: config file is not correctly JSON formatted!", end="\n\t")
|
||||
print(jse)
|
||||
sys.exit(1)
|
||||
|
||||
projects = json.loads(parser.get('projects', 'projects'))
|
||||
new_releases = []
|
||||
new_projects = []
|
||||
|
||||
if not parser.has_section('release'):
|
||||
parser.add_section('release')
|
||||
if not parser.has_section("release"):
|
||||
parser.add_section("release")
|
||||
|
||||
for project in projects:
|
||||
last_release = get_last_release(project)
|
||||
|
||||
if not parser.has_option('release', project):
|
||||
if not parser.has_option("release", project):
|
||||
new_projects.append(last_release)
|
||||
else:
|
||||
last_config_tag = parser.get('release', project)
|
||||
if last_config_tag != last_release['release_tag']:
|
||||
last_release['preview_tag'] = last_config_tag
|
||||
last_config_tag = parser.get("release", project)
|
||||
if last_config_tag != last_release["release_tag"]:
|
||||
last_release["previous_tag"] = last_config_tag
|
||||
new_releases.append(last_release)
|
||||
parser.set('release', project, last_release['release_tag'])
|
||||
parser.set("release", project, last_release["release_tag"])
|
||||
|
||||
if not new_releases and not new_projects:
|
||||
print("No new projets or new release detected. Bye!")
|
||||
return
|
||||
|
||||
content = ""
|
||||
|
||||
for new_r in new_releases:
|
||||
content += """
|
||||
<li><a href="{}" target="_blank">{}</a>: new release <a href="{}" target="_blank">{}</a> available (old: {}).
|
||||
(published {})</li>
|
||||
""".format(
|
||||
new_r['release_url'],
|
||||
new_r['project_name'],
|
||||
new_r['release_url'],
|
||||
new_r['release_tag'],
|
||||
new_r['preview_tag'],
|
||||
convert_date(new_r['published_date']))
|
||||
content += f"""
|
||||
<li><a href="{new_r["release_url"]}" target="_blank">{new_r["project_name"]}</a>
|
||||
: new release
|
||||
<a href="{new_r["release_url"]}" target="_blank">{new_r["release_tag"]}</a>
|
||||
available (old: {new_r["previous_tag"]}).
|
||||
(published {convert_date(new_r["published_date"])})</li>"""
|
||||
|
||||
for new_p in new_projects:
|
||||
content += """
|
||||
<li><a href="{}" target="_blank">{}</a> was added to your configuration. Last release: <a href="{}" target="_blank">{}</a>
|
||||
(published {})</li>""".format(
|
||||
new_p['release_url'],
|
||||
new_p['project_name'],
|
||||
new_p['release_url'],
|
||||
new_p['release_tag'],
|
||||
convert_date(new_p['published_date']))
|
||||
content += f"""
|
||||
<li><a href="{new_p["release_url"]}" target="_blank">{new_p["project_name"]}</a>
|
||||
was added to your configuration.
|
||||
Last release:
|
||||
<a href="{new_p["release_url"]}" target="_blank">{new_p["release_tag"]}</a>
|
||||
(published {convert_date(new_p["published_date"])})</li>"""
|
||||
|
||||
# print(content)
|
||||
|
||||
with open('template.html', 'r') as f_template:
|
||||
with open(template_file, "r", encoding="utf-8") as f_template:
|
||||
template = f_template.read()
|
||||
|
||||
send_mail(template.replace('{{content}}', content))
|
||||
send_mail(template.replace("{{content}}", content), default_config)
|
||||
|
||||
with open('conf.ini', 'w') as configfile:
|
||||
with open("conf.ini", "w", encoding="utf-8") as configfile:
|
||||
parser.write(configfile)
|
||||
|
||||
|
||||
def get_last_release(project):
|
||||
url = 'https://api.github.com/repos/{}/releases/latest'.format(project)
|
||||
result = requests.get(url)
|
||||
url = f"https://api.github.com/repos/{project}/releases/latest"
|
||||
result = requests.get(url, timeout=10)
|
||||
|
||||
print(project)
|
||||
print(url)
|
||||
release = result.json()
|
||||
release_tag = release['tag_name']
|
||||
published_date = release['published_at']
|
||||
release_tag = release["tag_name"]
|
||||
published_date = release["published_at"]
|
||||
# body = release['body']
|
||||
release_url = release['html_url']
|
||||
release_url = release["html_url"]
|
||||
|
||||
return {'release_tag': release_tag,
|
||||
'published_date': published_date,
|
||||
# 'body': body,
|
||||
'project_name': project,
|
||||
'release_url': release_url}
|
||||
return {
|
||||
"release_tag": release_tag,
|
||||
"published_date": published_date,
|
||||
# 'body': body,
|
||||
"project_name": project,
|
||||
"release_url": release_url,
|
||||
}
|
||||
|
||||
|
||||
def send_mail(content, config):
|
||||
smtp_port = config.get("smtp_port")
|
||||
smtp_server = config.get("smtp_server")
|
||||
sender_email = config.get("sender_email")
|
||||
receiver_email = config.get("receiver_email")
|
||||
|
||||
def send_mail(content):
|
||||
message = MIMEMultipart("alternative")
|
||||
message["Subject"] = "New Github releases"
|
||||
message["From"] = SENDER_EMAIL
|
||||
message["To"] = RECEIVER_EMAIL
|
||||
message["From"] = sender_email
|
||||
message["To"] = receiver_email
|
||||
|
||||
# part1 = MIMEText(text, "plain")
|
||||
part2 = MIMEText(content, "html")
|
||||
@@ -105,11 +131,13 @@ def send_mail(content):
|
||||
# message.attach(part1)
|
||||
message.attach(part2)
|
||||
|
||||
with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server:
|
||||
server.sendmail(SENDER_EMAIL, RECEIVER_EMAIL, message.as_string())
|
||||
with smtplib.SMTP(smtp_server, smtp_port) as server:
|
||||
server.sendmail(sender_email, receiver_email, message.as_string())
|
||||
|
||||
|
||||
def convert_date(date: str, dest_format="%d %b %Y at %H:%M") -> str:
|
||||
return datetime.datetime.strptime(date, "%Y-%m-%dT%H:%M:%SZ").strftime(dest_format)
|
||||
|
||||
def convert_date(date: str, format='%d %b %Y at %H:%M') -> str:
|
||||
return datetime.datetime.strptime(date, "%Y-%m-%dT%H:%M:%SZ").strftime(format)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
9
ofelia.ini
Normal file
9
ofelia.ini
Normal file
@@ -0,0 +1,9 @@
|
||||
[global]
|
||||
smtp-host = mailhog
|
||||
smtp-port = 1025
|
||||
email-to = max@ence.fr
|
||||
email-from = ofelia@container.sh
|
||||
|
||||
[job-run "notifier"]
|
||||
schedule = @every 10s
|
||||
container = github-release-notifier
|
||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
requests
|
||||
pre-commit
|
||||
black
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user