| 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.") |