Skip to content

Commit 5c67e72

Browse files
committed
change github issue creation to only create comment if submission id already has an issue (reopening if closed)
1 parent a5fdf5c commit 5c67e72

File tree

2 files changed

+260
-53
lines changed

2 files changed

+260
-53
lines changed

nmdc_server/api.py

Lines changed: 161 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1266,64 +1266,172 @@ def create_github_issue(submission: schemas_submission.SubmissionMetadataSchema,
12661266
# If the settings for issue creation weren't supplied return, no need to do anything further
12671267
if gh_url is None or token is None:
12681268
return None
1269-
1270-
# Gathering the fields we want to display in the issue
1269+
12711270
headers = {"Authorization": f"Bearer {token}", "Content-Type": "text/plain; charset=utf-8"}
1272-
study_form = submission.metadata_submission.studyForm
1273-
multiomics_form = submission.metadata_submission.multiOmicsForm
1274-
pi_name = study_form.piName
1275-
pi_orcid = study_form.piOrcid
1276-
data_generated = "Yes" if multiomics_form.dataGenerated else "No"
1277-
omics_processing_types = ", ".join(multiomics_form.omicsProcessingTypes)
1278-
sample_types = ", ".join(submission.metadata_submission.templates)
1279-
num_samples = submission.sample_count
1280-
1281-
# some variable data to supply depending on if data has been generated or not
1282-
id_dict = {
1283-
"NCBI ID: ": study_form.NCBIBioProjectId,
1284-
"GOLD ID: ": study_form.GOLDStudyId,
1285-
"JGI ID: ": multiomics_form.JGIStudyId,
1286-
"EMSL ID: ": multiomics_form.studyNumber,
1287-
"Alternative IDs: ": ", ".join(study_form.alternativeNames),
1288-
}
1289-
valid_ids = []
1290-
for key, value in id_dict.items():
1291-
if str(value) != "":
1292-
valid_ids.append(key + value)
1293-
1294-
# assemble the body of the API request
1295-
body_lis = [
1296-
f"Issue created from host: {settings.host}",
1297-
f"Submitter: {user.name}, {user.orcid}",
1298-
f"Submission ID: {submission.id}",
1299-
f"Has data been generated: {data_generated}",
1300-
f"PI name: {pi_name}",
1301-
f"PI orcid: {pi_orcid}",
1302-
f"Status: {SubmissionStatusEnum.SubmittedPendingReview.text}",
1303-
f"Data types: {omics_processing_types}",
1304-
f"Sample type: {sample_types}",
1305-
f"Number of samples: {num_samples}",
1306-
] + valid_ids
1307-
body_string = " \n ".join(body_lis)
1308-
payload_dict = {
1309-
"title": f"NMDC Submission: {submission.id}",
1310-
"body": body_string,
1311-
"assignees": [assignee],
1312-
}
13131271

1314-
payload = json.dumps(payload_dict)
1315-
1316-
# make request and log an error or success depending on reply
1317-
res = requests.post(url=gh_url, data=payload, headers=headers)
1318-
if res.status_code != 201:
1319-
logging.error(f"Github issue creation failed with code {res.status_code}")
1320-
logging.error(res.reason)
1272+
# Check for existing issues first
1273+
existing_issue = check_existing_github_issue(submission.id, headers, gh_url, user)
1274+
if existing_issue:
1275+
logging.info(f"GitHub issue already exists for submission {submission.id}: {existing_issue['html_url']}")
1276+
return existing_issue
1277+
13211278
else:
1322-
logging.info(f"Github issue creation successful with code {res.status_code}")
1323-
logging.info(res.reason)
13241279

1325-
return res
1280+
# Gathering the fields we want to display in the issue
1281+
study_form = submission.metadata_submission.studyForm
1282+
multiomics_form = submission.metadata_submission.multiOmicsForm
1283+
pi_name = study_form.piName
1284+
pi_orcid = study_form.piOrcid
1285+
data_generated = "Yes" if multiomics_form.dataGenerated else "No"
1286+
omics_processing_types = ", ".join(multiomics_form.omicsProcessingTypes)
1287+
sample_types = ", ".join(submission.metadata_submission.templates)
1288+
num_samples = submission.sample_count
1289+
1290+
# some variable data to supply depending on if data has been generated or not
1291+
id_dict = {
1292+
"NCBI ID: ": study_form.NCBIBioProjectId,
1293+
"GOLD ID: ": study_form.GOLDStudyId,
1294+
"JGI ID: ": multiomics_form.JGIStudyId,
1295+
"EMSL ID: ": multiomics_form.studyNumber,
1296+
"Alternative IDs: ": ", ".join(study_form.alternativeNames),
1297+
}
1298+
valid_ids = []
1299+
for key, value in id_dict.items():
1300+
if str(value) != "":
1301+
valid_ids.append(key + value)
1302+
1303+
# assemble the body of the API request
1304+
body_lis = [
1305+
f"Issue created from host: {settings.host}",
1306+
f"Submitter: {user.name}, {user.orcid}",
1307+
f"Submission ID: {submission.id}",
1308+
f"Has data been generated: {data_generated}",
1309+
f"PI name: {pi_name}",
1310+
f"PI orcid: {pi_orcid}",
1311+
f"Status: {SubmissionStatusEnum.SubmittedPendingReview.text}",
1312+
f"Data types: {omics_processing_types}",
1313+
f"Sample type: {sample_types}",
1314+
f"Number of samples: {num_samples}",
1315+
] + valid_ids
1316+
body_string = " \n ".join(body_lis)
1317+
payload_dict = {
1318+
"title": f"NMDC Submission: {submission.id}",
1319+
"body": body_string,
1320+
"assignees": [assignee],
1321+
}
1322+
1323+
payload = json.dumps(payload_dict)
1324+
1325+
# make request and log an error or success depending on reply
1326+
res = requests.post(url=gh_url, data=payload, headers=headers)
1327+
if res.status_code != 201:
1328+
logging.error(f"Github issue creation failed with code {res.status_code}")
1329+
logging.error(res.reason)
1330+
else:
1331+
logging.info(f"Github issue creation successful with code {res.status_code}")
1332+
logging.info(res.reason)
13261333

1334+
return res
1335+
1336+
def update_github_issue_for_resubmission(existing_issue, user, headers):
1337+
"""
1338+
Update an existing GitHub issue to note that the submission was resubmitted.
1339+
Adds a comment and optionally reopens the issue if it was closed.
1340+
"""
1341+
try:
1342+
issue_number = existing_issue.get("number")
1343+
issue_url = existing_issue.get("url") # API URL for the issue
1344+
1345+
if not issue_number or not issue_url:
1346+
logging.error("Could not find issue number or URL for existing GitHub issue")
1347+
return existing_issue
1348+
1349+
# Create a comment noting the resubmission
1350+
from datetime import datetime
1351+
timestamp = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC")
1352+
1353+
comment_body = f"""
1354+
## 🔄 Submission Resubmitted
1355+
1356+
**Resubmitted by:** {user.name} ({user.orcid})
1357+
**Timestamp:** {timestamp}
1358+
**Status:** {SubmissionStatusEnum.SubmittedPendingReview.text}
1359+
1360+
The submission has been updated and resubmitted for review.
1361+
""".strip()
1362+
1363+
# Add comment to the issue
1364+
comment_url = f"{issue_url}/comments"
1365+
comment_payload = {"body": comment_body}
1366+
1367+
comment_response = requests.post(
1368+
comment_url,
1369+
headers=headers,
1370+
data=json.dumps(comment_payload)
1371+
)
1372+
1373+
if comment_response.status_code == 201:
1374+
logging.info(f"Successfully added resubmission comment to GitHub issue #{issue_number}")
1375+
else:
1376+
logging.error(f"Failed to add comment to GitHub issue: {comment_response.status_code}")
1377+
1378+
# If the issue is closed, reopen it
1379+
if existing_issue.get("state") == "closed":
1380+
reopen_payload = {
1381+
"state": "open",
1382+
"state_reason": "reopened"
1383+
}
1384+
1385+
reopen_response = requests.patch(
1386+
issue_url,
1387+
headers=headers,
1388+
data=json.dumps(reopen_payload)
1389+
)
1390+
1391+
if reopen_response.status_code == 200:
1392+
logging.info(f"Successfully reopened GitHub issue #{issue_number}")
1393+
else:
1394+
logging.error(f"Failed to reopen GitHub issue: {reopen_response.status_code}")
1395+
1396+
return existing_issue
1397+
1398+
except Exception as e:
1399+
logging.error(f"Error updating GitHub issue for resubmission: {str(e)}")
1400+
return existing_issue
1401+
1402+
def check_existing_github_issue(submission_id: str, headers: dict, gh_base_url:str, user):
1403+
"""
1404+
Check if a GitHub issue already exists for the given submission ID using GitHub's search API.
1405+
"""
1406+
try:
1407+
repo_url = gh_base_url.replace('/issues', '')
1408+
search_url = f"{repo_url}/issues"
1409+
expected_title = f"NMDC Submission: {submission_id}"
1410+
params = {
1411+
"state": "all",
1412+
"per_page": 100,
1413+
}
1414+
response = requests.get(search_url, headers=headers, params=params)
1415+
1416+
if response.status_code == 200:
1417+
issues = response.json()
1418+
1419+
# Look for an issue with matching title
1420+
for issue in issues:
1421+
if issue.get("title") == expected_title:
1422+
logging.info(f"Found existing GitHub issue for submission {submission_id}")
1423+
updated_issue = update_github_issue_for_resubmission(issue, user, headers)
1424+
return updated_issue
1425+
else:
1426+
logging.info(f"No existing GitHub issue found for submission {submission_id}")
1427+
return None
1428+
else:
1429+
logging.warning(f"Failed to search GitHub issues: {response.status_code}")
1430+
return None
1431+
1432+
except Exception as e:
1433+
logging.error(f"Error searching GitHub issues: {str(e)}")
1434+
return None
13271435

13281436
@router.delete(
13291437
"/metadata_submission/{id}",

tests/test_submission.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from csv import DictReader
22
from datetime import UTC, datetime, timedelta
33

4+
import json
5+
from unittest.mock import Mock, patch
46
import pytest
57
from fastapi.encoders import jsonable_encoder
68
from nmdc_schema.nmdc import SubmissionStatusEnum
@@ -1426,3 +1428,100 @@ def test_delete_submission_study_images_success(
14261428
# Verify the image was deleted from storage
14271429
assert blob_to_delete.exists() is False
14281430
assert other_blob.exists() is True
1431+
1432+
1433+
def test_github_issue_resubmission_creates_comment_only(
1434+
db: Session, client: TestClient, logged_in_user
1435+
):
1436+
"""Test that when a GitHub issue already exists for a submission, only a comment is created (no new issue)."""
1437+
1438+
# Create a submission
1439+
submission = fakes.MetadataSubmissionFactory(
1440+
author=logged_in_user,
1441+
author_orcid=logged_in_user.orcid,
1442+
status=SubmissionStatusEnum.InProgress.text,
1443+
is_test_submission=False,
1444+
)
1445+
fakes.SubmissionRoleFactory(
1446+
submission=submission,
1447+
submission_id=submission.id,
1448+
user_orcid=logged_in_user.orcid,
1449+
role=SubmissionEditorRole.owner,
1450+
)
1451+
db.commit()
1452+
1453+
# Mock the existing GitHub issue that would be found
1454+
existing_issue = {
1455+
"number": 123,
1456+
"url": "https://api.github.com/repos/owner/repo/issues/123",
1457+
"html_url": "https://github.com/owner/repo/issues/123",
1458+
"title": f"NMDC Submission: {submission.id}",
1459+
"state": "open"
1460+
}
1461+
1462+
# Mock responses for the GitHub API calls
1463+
mock_responses = []
1464+
1465+
# Mock the search for existing issues (returns the existing issue)
1466+
search_response = Mock()
1467+
search_response.status_code = 200
1468+
search_response.json.return_value = [existing_issue] # List of issues from repo issues endpoint
1469+
mock_responses.append(search_response)
1470+
1471+
# Mock the comment creation response
1472+
comment_response = Mock()
1473+
comment_response.status_code = 201
1474+
comment_response.json.return_value = {"id": 456, "body": "comment content"}
1475+
mock_responses.append(comment_response)
1476+
1477+
# Patch the requests.get and requests.post calls
1478+
with patch('nmdc_server.api.requests.get') as mock_get, \
1479+
patch('nmdc_server.api.requests.post') as mock_post, \
1480+
patch('nmdc_server.api.settings') as mock_settings:
1481+
1482+
# Configure settings
1483+
mock_settings.github_issue_url = "https://api.github.com/repos/owner/repo/issues"
1484+
mock_settings.github_authentication_token = "fake_token"
1485+
mock_settings.github_issue_assignee = "assignee"
1486+
mock_settings.host = "test-host"
1487+
1488+
# Set up the mock responses
1489+
mock_get.return_value = search_response
1490+
mock_post.return_value = comment_response
1491+
1492+
# Update submission status to trigger GitHub issue creation/update
1493+
payload = {
1494+
"status": SubmissionStatusEnum.SubmittedPendingReview.text,
1495+
"metadata_submission": {}
1496+
}
1497+
1498+
response = client.request(
1499+
method="PATCH",
1500+
url=f"/api/metadata_submission/{submission.id}",
1501+
json=payload,
1502+
)
1503+
1504+
assert response.status_code == 200
1505+
1506+
# Verify that requests.get was called to search for existing issues
1507+
assert mock_get.call_count == 1
1508+
get_call = mock_get.call_args
1509+
assert "https://api.github.com/repos/owner/repo/issues" in get_call[0][0]
1510+
1511+
# Verify that requests.post was called to create a comment (not a new issue)
1512+
assert mock_post.call_count == 1
1513+
post_call = mock_post.call_args
1514+
1515+
# Verify the comment endpoint was called
1516+
assert post_call[0][0] == "https://api.github.com/repos/owner/repo/issues/123/comments"
1517+
1518+
# Verify the comment content includes resubmission information
1519+
comment_data = json.loads(post_call[1]['data'])
1520+
assert "Submission Resubmitted" in comment_data['body']
1521+
assert logged_in_user.name in comment_data['body']
1522+
assert logged_in_user.orcid in comment_data['body']
1523+
assert SubmissionStatusEnum.SubmittedPendingReview.text in comment_data['body']
1524+
1525+
# Verify headers include authorization
1526+
headers = post_call[1]['headers']
1527+
assert headers['Authorization'] == "Bearer fake_token"

0 commit comments

Comments
 (0)