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