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 # Initialize the Dash app app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) # Hugging Face variables HF_GEMINI_API_KEY = os.environ.get('HF_GEMINI_API_KEY') HF_GITHUB_TOKEN = os.environ.get('HF_GITHUB_TOKEN') # Global variables to store generated files and PR URL 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 # Create Markdown file 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" # Generate file name based on the current date file_name = f"{datetime.now().strftime('%m-%d-%Y')}.md" # Store the generated file content 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) # Generate file name based on end date file_name = f"{end_date}.md" # Determine SUMMARY.md location summary_folder = '/'.join(folder_location.split('/')[:-1]) summary_path = f"{summary_folder}/SUMMARY.md" # Get the current content of 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 # Add new file link at the top of the Releases section 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}" # Create a new branch for the PR 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) # Update SUMMARY.md in the new branch 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 ) # Create the new release notes file 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 ) # Create a pull request 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 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.")