diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 386a21c..0040bb3 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -15,6 +15,7 @@ sequenceDiagram App->>Jira: Searches for existing Jira issues Jira->>App: Provides list of existing issues if any App->>Jira: Creates/updates Jira issue + App->>repo: (Optional) Adds 'synced-to-jira' on the Issue App->>repo: (Optional) Adds a comment on the Issue App->>GH: Returns web response @@ -54,4 +55,4 @@ sequenceDiagram App1->>repo: (Optional) Adds a comment on the Issue App1->>GH: Returns web response -``` \ No newline at end of file +``` diff --git a/README.md b/README.md index e4c4e68..fd84593 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,11 @@ settings: # (Optional) (Default: false) Add a new comment in GitHub with a link to Jira created issue add_gh_comment: false + + # (Optional) (Default: false) Add a 'synced-to-jira' label to newly created issues once a + # corresponding ticket is successfully created in Jira. + # This label serves as confirmation that the issue sync process was completed successfully. + add_gh_synced_label: false # (Optional) (Default: true) Synchronize issue description from GitHub to Jira sync_description: true diff --git a/github_jira_sync_app/main.py b/github_jira_sync_app/main.py index 89452f6..a6f8ff5 100644 --- a/github_jira_sync_app/main.py +++ b/github_jira_sync_app/main.py @@ -50,6 +50,8 @@ > This message was autogenerated """ +gh_synced_label_name = "synced-to-jira" + def define_logger(): """Define logger to output to the file and to STDOUT.""" @@ -187,6 +189,15 @@ async def bot(request: Request, payload: dict = Body(...)): ) } + if payload["action"] == "labeled": + if payload["label"]["name"] == gh_synced_label_name: + return { + "msg": ( + f"Action was triggered by Issue being labeled with {gh_synced_label_name}." + " Purposefully ignored as caused by this bot." + ) + } + owner = payload["repository"]["owner"]["login"] repo_name = payload["repository"]["name"] @@ -293,10 +304,13 @@ async def bot(request: Request, payload: dict = Body(...)): new_issue = jira.create_issue(fields=issue_dict) existing_issues.append(new_issue) + if settings.get("add_gh_synced_label", False): + gh_issue.add_to_labels(gh_synced_label_name) + if settings["add_gh_comment"]: - gh_issue.create_comment( - gh_comment_body_template.format(jira_issue_link=new_issue.permalink()) - ) + gh_comment_body = gh_comment_body_template.format(jira_issue_link=new_issue.permalink()) + + gh_issue.create_comment(gh_comment_body) # need this since we allow to sync issue on many actions. And if someone commented # we first create a Jira issue, then create a comment diff --git a/github_jira_sync_app/settings.yaml b/github_jira_sync_app/settings.yaml index 96d7129..37fb375 100644 --- a/github_jira_sync_app/settings.yaml +++ b/github_jira_sync_app/settings.yaml @@ -2,6 +2,7 @@ settings: components: labels: add_gh_comment: false + add_gh_synced_label: false sync_description: true sync_comments: true epic_key: diff --git a/tests/unit/payloads/issue_labeled_synced_to_jira.json b/tests/unit/payloads/issue_labeled_synced_to_jira.json new file mode 100644 index 0000000..c9f1fe1 --- /dev/null +++ b/tests/unit/payloads/issue_labeled_synced_to_jira.json @@ -0,0 +1,210 @@ +{ + "action": "labeled", + "issue": { + "url": "https://api.github.com/repos/beliaev-maksim/test-ci/issues/30", + "repository_url": "https://api.github.com/repos/beliaev-maksim/test-ci", + "labels_url": "https://api.github.com/repos/beliaev-maksim/test-ci/issues/30/labels{/name}", + "comments_url": "https://api.github.com/repos/beliaev-maksim/test-ci/issues/30/comments", + "events_url": "https://api.github.com/repos/beliaev-maksim/test-ci/issues/30/events", + "html_url": "https://github.com/beliaev-maksim/test-ci/issues/30", + "id": 1745060534, + "node_id": "I_kwDOI-HsRM5oA4K2", + "number": 30, + "title": "day after", + "user": { + "login": "beliaev-maksim", + "id": 51964909, + "node_id": "MDQ6VXNlcjUxOTY0OTA5", + "avatar_url": "https://avatars.githubusercontent.com/u/51964909?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/beliaev-maksim", + "html_url": "https://github.com/beliaev-maksim", + "followers_url": "https://api.github.com/users/beliaev-maksim/followers", + "following_url": "https://api.github.com/users/beliaev-maksim/following{/other_user}", + "gists_url": "https://api.github.com/users/beliaev-maksim/gists{/gist_id}", + "starred_url": "https://api.github.com/users/beliaev-maksim/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/beliaev-maksim/subscriptions", + "organizations_url": "https://api.github.com/users/beliaev-maksim/orgs", + "repos_url": "https://api.github.com/users/beliaev-maksim/repos", + "events_url": "https://api.github.com/users/beliaev-maksim/events{/privacy}", + "received_events_url": "https://api.github.com/users/beliaev-maksim/received_events", + "type": "User", + "site_admin": false + }, + "labels": [ + { + "id": 5157325446, + "node_id": "LA_kwDOI-HsRM8AAAABM2aKhg", + "url": "https://api.github.com/repos/beliaev-maksim/test-ci/labels/bug", + "name": "bug", + "color": "d73a4a", + "default": true, + "description": "Something isn't working" + } + ], + "state": "open", + "locked": false, + "assignee": null, + "assignees": [ + + ], + "milestone": null, + "comments": 0, + "created_at": "2023-06-07T05:19:14Z", + "updated_at": "2023-06-07T05:19:14Z", + "closed_at": null, + "author_association": "OWNER", + "active_lock_reason": null, + "body": null, + "reactions": { + "url": "https://api.github.com/repos/beliaev-maksim/test-ci/issues/30/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "timeline_url": "https://api.github.com/repos/beliaev-maksim/test-ci/issues/30/timeline", + "performed_via_github_app": null, + "state_reason": null + }, + "label": { + "id": 5157325446, + "node_id": "LA_kwDOI-HsRM8AAAABM2aKhg", + "url": "https://api.github.com/repos/beliaev-maksim/test-ci/labels/synced-to-jira", + "name": "synced-to-jira", + "color": "d73a4a", + "default": false, + "description": "Ticket in Jira has been created for this issue" + }, + "repository": { + "id": 602008644, + "node_id": "R_kgDOI-HsRA", + "name": "test-ci", + "full_name": "beliaev-maksim/test-ci", + "private": false, + "owner": { + "login": "beliaev-maksim", + "id": 51964909, + "node_id": "MDQ6VXNlcjUxOTY0OTA5", + "avatar_url": "https://avatars.githubusercontent.com/u/51964909?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/beliaev-maksim", + "html_url": "https://github.com/beliaev-maksim", + "followers_url": "https://api.github.com/users/beliaev-maksim/followers", + "following_url": "https://api.github.com/users/beliaev-maksim/following{/other_user}", + "gists_url": "https://api.github.com/users/beliaev-maksim/gists{/gist_id}", + "starred_url": "https://api.github.com/users/beliaev-maksim/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/beliaev-maksim/subscriptions", + "organizations_url": "https://api.github.com/users/beliaev-maksim/orgs", + "repos_url": "https://api.github.com/users/beliaev-maksim/repos", + "events_url": "https://api.github.com/users/beliaev-maksim/events{/privacy}", + "received_events_url": "https://api.github.com/users/beliaev-maksim/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/beliaev-maksim/test-ci", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/beliaev-maksim/test-ci", + "forks_url": "https://api.github.com/repos/beliaev-maksim/test-ci/forks", + "keys_url": "https://api.github.com/repos/beliaev-maksim/test-ci/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/beliaev-maksim/test-ci/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/beliaev-maksim/test-ci/teams", + "hooks_url": "https://api.github.com/repos/beliaev-maksim/test-ci/hooks", + "issue_events_url": "https://api.github.com/repos/beliaev-maksim/test-ci/issues/events{/number}", + "events_url": "https://api.github.com/repos/beliaev-maksim/test-ci/events", + "assignees_url": "https://api.github.com/repos/beliaev-maksim/test-ci/assignees{/user}", + "branches_url": "https://api.github.com/repos/beliaev-maksim/test-ci/branches{/branch}", + "tags_url": "https://api.github.com/repos/beliaev-maksim/test-ci/tags", + "blobs_url": "https://api.github.com/repos/beliaev-maksim/test-ci/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/beliaev-maksim/test-ci/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/beliaev-maksim/test-ci/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/beliaev-maksim/test-ci/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/beliaev-maksim/test-ci/statuses/{sha}", + "languages_url": "https://api.github.com/repos/beliaev-maksim/test-ci/languages", + "stargazers_url": "https://api.github.com/repos/beliaev-maksim/test-ci/stargazers", + "contributors_url": "https://api.github.com/repos/beliaev-maksim/test-ci/contributors", + "subscribers_url": "https://api.github.com/repos/beliaev-maksim/test-ci/subscribers", + "subscription_url": "https://api.github.com/repos/beliaev-maksim/test-ci/subscription", + "commits_url": "https://api.github.com/repos/beliaev-maksim/test-ci/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/beliaev-maksim/test-ci/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/beliaev-maksim/test-ci/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/beliaev-maksim/test-ci/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/beliaev-maksim/test-ci/contents/{+path}", + "compare_url": "https://api.github.com/repos/beliaev-maksim/test-ci/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/beliaev-maksim/test-ci/merges", + "archive_url": "https://api.github.com/repos/beliaev-maksim/test-ci/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/beliaev-maksim/test-ci/downloads", + "issues_url": "https://api.github.com/repos/beliaev-maksim/test-ci/issues{/number}", + "pulls_url": "https://api.github.com/repos/beliaev-maksim/test-ci/pulls{/number}", + "milestones_url": "https://api.github.com/repos/beliaev-maksim/test-ci/milestones{/number}", + "notifications_url": "https://api.github.com/repos/beliaev-maksim/test-ci/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/beliaev-maksim/test-ci/labels{/name}", + "releases_url": "https://api.github.com/repos/beliaev-maksim/test-ci/releases{/id}", + "deployments_url": "https://api.github.com/repos/beliaev-maksim/test-ci/deployments", + "created_at": "2023-02-15T09:57:53Z", + "updated_at": "2023-05-15T09:25:41Z", + "pushed_at": "2023-05-15T09:29:16Z", + "git_url": "git://github.com/beliaev-maksim/test-ci.git", + "ssh_url": "git@github.com:beliaev-maksim/test-ci.git", + "clone_url": "https://github.com/beliaev-maksim/test-ci.git", + "svn_url": "https://github.com/beliaev-maksim/test-ci", + "homepage": null, + "size": 10, + "stargazers_count": 0, + "watchers_count": 0, + "language": "Python", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "has_discussions": false, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 2, + "license": null, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "topics": [ + + ], + "visibility": "public", + "forks": 0, + "open_issues": 2, + "watchers": 0, + "default_branch": "master" + }, + "sender": { + "login": "beliaev-maksim", + "id": 51964909, + "node_id": "MDQ6VXNlcjUxOTY0OTA5", + "avatar_url": "https://avatars.githubusercontent.com/u/51964909?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/beliaev-maksim", + "html_url": "https://github.com/beliaev-maksim", + "followers_url": "https://api.github.com/users/beliaev-maksim/followers", + "following_url": "https://api.github.com/users/beliaev-maksim/following{/other_user}", + "gists_url": "https://api.github.com/users/beliaev-maksim/gists{/gist_id}", + "starred_url": "https://api.github.com/users/beliaev-maksim/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/beliaev-maksim/subscriptions", + "organizations_url": "https://api.github.com/users/beliaev-maksim/orgs", + "repos_url": "https://api.github.com/users/beliaev-maksim/repos", + "events_url": "https://api.github.com/users/beliaev-maksim/events{/privacy}", + "received_events_url": "https://api.github.com/users/beliaev-maksim/received_events", + "type": "User", + "site_admin": false + }, + "installation": { + "id": 35534068, + "node_id": "MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMzU1MzQwNjg=" + } +} diff --git a/tests/unit/test_server.py b/tests/unit/test_server.py index 6de464a..cac61ba 100644 --- a/tests/unit/test_server.py +++ b/tests/unit/test_server.py @@ -231,3 +231,50 @@ def test_issue_closed_as_not_planned(signature_mock): assert response.status_code == 200 assert response.json() == {"msg": "Closed existing Jira Issue as not planned"} + + +@responses.activate(assert_all_requests_are_fired=True) +def test_issue_created_and_synced_label(signature_mock): + """Test when a bug is created on GitHub with the right label and is set + + Tests the following scenario: + 1. Authenticate in GitHub + 2. Get issue from GitHub + 3. Get content of .jira_sync_config.yaml from GitHub repo + 4. Ensure that the issue on GitHub is label with the approved label + 5. Authenticate in Jira + 6. Validate via JQL that this issue does not exist in Jira + 7. Create new issue in Jira + 8. Add synced-to-jira label to the issue. If it doesn't exist it is created + automatically by GitHub and then added + 9. The new webhook sent by GitHub for labeling the Issue with synced-to-jira + is immediately ignored + """ + + responses._add_from_file(UNITTESTS_DIR / "url_responses" / "github_auth.yaml") + responses._add_from_file( + UNITTESTS_DIR / "url_responses" / "github_settings_with_gh_sync_label.yaml" + ) + responses._add_from_file( + UNITTESTS_DIR / "url_responses" / "github_responses_add_synced_label.yaml" + ) + responses._add_from_file(UNITTESTS_DIR / "url_responses" / "jira_jql_no_issues.yaml") + responses._add_from_file(UNITTESTS_DIR / "url_responses" / "jira_auth_responses.yaml") + responses._add_from_file(UNITTESTS_DIR / "url_responses" / "jira_create_issue.yaml") + response = client.post( + "/", + json=_get_json("issue_created_without_label.json"), + ) + + assert response.status_code == 200 + assert response.json() == {"msg": "Issue was created in Jira. "} + + response = client.post("/", json=_get_json("issue_labeled_synced_to_jira.json")) + + assert response.status_code == 200 + assert response.json() == { + "msg": ( + "Action was triggered by Issue being labeled with synced-to-jira. " + "Purposefully ignored as caused by this bot." + ) + } diff --git a/tests/unit/url_responses/github_responses_add_synced_label.yaml b/tests/unit/url_responses/github_responses_add_synced_label.yaml new file mode 100644 index 0000000..7d8e4e0 --- /dev/null +++ b/tests/unit/url_responses/github_responses_add_synced_label.yaml @@ -0,0 +1,10 @@ +# this file is generated automatically via responses. Use _recorder.record() +responses: +# add the synced-to-jira label to the new issue +- response: + auto_calculate_content_length: false + body: '[{"id":208045946,"node_id":"MDU6TGFiZWwyMDgwNDU5NDY=","url":"https://api.github.com:443/repos/beliaev-maksim/test-ci/labels/synced-to-jira","name":"synced-to-jira","description":"Ticket in Jira has been created for this issue","color":"f29513","default":false}]' + content_type: text/plain + method: POST + status: 200 + url: https://api.github.com:443/repos/beliaev-maksim/test-ci/issues/30/labels \ No newline at end of file diff --git a/tests/unit/url_responses/github_settings_with_gh_sync_label.yaml b/tests/unit/url_responses/github_settings_with_gh_sync_label.yaml new file mode 100644 index 0000000..114a2cb --- /dev/null +++ b/tests/unit/url_responses/github_settings_with_gh_sync_label.yaml @@ -0,0 +1,28 @@ +# get content of .jira_sync_config.yaml to load the settings for the repo +responses: +- response: + auto_calculate_content_length: false + body: '{"name":".jira_sync_config.yaml","path":".github/.jira_sync_config.yaml","sha":"a1a8aab6de92fc3eed4211350200ada4a2405b9c","size":297,"url":"https://api.github.com/repos/beliaev-maksim/test-ci/contents/.github/.jira_sync_config.yaml?ref=master","html_url":"https://github.com/beliaev-maksim/test-ci/blob/master/.github/.jira_sync_config.yaml","git_url":"https://api.github.com/repos/beliaev-maksim/test-ci/git/blobs/a1a8aab6de92fc3eed4211350200ada4a2405b9c","download_url":"https://raw.githubusercontent.com/beliaev-maksim/test-ci/master/.github/.jira_sync_config.yaml","type":"file","content":"c2V0dGluZ3M6CiAgY29tcG9uZW50czoKICAgIC0gSW9UCiAgICAtIERBQ0ggVFQKICBsYWJlbHM6CiAgYWRkX2doX2NvbW1lbnQ6IGZhbHNlCiAgYWRkX2doX3N5bmNlZF9sYWJlbDogdHJ1ZQogIHN5bmNfZGVzY3JpcHRpb246IHRydWUKICBzeW5jX2NvbW1lbnRzOiB0cnVlCiAgZXBpY19rZXk6ICJNVEMtMjk2IgogIGppcmFfcHJvamVjdF9rZXk6ICJNVEMiCiAgc3RhdHVzX21hcHBpbmc6CiAgICBvcGVuZWQ6IFVudHJpYWdlZAogICAgY2xvc2VkOiBkb25lCiAgbGFiZWxfbWFwcGluZzoKICAgIGVuaGFuY2VtZW50OiBTdG9yeQo=","encoding":"base64","_links":{"self":"https://api.github.com/repos/beliaev-maksim/test-ci/contents/.github/.jira_sync_config.yaml?ref=master","git":"https://api.github.com/repos/beliaev-maksim/test-ci/git/blobs/a1a8aab6de92fc3eed4211350200ada4a2405b9c","html":"https://github.com/beliaev-maksim/test-ci/blob/master/.github/.jira_sync_config.yaml"}}' + content_type: text/plain + method: GET + status: 200 + url: https://api.github.com:443/repos/beliaev-maksim/test-ci/contents/.github/.jira_sync_config.yaml + + +# That is the base64 encoded content +#settings: +# components: +# - IoT +# - DACH TT +# labels: +# add_gh_comment: false +# add_gh_synced_label: true +# sync_description: true +# sync_comments: true +# epic_key: "MTC-296" +# jira_project_key: "MTC" +# status_mapping: +# opened: Untriaged +# closed: done +# label_mapping: +# enhancement: Story \ No newline at end of file