forked from Mirroring/github-release-notifier
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 08d27d3183 | |||
| 9d5e50c635 | |||
| eea62d8685 | |||
| 9a8a7042b3 | |||
| fb4ac8ea73 | |||
| f4edb03979 | |||
| 68fd6e78d9 | |||
| e57d9cf656 |
@@ -1,10 +1,12 @@
|
||||
FROM python:3.10-alpine3.18
|
||||
|
||||
RUN pip install requests
|
||||
WORKDIR /app
|
||||
COPY requirements.txt /app/requirements.txt
|
||||
RUN pip install --no-cache-dir --upgrade --requirement requirements.txt
|
||||
COPY notifier.py template.html /app/
|
||||
|
||||
# TODO Dev purporse
|
||||
COPY conf.ini /app/conf.ini
|
||||
|
||||
ENTRYPOINT ["python3", "/app/notifier.py"]
|
||||
EXPOSE 80
|
||||
CMD ["uvicorn", "notifier:app", "--host", "0.0.0.0", "--port", "80"]
|
||||
|
||||
24
Justfile
24
Justfile
@@ -1,21 +1,34 @@
|
||||
# https://github.com/casey/just
|
||||
|
||||
vbin := "./venv/bin"
|
||||
pip := vbin / "pip"
|
||||
python := vbin / "python"
|
||||
venv := "./venv"
|
||||
pip := venv / "bin/pip"
|
||||
python := venv / "bin/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
|
||||
run: _ensure_venv_is_ok
|
||||
{{ python }} notifier.py
|
||||
|
||||
# Launch the API
|
||||
api: _ensure_venv_is_ok
|
||||
{{ venv }}/bin/uvicorn notifier:app --reload
|
||||
|
||||
# Init python virtual env
|
||||
init:
|
||||
python3 -m venv venv
|
||||
{{ pip }} install --requirement requirements.txt
|
||||
sha256sum requirements.txt > {{ venv }}/requirements.sha
|
||||
|
||||
# Inspiration: https://github.com/behave/behave/blob/afb6b6716cd0f3e028829416475312db804a6aa9/justfile
|
||||
_ensure_venv_is_ok:
|
||||
#!/usr/bin/env python3
|
||||
from subprocess import run
|
||||
from os import path
|
||||
if run("sha256sum -c {{ venv }}/requirements.sha", shell=True).returncode != 0:
|
||||
run("just init", shell=True)
|
||||
|
||||
# Clean workspace - remove venv - and init
|
||||
reinit: hclean init
|
||||
@@ -50,8 +63,9 @@ dpush: dbuild
|
||||
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
|
||||
# Use just a number! Without 'v'! Release a version - create a tagged images, push it and create 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 ""
|
||||
git push --tags
|
||||
|
||||
54
README.md
54
README.md
@@ -12,33 +12,71 @@ Why using configuration?...
|
||||
------------------------
|
||||
...instead use stared project of your Github account?
|
||||
Cause I've got about 80 "stared" projects and I don't wan't to be alerted for new releases of each of these project.
|
||||
But, perhaps I'll add such a feature later on...
|
||||
|
||||
And, Github API limits is at 60 requests by seconds, and I want to write this script really quickly in a first time.
|
||||
|
||||
But, perhaps I'll add such a feature later on...
|
||||
|
||||
How to use?
|
||||
-----------
|
||||
Really simple!
|
||||
|
||||
* Edit conf.ini file to set `[config]` section
|
||||
* Install dependances: `pip install -r requirements.txt`
|
||||
* Edit conf.ini file to set `[config]` section:
|
||||
* your SMTP server configuration (host and port)
|
||||
* sender mail
|
||||
* receiver mail (:warning: not tested with more than 1 receiver)
|
||||
* Add the projects you want to follow in the section `[project]`
|
||||
* ...or use environment variable - same name but in upper case e.g. `SMTP_PORT`
|
||||
* Add projects you want to follow in the section `[project]`
|
||||
* Be careful to follow a JSON valid syntax as in the provided file, i.e. coma after each `autor/project` except the last one.
|
||||
|
||||
Execute the script: `python notifier.py`
|
||||
|
||||
After first execution, the `conf.ini` file will be filled with last release tag found by the script, as you can see in the provided file.
|
||||
|
||||
Hope you like, and have fun to read your mail!
|
||||
|
||||
Hey, what is this API?
|
||||
--------------------------
|
||||
Since it's not really conveniant to go on a VM and edit the config.ini every time I find a cool new project whose new versions I want to keep track of, there is an API to do this.
|
||||
|
||||
This is a really simple thing with no verification: you can add everything that you want!
|
||||
|
||||
Just one endpoint: `subscription`, and two method: GET and PUT.
|
||||
PUT take three parameters:
|
||||
* `project`: full project name e.g. `MaxenceG2M/github-release-notifier` or "short" name (without author) e.g. `github-release-notifier`
|
||||
* (optionnal) `author`: the author of the repository
|
||||
* (optionnal) `credentials`: just the TOTP check code.
|
||||
|
||||
Indeed, to have a minimal better-than-nothing protection, I opted for a simple TOTP code. No user/password or wathever.
|
||||
You can disable-it in the `config.ini`, or specify a custom key.
|
||||
If you don't specify a key, information will be displayed at startup (URL and the actual code. Be quick to check if it's ok!).
|
||||
|
||||
To start the API:
|
||||
```sh
|
||||
$ uvicorn notifier:app
|
||||
TOTP enabled.
|
||||
Information: otpauth://totp/Secret?secret=mysuperkey
|
||||
TOTP check: 377826
|
||||
INFO: Started server process [28264]
|
||||
INFO: Waiting for application startup.
|
||||
INFO: Application startup complete.
|
||||
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
||||
```
|
||||
|
||||
Cron-it with [ofelia](https://github.com/mcuadros/ofelia)
|
||||
---------------------------------------------------------
|
||||
Ofelia is very conveniant for croning the verification part while letting the API run continuously.
|
||||
So the `docker-compose.yml` use it to do this :)
|
||||
|
||||
<3
|
||||
|
||||
Problems I have to solve really quickly
|
||||
---------------------------------------
|
||||
I wrote this script really quickly, certainly faster than this README. So I already have two big proglems:
|
||||
* 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.
|
||||
One script to do all things.
|
||||
|
||||
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!
|
||||
For who's asking: yes, it's normal that I have put all code in ~~one main function~~ one main script [like a blind gunner.](https://media.giphy.com/media/1yMexL5rkwYhuiVEmZ/giphy.gif).
|
||||
I want to keep this script as simple as possible.
|
||||
|
||||
But overall, the script works and sends mail!
|
||||
|
||||
|
||||
6
conf.ini
6
conf.ini
@@ -1,10 +1,14 @@
|
||||
[config]
|
||||
github_api_url = https://api.github.com/repos/
|
||||
smtp_port = 1025
|
||||
smtp_server = mailhog
|
||||
smtp_server = mailpit
|
||||
sender_email = sender@host.eu
|
||||
receiver_email = receiver@anotherhost.eu
|
||||
|
||||
[api.totp]
|
||||
enabled = true
|
||||
key = mysuperkey
|
||||
|
||||
[projects]
|
||||
projects = [
|
||||
"borgbackup/borg",
|
||||
|
||||
@@ -6,13 +6,15 @@ services:
|
||||
image: github-release-notifier
|
||||
container_name: github-release-notifier
|
||||
volumes:
|
||||
- ./conf.ini:/app/conf.ini
|
||||
|
||||
mailhog:
|
||||
image: mailhog/mailhog:v1.0.1
|
||||
- ./conf.ini:/app/conf.ini
|
||||
ports:
|
||||
- "8025:8025"
|
||||
- "1025:1025"
|
||||
- 8000:80
|
||||
|
||||
mailpit:
|
||||
image: axllent/mailpit
|
||||
ports:
|
||||
- 8025:8025
|
||||
- 1025:1025
|
||||
|
||||
ofelia:
|
||||
image: mcuadros/ofelia:latest
|
||||
|
||||
110
notifier.py
110
notifier.py
@@ -7,7 +7,17 @@ import sys
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
import pyotp
|
||||
import requests
|
||||
from fastapi import FastAPI, status
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
#
|
||||
# CONF PART
|
||||
#
|
||||
SCRIPT_DIR = os.path.dirname(__file__)
|
||||
CONF_FILE = os.path.join(SCRIPT_DIR, "conf.ini")
|
||||
TEMPLATE_FILE = os.path.join(SCRIPT_DIR, "template.html")
|
||||
|
||||
|
||||
class EnvInjection(configparser.Interpolation):
|
||||
@@ -26,15 +36,79 @@ class EnvInjection(configparser.Interpolation):
|
||||
return env_value if env_value else file_value
|
||||
|
||||
|
||||
def main():
|
||||
script_dir = os.path.dirname(__file__)
|
||||
conf_file = os.path.join(script_dir, "conf.ini")
|
||||
template_file = os.path.join(script_dir, "template.html")
|
||||
|
||||
def load_conf(conf_file=CONF_FILE) -> configparser.ConfigParser:
|
||||
parser = configparser.ConfigParser(
|
||||
default_section="config", interpolation=EnvInjection()
|
||||
)
|
||||
parser.read(conf_file)
|
||||
return parser
|
||||
|
||||
|
||||
def load_totp():
|
||||
parser = load_conf()
|
||||
|
||||
if parser.getboolean("api.totp", "enabled", fallback=True):
|
||||
totp = pyotp.TOTP(parser.get("api.totp", "key", fallback=pyotp.random_base32()))
|
||||
# TODO Catch totp.now() error
|
||||
print(
|
||||
f"""TOTP enabled.
|
||||
Information: {totp.provisioning_uri()}
|
||||
TOTP check: {totp.now()}"""
|
||||
)
|
||||
return totp
|
||||
else:
|
||||
print("WARNING: Api is open without security")
|
||||
return type("totp", (object,), {"verify": lambda str: True})
|
||||
|
||||
|
||||
#
|
||||
# API PARTS
|
||||
#
|
||||
app = FastAPI()
|
||||
TOTP = load_totp()
|
||||
|
||||
|
||||
@app.get("/subscriptions")
|
||||
def get_projects():
|
||||
parser = load_conf()
|
||||
projects = json.loads(parser["projects"].get("projects"))
|
||||
return projects
|
||||
|
||||
|
||||
@app.put("/subscriptions")
|
||||
def put_projects(
|
||||
project: str, author: str | None = None, credentials: str | None = None
|
||||
):
|
||||
if not TOTP.verify(credentials):
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED, content="Unauthorized"
|
||||
)
|
||||
# TODO Check if project really exist?
|
||||
parser = load_conf()
|
||||
projects = json.loads(parser.get("projects", "projects"))
|
||||
|
||||
if author:
|
||||
project = f"{author}/{project}"
|
||||
|
||||
if project in projects:
|
||||
return project
|
||||
|
||||
projects.append(project)
|
||||
|
||||
# TODO Watch a converter for list: https://stackoverflow.com/a/53274707/1346391
|
||||
parser.set("projects", "projects", json.dumps(projects, indent=0))
|
||||
|
||||
with open("conf.ini", "w", encoding="utf-8") as configfile:
|
||||
parser.write(configfile)
|
||||
|
||||
return JSONResponse(status_code=status.HTTP_201_CREATED, content=project)
|
||||
|
||||
|
||||
#
|
||||
# SCRIPT PART
|
||||
#
|
||||
def main():
|
||||
parser = load_conf()
|
||||
default_config = parser["config"]
|
||||
|
||||
try:
|
||||
@@ -67,27 +141,31 @@ def main():
|
||||
return
|
||||
|
||||
content = ""
|
||||
news = []
|
||||
|
||||
for new_r in new_releases:
|
||||
news.append(new_r["project_name"])
|
||||
content += f"""
|
||||
<li><a href="{new_r["release_url"]}" target="_blank">{new_r["project_name"]}</a>
|
||||
<li>{get_html_project_link(new_r)}
|
||||
: new release
|
||||
<a href="{new_r["release_url"]}" target="_blank">{new_r["release_tag"]}</a>
|
||||
{get_html_release_link(new_r)}
|
||||
available (old: {new_r["previous_tag"]}).
|
||||
(published {convert_date(new_r["published_date"])})</li>"""
|
||||
|
||||
for new_p in new_projects:
|
||||
news.append(new_p["project_name"])
|
||||
content += f"""
|
||||
<li><a href="{new_p["release_url"]}" target="_blank">{new_p["project_name"]}</a>
|
||||
<li>{get_html_project_link(new_p)}
|
||||
was added to your configuration.
|
||||
Last release:
|
||||
<a href="{new_p["release_url"]}" target="_blank">{new_p["release_tag"]}</a>
|
||||
{get_html_release_link(new_p)}
|
||||
(published {convert_date(new_p["published_date"])})</li>"""
|
||||
|
||||
with open(template_file, "r", encoding="utf-8") as f_template:
|
||||
with open(TEMPLATE_FILE, "r", encoding="utf-8") as f_template:
|
||||
template = f_template.read()
|
||||
|
||||
send_mail(template.replace("{{content}}", content), default_config)
|
||||
print(f"Send a mail for new releases and projects on: {', '.join(news)}")
|
||||
|
||||
with open("conf.ini", "w", encoding="utf-8") as configfile:
|
||||
parser.write(configfile)
|
||||
@@ -97,8 +175,7 @@ def get_last_release(project):
|
||||
url = f"https://api.github.com/repos/{project}/releases/latest"
|
||||
result = requests.get(url, timeout=10)
|
||||
|
||||
print(project)
|
||||
print(url)
|
||||
print(f"Check {project} - {url}")
|
||||
release = result.json()
|
||||
release_tag = release["tag_name"]
|
||||
published_date = release["published_at"]
|
||||
@@ -114,6 +191,15 @@ def get_last_release(project):
|
||||
}
|
||||
|
||||
|
||||
def get_html_project_link(el):
|
||||
project_url = f'https://github.com/{el["project_name"]}'
|
||||
return f'<a href="{project_url}" target="_blank">{el["project_name"]}</a>'
|
||||
|
||||
|
||||
def get_html_release_link(el):
|
||||
return f'<a href="{el["release_url"]}" target="_blank">{el["release_tag"]}</a>'
|
||||
|
||||
|
||||
def send_mail(content, config):
|
||||
smtp_port = config.get("smtp_port")
|
||||
smtp_server = config.get("smtp_server")
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
[global]
|
||||
smtp-host = mailhog
|
||||
smtp-host = mailpit
|
||||
smtp-port = 1025
|
||||
email-to = max@ence.fr
|
||||
email-from = ofelia@container.sh
|
||||
|
||||
[job-run "notifier"]
|
||||
schedule = @every 10s
|
||||
[job-exec "notifier"]
|
||||
schedule = @every 30s
|
||||
container = github-release-notifier
|
||||
command = python3 /app/notifier.py
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
requests
|
||||
pre-commit
|
||||
black
|
||||
fastapi
|
||||
pre-commit
|
||||
pyotp
|
||||
requests
|
||||
uvicorn[standard]
|
||||
|
||||
@@ -5,62 +5,13 @@
|
||||
<title></title>
|
||||
|
||||
<style>
|
||||
button,hr,input{overflow:visible}audio,canvas,progress,video{display:inline-block}[type=checkbox],[type=radio],legend{box-sizing:border-box;padding:0}html{font-family:sans-serif;line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,main,menu,nav,section{display:block}h1{font-size:2em;margin:.67em 0}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}a:active,a:hover{outline-width:0}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}dfn{font-style:italic}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}a
|
||||
udio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:ButtonText dotted 1px}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{color:inherit;display:table;max-width:100%;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=se
|
||||
arch]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}[hidden],template{display:none}
|
||||
body {
|
||||
margin: auto;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale; }
|
||||
|
||||
.wysiwyg-content main > :first-child {
|
||||
padding-top: 0 !important;
|
||||
margin-top: 0 !important; }
|
||||
.wysiwyg-content main > :last-child {
|
||||
padding-bottom: 0 !important;
|
||||
margin-bottom: 0 !important; }
|
||||
|
||||
.wysiwyg-content img {
|
||||
max-width: 100%; }
|
||||
|
||||
.wysiwyg-content iframe {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
border: 0; }
|
||||
|
||||
.wysiwyg-content .emoji .emoji-text {
|
||||
font-size: 0; }
|
||||
|
||||
|
||||
/* https://github.com/sindresorhus/github-markdown-css/blob/gh-pages/github-markdown.css */
|
||||
/* From GitHub markdown view style */
|
||||
body {
|
||||
max-width: 980px; }
|
||||
main {
|
||||
padding: 45px; }
|
||||
|
||||
|
||||
/* https://github.com/sindresorhus/github-markdown-css/blob/gh-pages/github-markdown.css */
|
||||
|
||||
.markdown-body hr::after,.markdown-body:after{clear:both}.markdown-body hr::after,.markdown-body hr::before,.markdown-body:after,.markdown-body:before{display:table;content:""}@font-face{font-family:octicons-link;src:url(data:font/woff;charset=utf-8;base64,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) format('woff')}.markdown-body{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;color:#333;font-family:"Helvetica Neue",Helvetica,"Segoe UI",Arial,freesans,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size
|
||||
:16px;line-height:1.6;word-wrap:break-word}.markdown-body a{background-color:transparent;-webkit-text-decoration-skip:objects;color:#4078c0;text-decoration:none}.markdown-body a:active,.markdown-body a:hover{outline-width:0;text-decoration:underline}.markdown-body strong{font-weight:bolder}.markdown-body h1{margin:.67em 0}.markdown-body img{border-style:none}.markdown-body svg:not(:root){overflow:hidden}.markdown-body hr{box-sizing:content-box}.markdown-body input{margin:0;overflow:visible;font:13px/1.4 Helvetica,arial,nimbussansl,liberationsans,freesans,clean,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"}.markdown-body [type=button]:-moz-focusring,.markdown-body [type=reset]:-moz-focusring,.markdown-body [type=submit]:-moz-focusring,.markdown-body button:-moz-focusring{outline:ButtonText dotted 1px}.markdown-body [type=checkbox]{box-sizing:border-box;padding:0}.mar
|
||||
kdown-body td,.markdown-body th{padding:0}.markdown-body h1,.markdown-body h2{padding-bottom:.3em;border-bottom:1px solid #eee}.markdown-body *{box-sizing:border-box}.markdown-body blockquote{margin:0}.markdown-body ol ol,.markdown-body ul ol{list-style-type:lower-roman}.markdown-body ol ol ol,.markdown-body ol ul ol,.markdown-body ul ol ol,.markdown-body ul ul ol{list-style-type:lower-alpha}.markdown-body dd{margin-left:0}.markdown-body code{font-family:Consolas,"Liberation Mono",Menlo,Courier,monospace}.markdown-body pre{font:12px Consolas,"Liberation Mono",Menlo,Courier,monospace;word-wrap:normal}.markdown-body .pl-0{padding-left:0!important}.markdown-body .pl-1{padding-left:3px!important}.markdown-body .pl-2{padding-left:6px!important}.markdown-body .pl-3{padding-left:12px!important}.markdown-body .pl-4{padding-left:24px!important}.markdown-body .pl-5{padding-left:36px!important}.m
|
||||
arkdown-body .pl-6{padding-left:48px!important}.markdown-body .form-select::-ms-expand{opacity:0}.markdown-body>:first-child{margin-top:0!important}.markdown-body>:last-child{margin-bottom:0!important}.markdown-body a:not([href]){color:inherit;text-decoration:none}.markdown-body .anchor{display:inline-block;padding-right:2px;margin-left:-18px}.markdown-body .anchor:focus{outline:0}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{margin-top:1em;margin-bottom:16px;font-weight:700;line-height:1.4}.markdown-body h1 .octicon-link,.markdown-body h2 .octicon-link,.markdown-body h3 .octicon-link,.markdown-body h4 .octicon-link,.markdown-body h5 .octicon-link,.markdown-body h6 .octicon-link{color:#000;vertical-align:middle;visibility:hidden}.markdown-body h1:hover .anchor,.markdown-body h2:hover .anchor,.markdown-body h3:hover .anchor,.
|
||||
markdown-body h4:hover .anchor,.markdown-body h5:hover .anchor,.markdown-body h6:hover .anchor{text-decoration:none}.markdown-body h1:hover .anchor .octicon-link,.markdown-body h2:hover .anchor .octicon-link,.markdown-body h3:hover .anchor .octicon-link,.markdown-body h4:hover .anchor .octicon-link,.markdown-body h5:hover .anchor .octicon-link,.markdown-body h6:hover .anchor .octicon-link{visibility:visible}.markdown-body h1{font-size:2.25em;line-height:1.2}.markdown-body h1 .anchor{line-height:1}.markdown-body h2{font-size:1.75em;line-height:1.225}.markdown-body h2 .anchor{line-height:1}.markdown-body h3{font-size:1.5em;line-height:1.43}.markdown-body h3 .anchor,.markdown-body h4 .anchor{line-height:1.2}.markdown-body h4{font-size:1.25em}.markdown-body h5 .anchor,.markdown-body h6 .anchor{line-height:1.1}.markdown-body h5{font-size:1em}.markdown-body h6{font-size:1em;color:#777}.markd
|
||||
own-body blockquote,.markdown-body dl,.markdown-body ol,.markdown-body p,.markdown-body pre,.markdown-body table,.markdown-body ul{margin-top:0;margin-bottom:16px}.markdown-body hr{overflow:hidden;background:#e7e7e7;height:4px;padding:0;margin:16px 0;border:0}.markdown-body ol,.markdown-body ul{padding-left:2em}.markdown-body ol ol,.markdown-body ol ul,.markdown-body ul ol,.markdown-body ul ul{margin-top:0;margin-bottom:0}.markdown-body li>p{margin-top:16px}.markdown-body dl{padding:0}.markdown-body dl dt{padding:0;margin-top:16px;font-size:1em;font-style:italic;font-weight:700}.markdown-body dl dd{padding:0 16px;margin-bottom:16px}.markdown-body blockquote{padding:0 15px;color:#777;border-left:4px solid #ddd}.markdown-body blockquote>:first-child{margin-top:0}.markdown-body blockquote>:last-child{margin-bottom:0}.markdown-body table{border-spacing:0;border-collapse:collapse;display:bl
|
||||
ock;width:100%;overflow:auto;word-break:normal;word-break:keep-all}.markdown-body table th{font-weight:700}.markdown-body table td,.markdown-body table th{padding:6px 13px;border:1px solid #ddd}.markdown-body table tr{background-color:#fff;border-top:1px solid #ccc}.markdown-body table tr:nth-child(2n){background-color:#f8f8f8}.markdown-body img{max-width:100%;box-sizing:content-box;background-color:#fff}.markdown-body code{padding:.2em 0;margin:0;font-size:85%;background-color:rgba(0,0,0,.04);border-radius:3px}.markdown-body code:after,.markdown-body code:before{letter-spacing:-.2em;content:"\00a0"}.markdown-body pre>code{padding:0;margin:0;font-size:100%;word-break:normal;white-space:pre;background:0 0;border:0}.markdown-body .highlight{margin-bottom:16px}.markdown-body .highlight pre,.markdown-body pre{padding:16px;overflow:auto;font-size:85%;line-height:1.45;background-color:#f7f7f
|
||||
7;border-radius:3px}.markdown-body .highlight pre{margin-bottom:0;word-break:normal}.markdown-body pre code{display:inline;max-width:initial;padding:0;margin:0;overflow:initial;line-height:inherit;word-wrap:normal;background-color:transparent;border:0}.markdown-body pre code:after,.markdown-body pre code:before{content:normal}.markdown-body .pl-c{color:#969896}.markdown-body .pl-c1,.markdown-body .pl-s .pl-v{color:#0086b3}.markdown-body .pl-e,.markdown-body .pl-en{color:#795da3}.markdown-body .pl-s .pl-s1,.markdown-body .pl-smi{color:#333}.markdown-body .pl-ent{color:#63a35c}.markdown-body .pl-k{color:#a71d5d}.markdown-body .pl-pds,.markdown-body .pl-s,.markdown-body .pl-s .pl-pse .pl-s1,.markdown-body .pl-sr,.markdown-body .pl-sr .pl-cce,.markdown-body .pl-sr .pl-sra,.markdown-body .pl-sr .pl-sre{color:#183691}.markdown-body .pl-v{color:#ed6a43}.markdown-body.pl-id{color:#b52a1d}.mark
|
||||
down-body .pl-ii{background-color:#b52a1d;color:#f8f8f8}.markdown-body .pl-sr .pl-cce{color:#63a35c;font-weight:700}.markdown-body .pl-ml{color:#693a17}.markdown-body .pl-mh,.markdown-body .pl-mh .pl-en,.markdown-body .pl-ms{color:#1d3e81;font-weight:700}.markdown-body .pl-mq{color:teal}.markdown-body .pl-mi{color:#333;font-style:italic}.markdown-body .pl-mb{color:#333;font-weight:700}.markdown-body .pl-md{background-color:#ffecec;color:#bd2c00}.markdown-body .pl-mi1{background-color:#eaffea;color:#55a532}.markdown-body .pl-mdr{color:#795da3;font-weight:700}.markdown-body .pl-mo{color:#1d3e81}.markdown-body kbd{display:inline-block;padding:3px 5px;font:11px Consolas,"Liberation Mono",Menlo,Courier,monospace;line-height:10px;color:#555;vertical-align:middle;background-color:#fcfcfc;border:1px solid #ccc;border-bottom-color:#bbb;border-radius:3px;box-shadow:inset 0 -1px 0 #bbb}
|
||||
.markdown-body .full-commit .btn-outline:not(:disabled):hover{color:#4078c0;border:1px solid #4078c0}.markdown-body :checked+.radio-label{position:relative;z-index:1;border-color:#4078c0}.markdown-body .octicon{display:inline-block;vertical-align:text-top;fill:currentColor}.markdown-body .task-list-item{list-style-type:none}.markdown-body .task-list-item+.task-list-item{margin-top:3px}.markdown-body .task-list-item input{margin:0 .2em .25em -1.6em;vertical-align:middle}.markdown-body hr{border-bottom-color:#eee}
|
||||
.markdown-body a,.markdown-body h1:hover .anchor,.markdown-body h2:hover .anchor,.markdown-body h3:hover .anchor,.markdown-body h4:hover .anchor,.markdown-body h5:hover .anchor,.markdown-body h6:hover .anchor{text-decoration:none}body{margin:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;max-width:980px;padding:45px}.markdown-body{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;color:#333;font-family:"Helvetica Neue",Helvetica,"Segoe UI",Arial,freesans,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:16px;line-height:1.6;word-wrap:break-word}.markdown-body a{background-color:transparent;-webkit-text-decoration-skip:objects;color:#4078c0}.markdown-body a:hover{outline-width:0;text-decoration:underline}.markdown-body strong{font-weight:bolder}.markdown-body h1{margin:.67em 0}.markdown-body h1,.markdown-body h2{padding-bottom:.3em;border-bottom:1px solid #eee}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{margin-top:1em;margin-bottom:16px;font-weight:700;line-height:1.4}.markdown-body h1 .octicon-link,.markdown-body h2 .octicon-link,.markdown-body h3 .octicon-link,.markdown-body h4 .octicon-link,.markdown-body h5 .octicon-link,.markdown-body h6 .octicon-link{color:#000;vertical-align:middle;visibility:hidden}.markdown-body h1:hover .anchor .octicon-link,.markdown-body h2:hover .anchor .octicon-link,.markdown-body h3:hover .anchor .octicon-link,.markdown-body h4:hover .anchor .octicon-link,.markdown-body h5:hover .anchor .octicon-link,.markdown-body h6:hover .anchor .octicon-link{visibility:visible}.markdown-body h1{font-size:2.25em;line-height:1.2}.markdown-body h1 .anchor,.markdown-body h2 .anchor{line-height:1}.markdown-body h2{font-size:1.75em;line-height:1.225}.markdown-body h3{font-size:1.5em;line-height:1.43}.markdown-body h3 .anchor,.markdown-body h4 .anchor{line-height:1.2}.markdown-body .octicon{display:inline-block;vertical-align:text-top;fill:currentColor}.markdown-body hr{border-bottom-color:#eee}
|
||||
</style>
|
||||
</head>
|
||||
<body class="markdown-body wysiwyg-content">
|
||||
<main><h1 id="-some-new-release-on-github-project-available-">
|
||||
<img src="https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png" height="32">
|
||||
Some new release on Github project available!</h1>
|
||||
<p><ul>{{content}}</ul></p>
|
||||
</main>
|
||||
</body>
|
||||
<div class="markdown-body wysiwyg-content">
|
||||
<h1 id="-some-new-release-on-github-project-available-">
|
||||
<img src="https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png" height="32">
|
||||
Some new release on Github project available!</h1>
|
||||
<p><ul>{{content}}</ul></p>
|
||||
</div>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user