diff --git a/README.md b/README.md index 07759d52..6cfc2628 100644 --- a/README.md +++ b/README.md @@ -960,6 +960,12 @@ jobdir = Job dir: `{symlink}` `jobdir` is used as the fourth line in a comment to a PR when a new job has been created. +```ini +commit_sha = Commit SHA: `{commit_sha}` +``` + +`commit_sha` is used as the format string for the fifth line in a comment to a PR when a new job has been created. The `{commit_sha}` placeholder is replaced with the commit SHA from the cloned repository (i.e., the base branch HEAD at the time the job starts). + ```ini with_accelerator =  and accelerator `{accelerator}` ``` diff --git a/app.cfg.example b/app.cfg.example index be8e2198..f473cff1 100644 --- a/app.cfg.example +++ b/app.cfg.example @@ -415,6 +415,9 @@ new_job_instance_repo = New job on instance `{app_name}` for repository `{repo_i build_on_arch = Building on: `{on_arch}`{on_accelerator} build_for_arch = Building for: `{for_arch}`{for_accelerator} jobdir = Job dir: `{symlink}` +# commit_sha format for the 5th line in the initial PR comment; `{commit_sha}` is replaced with the commit SHA +# from the cloned repository (i.e., the base branch HEAD at the time the job starts). +commit_sha = Commit SHA: {commit_sha} with_accelerator =  and accelerator `{accelerator}` # initial_comment = New job on instance `{app_name}` for repository `{repo_id}`\nBuilding on: `{on_arch}`{on_accelerator}\nBuilding for: `{for_arch}`{for_accelerator}\nJob dir: `{symlink}` # no longer used diff --git a/eessi_bot_event_handler.py b/eessi_bot_event_handler.py index 41787e1b..91e26914 100644 --- a/eessi_bot_event_handler.py +++ b/eessi_bot_event_handler.py @@ -633,7 +633,8 @@ def handle_bot_command_status(self, event_info, bot_command): # Keep only the first entry for each 'for arch', as that is now the newest status_table_last = { - 'on arch': [], 'for arch': [], 'for repo': [], 'date': [], 'status': [], 'url': [], 'result': [] + 'on arch': [], 'for arch': [], 'for repo': [], 'date': [], 'status': [], 'url': [], 'result': [], + 'commit sha': [] } for x in range(0, len(sorted_table['date'])): # Check if the current 'for arch' AND 'for repo' are already in the status_table_last. If not, add it @@ -664,13 +665,18 @@ def handle_bot_command_status(self, event_info, bot_command): comment_status = '' comment_status += "\nThis is the status of all the `bot: build` commands:" - comment_status += "\n|on|for|repo|result|date|status|url|" - comment_status += "\n|----|----|----|------|----|------|---|" + + # Build header + all_columns = ['on', 'for', 'repo', 'result', 'commit SHA', 'date', 'status', 'url'] + comment_status += f"\n|{'|'.join(all_columns)}|" + comment_status += f"\n|{'|'.join(['----'] * len(all_columns))}|" + for x in range(0, len(status_table['date'])): comment_status += f"\n|{status_table['on arch'][x]}|" comment_status += f"{status_table['for arch'][x]}|" comment_status += f"{status_table['for repo'][x]}|" comment_status += f"{status_table['result'][x]}|" + comment_status += f"{status_table['commit sha'][x]}|" comment_status += f"{status_table['date'][x]}|" comment_status += f"{status_table['status'][x]}|" comment_status += f"{status_table['url'][x]}|" diff --git a/tasks/build.py b/tasks/build.py index 6c191013..83be0e23 100644 --- a/tasks/build.py +++ b/tasks/build.py @@ -972,6 +972,10 @@ def create_pr_comment(job, job_id, app_name, pr, symlink, build_params): # get current date and time dt = datetime.now(timezone.utc) + # Get commit SHA from cloned repo + commit_sha, _, _ = run_cmd('git rev-parse HEAD', 'Get commit SHA', job.working_dir, raise_on_error=False) + commit_sha = commit_sha.strip() if commit_sha else '' + # construct initial job comment buildenv = config.read_config()[config.SECTION_BUILDENV] job_handover_protocol = buildenv.get(config.BUILDENV_SETTING_JOB_HANDOVER_PROTOCOL) @@ -979,6 +983,27 @@ def create_pr_comment(job, job_id, app_name, pr, symlink, build_params): build_on_arch = submitted_job_comments_cfg[config.SUBMITTED_JOB_COMMENTS_SETTING_BUILD_ON_ARCH] build_for_arch = submitted_job_comments_cfg[config.SUBMITTED_JOB_COMMENTS_SETTING_BUILD_FOR_ARCH] jobdir = submitted_job_comments_cfg[config.SUBMITTED_JOB_COMMENTS_SETTING_JOBDIR] + commit_sha_fmt = submitted_job_comments_cfg.get( + config.SUBMITTED_JOB_COMMENTS_SETTING_COMMIT_SHA, 'Commit SHA: {commit_sha}') + + # Build header lines (1-4) + header_lines = (f"{new_job_instance_repo}\n" + f"{build_on_arch}\n" + f"{build_for_arch}\n" + f"{jobdir}\n").format( + app_name=app_name, + on_arch=on_arch, + for_arch=for_arch, + symlink=symlink, + repo_id=job.repo_id, + on_accelerator=on_accelerator_str, + for_accelerator=for_accelerator_str) + + # Build line 5 + commit_sha_line = '' + if commit_sha_fmt: + commit_sha_line += f"{commit_sha_fmt.format(commit_sha=commit_sha)}\n" + if job_handover_protocol == config.JOB_HANDOVER_PROTOCOL_DELAYED_BEGIN: release_msg_string = config.SUBMITTED_JOB_COMMENTS_SETTING_AWAITS_RELEASE_DELAYED_BEGIN_MSG release_comment_template = submitted_job_comments_cfg[release_msg_string] @@ -987,44 +1012,24 @@ def create_pr_comment(job, job_id, app_name, pr, symlink, build_params): poll_interval = int(job_manager_cfg.get(config.JOB_MANAGER_SETTING_POLL_INTERVAL)) delay_factor = float(buildenv.get(config.BUILDENV_SETTING_JOB_DELAY_BEGIN_FACTOR, 2)) eligible_in_seconds = int(poll_interval * delay_factor) - job_comment = (f"{new_job_instance_repo}\n" - f"{build_on_arch}\n" - f"{build_for_arch}\n" - f"{jobdir}\n" - f"|date|job status|comment|\n" - f"|----------|----------|------------------------|\n" - f"|{dt.strftime('%b %d %X %Z %Y')}|" - f"submitted|" - f"{release_comment_template}|").format( - app_name=app_name, - on_arch=on_arch, - for_arch=for_arch, - symlink=symlink, - repo_id=job.repo_id, - job_id=job_id, - delay_seconds=eligible_in_seconds, - on_accelerator=on_accelerator_str, - for_accelerator=for_accelerator_str) + table = (f"|date|job status|comment|\n" + f"|----------|----------|------------------------|\n" + f"|{dt.strftime('%b %d %X %Z %Y')}|" + f"submitted|" + f"{release_comment_template}|").format( + job_id=job_id, + delay_seconds=eligible_in_seconds) + job_comment = header_lines + commit_sha_line + table else: release_msg_string = config.SUBMITTED_JOB_COMMENTS_SETTING_AWAITS_RELEASE_HOLD_RELEASE_MSG release_comment_template = submitted_job_comments_cfg[release_msg_string] - job_comment = (f"{new_job_instance_repo}\n" - f"{build_on_arch}\n" - f"{build_for_arch}\n" - f"{jobdir}\n" - f"|date|job status|comment|\n" - f"|----------|----------|------------------------|\n" - f"|{dt.strftime('%b %d %X %Z %Y')}|" - f"submitted|" - f"{release_comment_template}|").format( - app_name=app_name, - on_arch=on_arch, - for_arch=for_arch, - symlink=symlink, - repo_id=job.repo_id, - job_id=job_id, - on_accelerator=on_accelerator_str, - for_accelerator=for_accelerator_str) + table = (f"|date|job status|comment|\n" + f"|----------|----------|------------------------|\n" + f"|{dt.strftime('%b %d %X %Z %Y')}|" + f"submitted|" + f"{release_comment_template}|").format( + job_id=job_id) + job_comment = header_lines + commit_sha_line + table # create comment to pull request repo_name = pr.base.repo.full_name @@ -1212,7 +1217,8 @@ def request_bot_build_issue_comments(repo_name, pr_number): """ fn = sys._getframe().f_code.co_name - status_table = {'on arch': [], 'for arch': [], 'for repo': [], 'date': [], 'status': [], 'url': [], 'result': []} + status_table = {'on arch': [], 'for arch': [], 'for repo': [], 'date': [], 'status': [], 'url': [], + 'result': [], 'commit sha': []} cfg = config.read_config() github_section = cfg[config.SECTION_GITHUB] api_timeout = int(github_section.get(config.GITHUB_SETTING_API_TIMEOUT, 10)) @@ -1342,6 +1348,18 @@ def request_bot_build_issue_comments(repo_name, pr_number): msg += f"{for_arch_re.pattern}\n" raise ValueError(msg) + # Extract commit SHA (line 5, index 4) + commit_sha = '' + commit_sha_fmt = submitted_job_comments_section.get( + config.SUBMITTED_JOB_COMMENTS_SETTING_COMMIT_SHA, 'Commit SHA: `{commit_sha}`') + if len(comment_body) >= 5: + commit_sha_re = template_to_regex(commit_sha_fmt) + commit_sha_match = re.match(commit_sha_re, comment_body[4]) + if commit_sha_match: + commit_sha = commit_sha_match.group('commit_sha') + else: + commit_sha = '' + # get date, status, url and result from the markdown table comment_table = comment['body'][comment['body'].find('|'):comment['body'].rfind('|')+1] @@ -1397,6 +1415,7 @@ def request_bot_build_issue_comments(repo_name, pr_number): status_table['status'].append(status) status_table['url'].append(url) status_table['result'].append(result) + status_table['commit sha'].append(commit_sha) return status_table diff --git a/tests/test_app.cfg b/tests/test_app.cfg index d0fff239..e35586ef 100644 --- a/tests/test_app.cfg +++ b/tests/test_app.cfg @@ -25,6 +25,7 @@ new_job_instance_repo = New job on instance `{app_name}` for repository `{repo_i build_on_arch = Building on: `{on_arch}`{on_accelerator} build_for_arch = Building for: `{for_arch}`{for_accelerator} jobdir = Job dir: `{symlink}` +commit_sha = Commit SHA: `{commit_sha}` with_accelerator =  and accelerator `{accelerator}` [new_job_comments] @@ -37,3 +38,6 @@ running_job = job `{job_id}` is running [bot_control] command_permission = user01 second_user + +[github] +api_timeout = 10 diff --git a/tests/test_task_build.py b/tests/test_task_build.py index cab91a35..26e26a82 100644 --- a/tests/test_task_build.py +++ b/tests/test_task_build.py @@ -18,7 +18,7 @@ import filecmp import os import re -from unittest.mock import patch +from unittest.mock import Mock, patch # Third party imports (anything installed into the local Python environment) from collections import namedtuple @@ -26,7 +26,7 @@ import pytest # Local application imports (anything from EESSI/eessi-bot-software-layer) -from tasks.build import Job, create_pr_comment +from tasks.build import Job, create_pr_comment, request_bot_build_issue_comments from tools import run_cmd, run_subprocess from tools.build_params import EESSIBotBuildParams from tools.job_metadata import create_metadata_file, read_metadata_file @@ -528,3 +528,88 @@ def test_create_read_metadata_file(mocked_github, tmp_path): job_id5 = "555" with pytest.raises(TypeError): create_metadata_file(job5, job_id5, pr_comment) + + +@pytest.mark.repo_name("EESSI/software-layer") +@pytest.mark.pr_number(1) +def test_create_pr_comment_with_commit_sha(monkeypatch, mocked_github, tmp_path): + """Tests that create_pr_comment includes commit SHA from cloned repo.""" + import subprocess + monkeypatch.setattr('tools.pr_comments.github', mocked_github) + + # Set up a git repo in tmp_path with a commit + subprocess.run(['git', 'init'], cwd=tmp_path, capture_output=True) + subprocess.run(['git', 'config', 'user.name', 'test'], cwd=tmp_path, capture_output=True) + subprocess.run(['git', 'config', 'user.email', 'test@test.com'], cwd=tmp_path, capture_output=True) + test_file = os.path.join(tmp_path, 'test.txt') + with open(test_file, 'w') as f: + f.write('test content') + subprocess.run(['git', 'add', '.'], cwd=tmp_path, capture_output=True) + subprocess.run(['git', 'commit', '-m', 'Initial commit'], cwd=tmp_path, capture_output=True) + + ym = datetime.today().strftime('%Y.%m') + pr_number = 1 + job = Job(tmp_path, "test/architecture", "EESSI", "--speed-up", ym, pr_number, "fpga/magic", "user01") + build_params = EESSIBotBuildParams("arch=amd/zen4,accel=nvidia/cc90") + + job_id = "123" + app_name = "pytest" + + repo_name = "EESSI/software-layer" + repo = mocked_github.get_repo(repo_name) + pr = repo.get_pull(pr_number) + symlink = "/symlink" + comment = create_pr_comment(job, job_id, app_name, pr, symlink, build_params) + + # Get the actual commit SHA + result = subprocess.run(['git', 'rev-parse', 'HEAD'], cwd=tmp_path, capture_output=True, text=True) + expected_sha = result.stdout.strip() + + assert comment.id == 1 + assert f"Commit SHA: `{expected_sha}`" in comment.body + + +@pytest.mark.repo_name("EESSI/software-layer") +@pytest.mark.pr_number(1) +def test_request_bot_build_issue_comments(monkeypatch): + """Tests that request_bot_build_issue_comments extracts commit SHA.""" + from tools import config as build_config + original_read_config = build_config.read_config + + def mock_read_config(path='app.cfg'): + cfg = original_read_config(path) + return cfg + + monkeypatch.setattr('tasks.build.config.read_config', mock_read_config) + + # Mock the GitHub token + token_mock = Mock() + token_mock.token = 'mock-token' + monkeypatch.setattr('tasks.build.github.token', lambda: token_mock) + monkeypatch.setattr('tasks.build.github.get_instance', lambda: Mock()) + + # Mock the response from the GitHub API + comment_body = "\n".join([ + "New job on instance `pytest` for repository `EESSI/software-layer`", + "Building on: `x86_64/generic`", + "Building for: `x86_64/generic`", + "Job dir: `symlink`", + "Commit SHA: `abc123`", + "|date|job status|comment|", + "|----------|----------|------------------------|", + "|Jan 01 00:00:00 UTC 2025|finished|SUCCESS|", + ]) + response_mock = Mock() + response_mock.json.return_value = [{'body': comment_body, 'html_url': 'https://example.com'}] + response_mock.links = {} + response_mock.headers = { + 'X-RateLimit-Reset': '0', + 'X-RateLimit-Limit': '5000', + 'X-RateLimit-Remaining': '4999', + } + monkeypatch.setattr('tasks.build.requests.get', lambda *args, **kwargs: response_mock) + + status_table = request_bot_build_issue_comments('EESSI/software-layer', 1) + + assert status_table['commit sha'] == ['abc123'] + assert status_table['result'] == [':grin: SUCCESS'] diff --git a/tools/config.py b/tools/config.py index 10a7590d..2a7dced1 100644 --- a/tools/config.py +++ b/tools/config.py @@ -130,6 +130,7 @@ SUBMITTED_JOB_COMMENTS_SETTING_JOBDIR = 'jobdir' SUBMITTED_JOB_COMMENTS_SETTING_INITIAL_COMMENT = 'initial_comment' SUBMITTED_JOB_COMMENTS_SETTING_WITH_ACCELERATOR = 'with_accelerator' +SUBMITTED_JOB_COMMENTS_SETTING_COMMIT_SHA = 'commit_sha' SECTION_CLEAN_UP = 'clean_up' CLEAN_UP_SETTING_TRASH_BIN_ROOT_DIR = 'trash_bin_dir'