|
import dash |
|
from dash import dcc, html, Input, Output, State, callback |
|
import dash_bootstrap_components as dbc |
|
from datetime import datetime, timedelta |
|
import google.generativeai as genai |
|
from github import Github, GithubException |
|
import gitlab |
|
import docx |
|
import tempfile |
|
import requests |
|
import os |
|
import threading |
|
import io |
|
|
|
|
|
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) |
|
|
|
|
|
HF_GEMINI_API_KEY = os.environ.get('HF_GEMINI_API_KEY') |
|
HF_GITHUB_TOKEN = os.environ.get('HF_GITHUB_TOKEN') |
|
|
|
|
|
generated_file = None |
|
pr_url = None |
|
|
|
def generate_release_notes(git_provider, repo_url, start_date, end_date, folder_location): |
|
global generated_file |
|
try: |
|
start_date = datetime.strptime(start_date, "%Y-%m-%d") |
|
end_date = datetime.strptime(end_date, "%Y-%m-%d") |
|
|
|
if git_provider == "GitHub": |
|
g = Github(HF_GITHUB_TOKEN) |
|
repo = g.get_repo(repo_url) |
|
commits = list(repo.get_commits(since=start_date, until=end_date)) |
|
commit_messages = [commit.commit.message for commit in commits] |
|
elif git_provider == "GitLab": |
|
gl = gitlab.Gitlab(url='https://gitlab.com', private_token=HF_GITHUB_TOKEN) |
|
project = gl.projects.get(repo_url) |
|
commits = project.commits.list(since=start_date.isoformat(), until=end_date.isoformat()) |
|
commit_messages = [commit.message for commit in commits] |
|
elif git_provider == "Gitea": |
|
base_url = "https://gitea.com/api/v1" |
|
headers = {"Authorization": f"token {HF_GITHUB_TOKEN}"} |
|
response = requests.get(f"{base_url}/repos/{repo_url}/commits", headers=headers, params={ |
|
"since": start_date.isoformat(), |
|
"until": end_date.isoformat() |
|
}) |
|
response.raise_for_status() |
|
commits = response.json() |
|
commit_messages = [commit['commit']['message'] for commit in commits] |
|
else: |
|
return "Unsupported Git provider", None |
|
|
|
commit_text = "\n".join(commit_messages) |
|
|
|
if not commit_text: |
|
return "No commits found in the specified date range.", None |
|
|
|
genai.configure(api_key=HF_GEMINI_API_KEY) |
|
model = genai.GenerativeModel('gemini-2.0-flash-lite') |
|
|
|
prompt = f"""Based on the following commit messages, generate comprehensive release notes: |
|
{commit_text} |
|
Please organize the release notes into sections such as: |
|
1. New Features |
|
2. Bug Fixes |
|
3. Improvements |
|
4. Breaking Changes (if any) |
|
Provide a concise summary for each item. Do not include any links, but keep issue numbers if present. |
|
|
|
Important formatting instructions: |
|
- The output should be plain text without any markdown or "-" for post processing |
|
- Use section titles followed by a colon (e.g., "New Features:") |
|
- Start each item on a new line |
|
- Be sure to briefly explain the why and benefits of the change for average users that are non-technical |
|
""" |
|
|
|
response = model.generate_content(prompt) |
|
release_notes = response.text |
|
|
|
|
|
markdown_content = "# Release Notes\n\n" |
|
for line in release_notes.split('\n'): |
|
line = line.strip() |
|
if line.endswith(':'): |
|
markdown_content += f"\n## {line}\n\n" |
|
elif line: |
|
markdown_content += f"- {line}\n" |
|
|
|
|
|
file_name = f"{datetime.now().strftime('%m-%d-%Y')}.md" |
|
|
|
|
|
generated_file = io.BytesIO(markdown_content.encode()) |
|
generated_file.seek(0) |
|
|
|
return release_notes, file_name |
|
|
|
except Exception as e: |
|
return f"An error occurred: {str(e)}", None |
|
|
|
def update_summary_and_create_pr(repo_url, folder_location, start_date, end_date, markdown_content): |
|
global pr_url |
|
try: |
|
g = Github(HF_GITHUB_TOKEN) |
|
repo = g.get_repo(repo_url) |
|
|
|
|
|
file_name = f"{end_date}.md" |
|
|
|
|
|
summary_folder = '/'.join(folder_location.split('/')[:-1]) |
|
summary_path = f"{summary_folder}/SUMMARY.md" |
|
|
|
|
|
try: |
|
summary_file = repo.get_contents(summary_path) |
|
summary_content = summary_file.decoded_content.decode() |
|
except GithubException as e: |
|
if e.status == 404: |
|
summary_content = "* [Releases](README.md)\n" |
|
else: |
|
raise |
|
|
|
|
|
new_entry = f" * [{end_date}](rel/{file_name})\n" |
|
releases_index = summary_content.find("* [Releases]") |
|
if releases_index != -1: |
|
insert_position = summary_content.find("\n", releases_index) + 1 |
|
updated_summary = (summary_content[:insert_position] + new_entry + |
|
summary_content[insert_position:]) |
|
else: |
|
updated_summary = summary_content + f"* [Releases](README.md)\n{new_entry}" |
|
|
|
|
|
base_branch = repo.default_branch |
|
new_branch = f"update-release-notes-{datetime.now().strftime('%Y%m%d%H%M%S')}" |
|
ref = repo.get_git_ref(f"heads/{base_branch}") |
|
repo.create_git_ref(ref=f"refs/heads/{new_branch}", sha=ref.object.sha) |
|
|
|
|
|
repo.update_file( |
|
summary_path, |
|
f"Update SUMMARY.md with new release notes {file_name}", |
|
updated_summary, |
|
summary_file.sha if 'summary_file' in locals() else None, |
|
branch=new_branch |
|
) |
|
|
|
|
|
new_file_path = f"{folder_location}/{file_name}" |
|
repo.create_file( |
|
new_file_path, |
|
f"Add release notes {file_name}", |
|
markdown_content, |
|
branch=new_branch |
|
) |
|
|
|
|
|
pr = repo.create_pull( |
|
title=f"Add release notes {file_name} and update SUMMARY.md", |
|
body="Automatically generated PR to add new release notes and update SUMMARY.md.", |
|
head=new_branch, |
|
base=base_branch |
|
) |
|
|
|
pr_url = pr.html_url |
|
return f"Pull request created: {pr_url}" |
|
except Exception as e: |
|
print(f"Error: {str(e)}") |
|
return f"Error creating PR: {str(e)}" |
|
|
|
|
|
app.layout = dbc.Container([ |
|
html.H1("Automated Release Notes Generator", className="mb-4"), |
|
dbc.Card([ |
|
dbc.CardBody([ |
|
dbc.Form([ |
|
dbc.Row([ |
|
dbc.Col([ |
|
dcc.Dropdown( |
|
id='git-provider', |
|
options=[ |
|
{'label': 'GitHub', 'value': 'GitHub'}, |
|
{'label': 'GitLab', 'value': 'GitLab'}, |
|
{'label': 'Gitea', 'value': 'Gitea'} |
|
], |
|
placeholder="Select Git Provider" |
|
) |
|
], width=12, className="mb-3"), |
|
]), |
|
dbc.Row([ |
|
dbc.Col([ |
|
dbc.Input(id='repo-url', placeholder="Repository URL (e.g., MicroHealthLLC/maiko-assistant)", type="text") |
|
], width=12, className="mb-3"), |
|
]), |
|
dbc.Row([ |
|
dbc.Col([ |
|
dbc.Input(id='start-date', placeholder="Start Date (YYYY-MM-DD)", type="text") |
|
], width=12, className="mb-3"), |
|
]), |
|
dbc.Row([ |
|
dbc.Col([ |
|
dbc.Input(id='end-date', placeholder="End Date (YYYY-MM-DD)", type="text") |
|
], width=12, className="mb-3"), |
|
]), |
|
dbc.Row([ |
|
dbc.Col([ |
|
dbc.Input(id='folder-location', placeholder="Folder Location (e.g., documentation/releases/rel)", type="text") |
|
], width=12, className="mb-3"), |
|
]), |
|
dbc.Row([ |
|
dbc.Col([ |
|
dbc.Button("Generate Release Notes", id="generate-button", color="primary", className="me-2 mb-2"), |
|
], width=4), |
|
dbc.Col([ |
|
dbc.Button("Download Markdown", id="download-button", color="secondary", className="me-2 mb-2", disabled=True), |
|
], width=4), |
|
dbc.Col([ |
|
dbc.Button("Create PR", id="pr-button", color="info", className="mb-2", disabled=True), |
|
], width=4), |
|
]), |
|
dbc.Row([ |
|
dbc.Col([ |
|
dcc.Loading( |
|
id="pr-loading", |
|
type="circle", |
|
children=[html.Div(id="pr-output")] |
|
) |
|
], width=12, className="mb-3"), |
|
]), |
|
]), |
|
]) |
|
], className="mb-4"), |
|
dbc.Card([ |
|
dbc.CardBody([ |
|
html.H4("Generated Release Notes"), |
|
dcc.Loading( |
|
id="loading-output", |
|
type="circle", |
|
children=[html.Pre(id="output-notes", style={"white-space": "pre-wrap"})] |
|
) |
|
]) |
|
]), |
|
dcc.Download(id="download-markdown") |
|
]) |
|
|
|
@app.callback( |
|
[Output("output-notes", "children"), |
|
Output("download-button", "disabled"), |
|
Output("pr-button", "disabled"), |
|
Output("download-markdown", "data"), |
|
Output("pr-button", "children"), |
|
Output("pr-output", "children")], |
|
[Input("generate-button", "n_clicks"), |
|
Input("download-button", "n_clicks"), |
|
Input("pr-button", "n_clicks")], |
|
[State("git-provider", "value"), |
|
State("repo-url", "value"), |
|
State("start-date", "value"), |
|
State("end-date", "value"), |
|
State("folder-location", "value")] |
|
) |
|
def handle_all_actions(generate_clicks, download_clicks, pr_clicks, |
|
git_provider, repo_url, start_date, end_date, folder_location): |
|
global generated_file, pr_url |
|
ctx = dash.callback_context |
|
|
|
if not ctx.triggered: |
|
return "", True, True, None, "Create PR", "" |
|
|
|
button_id = ctx.triggered[0]['prop_id'].split('.')[0] |
|
|
|
if button_id == "generate-button": |
|
notes, file_name = generate_release_notes(git_provider, repo_url, start_date, end_date, folder_location) |
|
return notes, False, False, None, "Create PR", "" |
|
|
|
elif button_id == "download-button": |
|
if generated_file is None: |
|
return dash.no_update, dash.no_update, dash.no_update, None, dash.no_update, "" |
|
return (dash.no_update, dash.no_update, dash.no_update, |
|
dcc.send_bytes(generated_file.getvalue(), f"release_notes_{datetime.now().strftime('%Y%m%d%H%M%S')}.md"), |
|
dash.no_update, "") |
|
|
|
elif button_id == "pr-button": |
|
if generated_file is None: |
|
return dash.no_update, dash.no_update, dash.no_update, None, "Error: No file generated", "No file generated" |
|
|
|
markdown_content = generated_file.getvalue().decode() |
|
|
|
result = update_summary_and_create_pr(repo_url, folder_location, start_date, end_date, markdown_content) |
|
|
|
if pr_url: |
|
return dash.no_update, dash.no_update, True, None, f"PR Created", f"PR Created: {pr_url}" |
|
else: |
|
return dash.no_update, dash.no_update, False, None, "PR Creation Failed", result |
|
|
|
return dash.no_update, dash.no_update, dash.no_update, None, dash.no_update, "" |
|
|
|
if __name__ == '__main__': |
|
print("Starting the Dash application...") |
|
app.run(debug=True, host='0.0.0.0', port=7860) |
|
print("Dash application has finished running.") |