Browse Source

feat(ingest): Automatically associate commits to checksum release (#36491)

Feature for Workflow 2.0. If the SDK is configured to file an event with the release version matching the release commit SHA, ingest will look to see if there have been commits between the release version and the previous release on Github. If there has been, it will register those GH commits as sentry commits and add them to the release.

This will allow sentry to only notify developers who worked on the current release and reduce notification spam.
Aniket Das 2 years ago
parent
commit
272d35503a

+ 370 - 83
fixtures/github.py

@@ -220,6 +220,9 @@ PUSH_EVENT_EXAMPLE_INSTALLATION = r"""{
   }
 }"""
 
+LATER_COMMIT_SHA = "6dcb09b5b57875f334f61aebed695e2e4193db5e"
+EARLIER_COMMIT_SHA = "2edc6bc02366b2b9b0e8fa2ace3f93502e324b39"
+# Example taken from github documentation https://docs.github.com/en/rest/commits/commits#compare-two-commits
 COMPARE_COMMITS_EXAMPLE = """{
   "url": "https://api.github.com/repos/octocat/Hello-World/compare/master...topic",
   "html_url": "https://github.com/octocat/Hello-World/compare/master...topic",
@@ -473,6 +476,7 @@ COMPARE_COMMITS_EXAMPLE = """{
   ]
 }"""
 
+# Example taken from github example https://docs.github.com/en/rest/commits/commits#list-commits
 GET_LAST_COMMITS_EXAMPLE = """[
   {
     "url": "https://api.github.com/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e",
@@ -2085,7 +2089,8 @@ PULL_REQUEST_CLOSED_EVENT_EXAMPLE = b"""{
   }
 }"""
 
-COMPARE_COMMITS_EXAMPLE = """{
+# Example taken from github with additional example commit added https://docs.github.com/en/rest/commits/commits#compare-two-commits
+COMPARE_COMMITS_EXAMPLE_WITH_INTERMEDIATE = """{
   "url": "https://api.github.com/repos/octocat/Hello-World/compare/master...topic",
   "html_url": "https://github.com/octocat/Hello-World/compare/master...topic",
   "permalink_url": "https://github.com/octocat/Hello-World/compare/octocat:bbcd538c8e72b8c175046e27cc8f907076331401...octocat:0328041d1152db8ae77652d1618a02e57f745f17",
@@ -2241,11 +2246,86 @@ COMPARE_COMMITS_EXAMPLE = """{
       }
     ]
   },
-  "status": "behind",
-  "ahead_by": 1,
-  "behind_by": 2,
-  "total_commits": 1,
+  "status": "ahead",
+  "ahead_by": 2,
+  "behind_by": 0,
+  "total_commits": 2,
   "commits": [
+    {
+      "url": "https://api.github.com/repos/octocat/Hello-World/commits/2edc6bc02366b2b9b0e8fa2ace3f93502e324b39",
+      "sha": "2edc6bc02366b2b9b0e8fa2ace3f93502e324b39",
+      "html_url": "https://github.com/octocat/Hello-World/commit/2edc6bc02366b2b9b0e8fa2ace3f93502e324b39",
+      "comments_url": "https://api.github.com/repos/octocat/Hello-World/commits/2edc6bc02366b2b9b0e8fa2ace3f93502e324b39/comments",
+      "commit": {
+        "url": "https://api.github.com/repos/octocat/Hello-World/git/commits/2edc6bc02366b2b9b0e8fa2ace3f93502e324b39",
+        "author": {
+          "name": "Monalisa Octocat",
+          "email": "support@github.com",
+          "date": "2011-04-14T16:00:49Z"
+        },
+        "committer": {
+          "name": "Monalisa Octocat",
+          "email": "support@github.com",
+          "date": "2011-04-14T16:00:49Z"
+        },
+        "message": "Fix all the bugs",
+        "tree": {
+          "url": "https://api.github.com/repos/octocat/Hello-World/tree/2edc6bc02366b2b9b0e8fa2ace3f93502e324b39",
+          "sha": "2edc6bc02366b2b9b0e8fa2ace3f93502e324b39"
+        },
+        "comment_count": 0,
+        "verification": {
+          "verified": true,
+          "reason": "valid",
+          "signature": "-----BEGIN PGP MESSAGE----------END PGP MESSAGE-----",
+          "payload": "tree 2edc6bc02366b2b9b0e8fa2ace3f93502e324b39..."
+        }
+      },
+      "author": {
+        "login": "octocat",
+        "id": 1,
+        "avatar_url": "https://github.com/images/error/octocat_happy.gif",
+        "gravatar_id": "",
+        "url": "https://api.github.com/users/octocat",
+        "html_url": "https://github.com/octocat",
+        "followers_url": "https://api.github.com/users/octocat/followers",
+        "following_url": "https://api.github.com/users/octocat/following{/other_user}",
+        "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
+        "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
+        "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
+        "organizations_url": "https://api.github.com/users/octocat/orgs",
+        "repos_url": "https://api.github.com/users/octocat/repos",
+        "events_url": "https://api.github.com/users/octocat/events{/privacy}",
+        "received_events_url": "https://api.github.com/users/octocat/received_events",
+        "type": "User",
+        "site_admin": false
+      },
+      "committer": {
+        "login": "octocat",
+        "id": 1,
+        "avatar_url": "https://github.com/images/error/octocat_happy.gif",
+        "gravatar_id": "",
+        "url": "https://api.github.com/users/octocat",
+        "html_url": "https://github.com/octocat",
+        "followers_url": "https://api.github.com/users/octocat/followers",
+        "following_url": "https://api.github.com/users/octocat/following{/other_user}",
+        "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
+        "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
+        "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
+        "organizations_url": "https://api.github.com/users/octocat/orgs",
+        "repos_url": "https://api.github.com/users/octocat/repos",
+        "events_url": "https://api.github.com/users/octocat/events{/privacy}",
+        "received_events_url": "https://api.github.com/users/octocat/received_events",
+        "type": "User",
+        "site_admin": false
+      },
+      "parents": [
+        {
+          "url": "https://api.github.com/repos/octocat/Hello-World/commits/2edc6bc02366b2b9b0e8fa2ace3f93502e324b39",
+          "sha": "2edc6bc02366b2b9b0e8fa2ace3f93502e324b39"
+        }
+      ]
+    },
     {
       "url": "https://api.github.com/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e",
       "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e",
@@ -2338,84 +2418,7 @@ COMPARE_COMMITS_EXAMPLE = """{
   ]
 }"""
 
-GET_LAST_COMMITS_EXAMPLE = """[
-  {
-    "url": "https://api.github.com/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e",
-    "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e",
-    "html_url": "https://github.com/octocat/Hello-World/commit/6dcb09b5b57875f334f61aebed695e2e4193db5e",
-    "comments_url": "https://api.github.com/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e/comments",
-    "commit": {
-      "url": "https://api.github.com/repos/octocat/Hello-World/git/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e",
-      "author": {
-        "name": "Monalisa Octocat",
-        "email": "support@github.com",
-        "date": "2011-04-14T16:00:49Z"
-      },
-      "committer": {
-        "name": "Monalisa Octocat",
-        "email": "support@github.com",
-        "date": "2011-04-14T16:00:49Z"
-      },
-      "message": "Fix all the bugs",
-      "tree": {
-        "url": "https://api.github.com/repos/octocat/Hello-World/tree/6dcb09b5b57875f334f61aebed695e2e4193db5e",
-        "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e"
-      },
-      "comment_count": 0,
-      "verification": {
-        "verified": true,
-        "reason": "valid",
-        "signature": "-----BEGIN PGP MESSAGE----------END PGP MESSAGE-----",
-        "payload": "tree 6dcb09b5b57875f334f61aebed695e2e4193db5e..."
-      }
-    },
-    "author": {
-      "login": "octocat",
-      "id": 1,
-      "avatar_url": "https://github.com/images/error/octocat_happy.gif",
-      "gravatar_id": "",
-      "url": "https://api.github.com/users/octocat",
-      "html_url": "https://github.com/octocat",
-      "followers_url": "https://api.github.com/users/octocat/followers",
-      "following_url": "https://api.github.com/users/octocat/following{/other_user}",
-      "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
-      "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
-      "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
-      "organizations_url": "https://api.github.com/users/octocat/orgs",
-      "repos_url": "https://api.github.com/users/octocat/repos",
-      "events_url": "https://api.github.com/users/octocat/events{/privacy}",
-      "received_events_url": "https://api.github.com/users/octocat/received_events",
-      "type": "User",
-      "site_admin": false
-    },
-    "committer": {
-      "login": "octocat",
-      "id": 1,
-      "avatar_url": "https://github.com/images/error/octocat_happy.gif",
-      "gravatar_id": "",
-      "url": "https://api.github.com/users/octocat",
-      "html_url": "https://github.com/octocat",
-      "followers_url": "https://api.github.com/users/octocat/followers",
-      "following_url": "https://api.github.com/users/octocat/following{/other_user}",
-      "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
-      "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
-      "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
-      "organizations_url": "https://api.github.com/users/octocat/orgs",
-      "repos_url": "https://api.github.com/users/octocat/repos",
-      "events_url": "https://api.github.com/users/octocat/events{/privacy}",
-      "received_events_url": "https://api.github.com/users/octocat/received_events",
-      "type": "User",
-      "site_admin": false
-    },
-    "parents": [
-      {
-        "url": "https://api.github.com/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e",
-        "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e"
-      }
-    ]
-  }
-]"""
-
+# Example taken from github https://docs.github.com/en/rest/commits/commits#get-a-commit
 GET_COMMIT_EXAMPLE = r"""
 {
   "url": "https://api.github.com/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e",
@@ -2544,3 +2547,287 @@ GET_COMMIT_EXAMPLE = r"""
   ]
 }
 """
+
+# Example taken from github https://docs.github.com/en/rest/commits/commits#get-a-commit
+GET_PRIOR_COMMIT_EXAMPLE = r"""
+{
+  "url": "https://api.github.com/repos/octocat/Hello-World/commits/2edc6bc02366b2b9b0e8fa2ace3f93502e324b39",
+  "sha": "2edc6bc02366b2b9b0e8fa2ace3f93502e324b39",
+  "node_id": "MDY6Q29tbWl0NmRjYjA5YjViNTc4NzVmMzM0ZjYxYWViZWQ2OTVlMmU0MTkzZGI1ZQ==",
+  "html_url": "https://github.com/octocat/Hello-World/commit/2edc6bc02366b2b9b0e8fa2ace3f93502e324b39",
+  "comments_url": "https://api.github.com/repos/octocat/Hello-World/commits/2edc6bc02366b2b9b0e8fa2ace3f93502e324b39/comments",
+  "commit": {
+    "url": "https://api.github.com/repos/octocat/Hello-World/git/commits/2edc6bc02366b2b9b0e8fa2ace3f93502e324b39",
+    "author": {
+      "name": "Monalisa Octocat",
+      "email": "support@github.com",
+      "date": "2011-04-14T16:00:49Z"
+    },
+    "committer": {
+      "name": "Monalisa Octocat",
+      "email": "support@github.com",
+      "date": "2011-04-14T16:00:49Z"
+    },
+    "message": "Fix all the bugs",
+    "tree": {
+      "url": "https://api.github.com/repos/octocat/Hello-World/tree/2edc6bc02366b2b9b0e8fa2ace3f93502e324b39",
+      "sha": "2edc6bc02366b2b9b0e8fa2ace3f93502e324b39"
+    },
+    "comment_count": 0,
+    "verification": {
+      "verified": false,
+      "reason": "unsigned",
+      "signature": null,
+      "payload": null
+    }
+  },
+  "author": {
+    "login": "octocat",
+    "id": 1,
+    "node_id": "MDQ6VXNlcjE=",
+    "avatar_url": "https://github.com/images/error/octocat_happy.gif",
+    "gravatar_id": "",
+    "url": "https://api.github.com/users/octocat",
+    "html_url": "https://github.com/octocat",
+    "followers_url": "https://api.github.com/users/octocat/followers",
+    "following_url": "https://api.github.com/users/octocat/following{/other_user}",
+    "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
+    "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
+    "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
+    "organizations_url": "https://api.github.com/users/octocat/orgs",
+    "repos_url": "https://api.github.com/users/octocat/repos",
+    "events_url": "https://api.github.com/users/octocat/events{/privacy}",
+    "received_events_url": "https://api.github.com/users/octocat/received_events",
+    "type": "User",
+    "site_admin": false
+  },
+  "committer": {
+    "login": "octocat",
+    "id": 1,
+    "node_id": "MDQ6VXNlcjE=",
+    "avatar_url": "https://github.com/images/error/octocat_happy.gif",
+    "gravatar_id": "",
+    "url": "https://api.github.com/users/octocat",
+    "html_url": "https://github.com/octocat",
+    "followers_url": "https://api.github.com/users/octocat/followers",
+    "following_url": "https://api.github.com/users/octocat/following{/other_user}",
+    "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
+    "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
+    "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
+    "organizations_url": "https://api.github.com/users/octocat/orgs",
+    "repos_url": "https://api.github.com/users/octocat/repos",
+    "events_url": "https://api.github.com/users/octocat/events{/privacy}",
+    "received_events_url": "https://api.github.com/users/octocat/received_events",
+    "type": "User",
+    "site_admin": false
+  },
+  "parents": [
+    {
+      "url": "https://api.github.com/repos/octocat/Hello-World/commits/2edc6bc02366b2b9b0e8fa2ace3f93502e324b39",
+      "sha": "2edc6bc02366b2b9b0e8fa2ace3f93502e324b39"
+    }
+  ],
+  "stats": {
+    "additions": 104,
+    "deletions": 4,
+    "total": 108
+  },
+  "files": [
+    {
+      "filename": "file1.txt",
+      "additions": 10,
+      "deletions": 2,
+      "changes": 12,
+      "status": "modified",
+      "raw_url": "https://github.com/octocat/Hello-World/raw/7ca483543807a51b6079e54ac4cc392bc29ae284/file1.txt",
+      "blob_url": "https://github.com/octocat/Hello-World/blob/7ca483543807a51b6079e54ac4cc392bc29ae284/file1.txt",
+      "patch": "@@ -29,7 +29,7 @@\n....."
+    },
+    {
+      "filename": "added.txt",
+      "additions": 10,
+      "deletions": 0,
+      "changes": 0,
+      "status": "added",
+      "raw_url": "https://github.com/octocat/Hello-World/raw/7ca483543807a51b6079e54ac4cc392bc29ae284/added.txt",
+      "blob_url": "https://github.com/octocat/Hello-World/blob/7ca483543807a51b6079e54ac4cc392bc29ae284/added.txt",
+      "patch": "@@ -29,7 +29,7 @@\n....."
+    },
+    {
+      "filename": "removed.txt",
+      "additions": 0,
+      "deletions": 10,
+      "changes": 0,
+      "status": "removed",
+      "raw_url": "https://github.com/octocat/Hello-World/raw/7ca483543807a51b6079e54ac4cc392bc29ae284/added.txt",
+      "blob_url": "https://github.com/octocat/Hello-World/blob/7ca483543807a51b6079e54ac4cc392bc29ae284/added.txt",
+      "patch": "@@ -29,7 +29,7 @@\n....."
+    },
+    {
+      "filename": "renamed.txt",
+      "previous_filename": "old_name.txt",
+      "additions": 0,
+      "deletions": 0,
+      "changes": 0,
+      "status": "renamed",
+      "raw_url": "https://github.com/octocat/Hello-World/raw/7ca483543807a51b6079e54ac4cc392bc29ae284/added.txt",
+      "blob_url": "https://github.com/octocat/Hello-World/blob/7ca483543807a51b6079e54ac4cc392bc29ae284/added.txt",
+      "patch": "@@ -29,7 +29,7 @@\n....."
+    }
+  ]
+}
+"""
+
+# Example taken from github with extra commit added https://docs.github.com/en/rest/commits/commits#list-commits
+GET_LAST_2_COMMITS_EXAMPLE = """[
+{
+  "url": "https://api.github.com/repos/octocat/Hello-World/commits/2edc6bc02366b2b9b0e8fa2ace3f93502e324b39",
+  "sha": "2edc6bc02366b2b9b0e8fa2ace3f93502e324b39",
+  "html_url": "https://github.com/octocat/Hello-World/commit/2edc6bc02366b2b9b0e8fa2ace3f93502e324b39",
+  "comments_url": "https://api.github.com/repos/octocat/Hello-World/commits/2edc6bc02366b2b9b0e8fa2ace3f93502e324b39/comments",
+  "commit": {
+    "url": "https://api.github.com/repos/octocat/Hello-World/git/commits/2edc6bc02366b2b9b0e8fa2ace3f93502e324b39",
+    "author": {
+      "name": "Monalisa Octocat",
+      "email": "support@github.com",
+      "date": "2011-04-14T16:00:49Z"
+    },
+    "committer": {
+      "name": "Monalisa Octocat",
+      "email": "support@github.com",
+      "date": "2011-04-14T16:00:49Z"
+    },
+    "message": "Fix all the bugs",
+    "tree": {
+      "url": "https://api.github.com/repos/octocat/Hello-World/tree/2edc6bc02366b2b9b0e8fa2ace3f93502e324b39",
+      "sha": "2edc6bc02366b2b9b0e8fa2ace3f93502e324b39"
+    },
+    "comment_count": 0,
+    "verification": {
+      "verified": true,
+      "reason": "valid",
+      "signature": "-----BEGIN PGP MESSAGE----------END PGP MESSAGE-----",
+      "payload": "tree 2edc6bc02366b2b9b0e8fa2ace3f93502e324b39..."
+    }
+  },
+  "author": {
+    "login": "octocat",
+    "id": 1,
+    "avatar_url": "https://github.com/images/error/octocat_happy.gif",
+    "gravatar_id": "",
+    "url": "https://api.github.com/users/octocat",
+    "html_url": "https://github.com/octocat",
+    "followers_url": "https://api.github.com/users/octocat/followers",
+    "following_url": "https://api.github.com/users/octocat/following{/other_user}",
+    "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
+    "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
+    "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
+    "organizations_url": "https://api.github.com/users/octocat/orgs",
+    "repos_url": "https://api.github.com/users/octocat/repos",
+    "events_url": "https://api.github.com/users/octocat/events{/privacy}",
+    "received_events_url": "https://api.github.com/users/octocat/received_events",
+    "type": "User",
+    "site_admin": false
+  },
+  "committer": {
+    "login": "octocat",
+    "id": 1,
+    "avatar_url": "https://github.com/images/error/octocat_happy.gif",
+    "gravatar_id": "",
+    "url": "https://api.github.com/users/octocat",
+    "html_url": "https://github.com/octocat",
+    "followers_url": "https://api.github.com/users/octocat/followers",
+    "following_url": "https://api.github.com/users/octocat/following{/other_user}",
+    "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
+    "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
+    "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
+    "organizations_url": "https://api.github.com/users/octocat/orgs",
+    "repos_url": "https://api.github.com/users/octocat/repos",
+    "events_url": "https://api.github.com/users/octocat/events{/privacy}",
+    "received_events_url": "https://api.github.com/users/octocat/received_events",
+    "type": "User",
+    "site_admin": false
+  },
+  "parents": [
+    {
+      "url": "https://api.github.com/repos/octocat/Hello-World/commits/2edc6bc02366b2b9b0e8fa2ace3f93502e324b39",
+      "sha": "2edc6bc02366b2b9b0e8fa2ace3f93502e324b39"
+    }
+  ]
+},
+{
+  "url": "https://api.github.com/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e",
+  "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e",
+  "html_url": "https://github.com/octocat/Hello-World/commit/6dcb09b5b57875f334f61aebed695e2e4193db5e",
+  "comments_url": "https://api.github.com/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e/comments",
+  "commit": {
+    "url": "https://api.github.com/repos/octocat/Hello-World/git/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e",
+    "author": {
+      "name": "Monalisa Octocat",
+      "email": "support@github.com",
+      "date": "2011-04-14T16:00:49Z"
+    },
+    "committer": {
+      "name": "Monalisa Octocat",
+      "email": "support@github.com",
+      "date": "2011-04-14T16:00:49Z"
+    },
+    "message": "Fix all the bugs",
+    "tree": {
+      "url": "https://api.github.com/repos/octocat/Hello-World/tree/6dcb09b5b57875f334f61aebed695e2e4193db5e",
+      "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e"
+    },
+    "comment_count": 0,
+    "verification": {
+      "verified": true,
+      "reason": "valid",
+      "signature": "-----BEGIN PGP MESSAGE----------END PGP MESSAGE-----",
+      "payload": "tree 6dcb09b5b57875f334f61aebed695e2e4193db5e..."
+    }
+  },
+  "author": {
+    "login": "octocat",
+    "id": 1,
+    "avatar_url": "https://github.com/images/error/octocat_happy.gif",
+    "gravatar_id": "",
+    "url": "https://api.github.com/users/octocat",
+    "html_url": "https://github.com/octocat",
+    "followers_url": "https://api.github.com/users/octocat/followers",
+    "following_url": "https://api.github.com/users/octocat/following{/other_user}",
+    "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
+    "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
+    "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
+    "organizations_url": "https://api.github.com/users/octocat/orgs",
+    "repos_url": "https://api.github.com/users/octocat/repos",
+    "events_url": "https://api.github.com/users/octocat/events{/privacy}",
+    "received_events_url": "https://api.github.com/users/octocat/received_events",
+    "type": "User",
+    "site_admin": false
+  },
+  "committer": {
+    "login": "octocat",
+    "id": 1,
+    "avatar_url": "https://github.com/images/error/octocat_happy.gif",
+    "gravatar_id": "",
+    "url": "https://api.github.com/users/octocat",
+    "html_url": "https://github.com/octocat",
+    "followers_url": "https://api.github.com/users/octocat/followers",
+    "following_url": "https://api.github.com/users/octocat/following{/other_user}",
+    "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
+    "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
+    "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
+    "organizations_url": "https://api.github.com/users/octocat/orgs",
+    "repos_url": "https://api.github.com/users/octocat/repos",
+    "events_url": "https://api.github.com/users/octocat/events{/privacy}",
+    "received_events_url": "https://api.github.com/users/octocat/received_events",
+    "type": "User",
+    "site_admin": false
+  },
+  "parents": [
+    {
+      "url": "https://api.github.com/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e",
+      "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e"
+    }
+  ]
+}
+]"""

+ 2 - 0
src/sentry/conf/server.py

@@ -1187,6 +1187,8 @@ SENTRY_FEATURES = {
     "projects:servicehooks": False,
     # Use Kafka (instead of Celery) for ingestion pipeline.
     "projects:kafka-ingest": False,
+    # Workflow 2.0 Auto associate commits to commit sha release
+    "projects:auto-associate-commits-to-release": False,
     # Automatically opt IN users to receiving Slack notifications.
     "users:notification-slack-automatic": False,
     # Don't add feature defaults down here! Please add them in their associated

+ 56 - 0
src/sentry/event_manager.py

@@ -2,6 +2,7 @@ import copy
 import ipaddress
 import logging
 import random
+import re
 import time
 from datetime import datetime, timedelta
 from io import BytesIO
@@ -76,10 +77,13 @@ from sentry.models import (
     get_crashreport_key,
 )
 from sentry.models.grouphistory import GroupHistoryStatus, record_group_history
+from sentry.models.integrations.repository_project_path_config import RepositoryProjectPathConfig
 from sentry.plugins.base import plugins
 from sentry.projectoptions.defaults import BETA_GROUPING_CONFIG, DEFAULT_GROUPING_CONFIG
 from sentry.reprocessing2 import is_reprocessed_event, save_unprocessed_event
+from sentry.shared_integrations.exceptions import ApiError
 from sentry.signals import first_event_received, first_transaction_received, issue_unresolved
+from sentry.tasks.commits import fetch_commits
 from sentry.tasks.integrations import kick_off_status_syncs
 from sentry.tasks.process_buffer import buffer_incr
 from sentry.types.activity import ActivityType
@@ -688,6 +692,53 @@ def _pull_out_data(jobs, projects):
         )
 
 
+def _is_commit_sha(version: str):
+    return re.match(r"[0-9a-f]{40}", version) is not None
+
+
+def _associate_commits_with_release(release: Release, project: Project):
+    previous_release = release.get_previous_release(project)
+    possible_repos = (
+        RepositoryProjectPathConfig.objects.select_related(
+            "repository", "organization_integration", "organization_integration__integration"
+        )
+        .filter(project=project, repository__provider="integrations:github")
+        .all()
+    )
+    if possible_repos:
+        # If it does exist, kick off a task to look if the commit exists in the repository
+        target_repo = None
+        for repo_proj_path_model in possible_repos:
+            integration_installation = (
+                repo_proj_path_model.organization_integration.integration.get_installation(
+                    organization_id=project.organization.id
+                )
+            )
+            repo_client = integration_installation.get_client()
+            try:
+                repo_client.get_commit(
+                    repo=repo_proj_path_model.repository.name, sha=release.version
+                )
+                target_repo = repo_proj_path_model.repository
+                break
+            except ApiError as exc:
+                if exc.code != 404:
+                    raise
+
+        if target_repo is not None:
+            # If it does exist, fetch the commits for that repo
+            fetch_commits.apply_async(
+                kwargs={
+                    "release_id": release.id,
+                    "user_id": None,
+                    "refs": [{"repository": target_repo.name, "commit": release.version}],
+                    "prev_release_id": previous_release.id
+                    if previous_release is not None
+                    else None,
+                }
+            )
+
+
 @metrics.wraps("save_event.get_or_create_release_many")
 def _get_or_create_release_many(jobs, projects):
     jobs_with_releases = {}
@@ -711,6 +762,11 @@ def _get_or_create_release_many(jobs, projects):
             date_added=release_date_added[(project_id, version)],
         )
 
+        if features.has(
+            "projects:auto-associate-commits-to-release", projects[project_id]
+        ) and _is_commit_sha(release.version):
+            safe_execute(_associate_commits_with_release, release, projects[project_id])
+
         for job in jobs_to_update:
             # Don't allow a conflicting 'release' tag
             data = job["data"]

+ 3 - 0
src/sentry/features/__init__.py

@@ -211,6 +211,9 @@ default_manager.add("projects:similarity-view-v2", ProjectFeature)
 default_manager.add("projects:plugins", ProjectPluginFeature)
 default_manager.add("users:notification-slack-automatic", UserFeature)
 
+# Workflow 2.0 Project features
+default_manager.add("projects:auto-associate-commits-to-release", ProjectFeature)
+
 
 # This is a gross hardcoded list, but there's no
 # other sensible way to manage this right now without augmenting

+ 8 - 0
src/sentry/models/release.py

@@ -540,6 +540,14 @@ class Release(Model):
     def is_semver_release(self):
         return self.package is not None
 
+    def get_previous_release(self, project):
+        """Get the release prior to this one. None if none exists"""
+        return (
+            ReleaseProject.objects.filter(project=project, release__date_added__lt=self.date_added)
+            .order_by("-release__date_added")
+            .first()
+        )
+
     @staticmethod
     def is_semver_version(version):
         """

+ 3 - 1
src/sentry/tasks/commits.py

@@ -72,7 +72,9 @@ def fetch_commits(release_id, user_id, refs, prev_release_id=None, **kwargs):
     commit_list = []
 
     release = Release.objects.get(id=release_id)
-    user = User.objects.get(id=user_id)
+    # TODO: Need a better way to error handle no user_id. We need the SDK to be able to call this without user context
+    # to autoassociate commits to releases
+    user = User.objects.get(id=user_id) if user_id is not None else None
     prev_release = None
     if prev_release_id is not None:
         try:

+ 280 - 2
tests/sentry/event_manager/test_event_manager.py

@@ -5,10 +5,20 @@ from time import time
 from unittest import mock
 
 import pytest
+import responses
 from django.core.cache import cache
 from django.test.utils import override_settings
 from django.utils import timezone
-
+from rest_framework.status import HTTP_404_NOT_FOUND
+
+from fixtures.github import (
+    COMPARE_COMMITS_EXAMPLE_WITH_INTERMEDIATE,
+    EARLIER_COMMIT_SHA,
+    GET_COMMIT_EXAMPLE,
+    GET_LAST_2_COMMITS_EXAMPLE,
+    GET_PRIOR_COMMIT_EXAMPLE,
+    LATER_COMMIT_SHA,
+)
 from sentry import nodestore
 from sentry.app import tsdb
 from sentry.attachments import CachedAttachment, attachment_cache
@@ -25,6 +35,7 @@ from sentry.ingest.inbound_filters import FilterStatKeys
 from sentry.models import (
     Activity,
     Commit,
+    CommitAuthor,
     Environment,
     ExternalIssue,
     Group,
@@ -42,6 +53,7 @@ from sentry.models import (
     PullRequestCommit,
     Release,
     ReleaseCommit,
+    ReleaseHeadCommit,
     ReleaseProjectEnvironment,
     UserReport,
 )
@@ -49,8 +61,10 @@ from sentry.projectoptions.defaults import DEFAULT_GROUPING_CONFIG, LEGACY_GROUP
 from sentry.spans.grouping.utils import hash_values
 from sentry.testutils import TestCase, assert_mock_called_once_with_partial
 from sentry.types.activity import ActivityType
+from sentry.utils import json
 from sentry.utils.cache import cache_key_for_event
 from sentry.utils.outcomes import Outcome
+from tests.sentry.integrations.github.test_repository import stub_installation_token
 
 
 def make_event(**kwargs):
@@ -64,13 +78,15 @@ def make_event(**kwargs):
     return result
 
 
-class EventManagerTest(TestCase):
+class EventManagerTestMixin:
     def make_release_event(self, release_name, project_id):
         manager = EventManager(make_event(release=release_name))
         manager.normalize()
         event = manager.save(project_id)
         return event
 
+
+class EventManagerTest(TestCase, EventManagerTestMixin):
     def test_similar_message_prefix_doesnt_group(self):
         # we had a regression which caused the default hash to just be
         # 'event.message' instead of '[event.message]' which caused it to
@@ -2066,6 +2082,268 @@ class EventManagerTest(TestCase):
             assert project.get_option("sentry:grouping_config") == DEFAULT_GROUPING_CONFIG
 
 
+class AutoAssociateCommitTest(TestCase, EventManagerTestMixin):
+    def setUp(self):
+        super().setUp()
+        self.repo_name = "example"
+        self.project = self.create_project(name="foo")
+        self.integration = Integration.objects.create(
+            provider="github", name=self.repo_name, external_id="654321"
+        )
+        self.org_integration = self.integration.add_organization(
+            self.project.organization, self.user
+        )
+        self.repo = self.create_repo(
+            project=self.project,
+            name=self.repo_name,
+            provider="integrations:github",
+            integration_id=self.integration.id,
+        )
+        self.repo.update(config={"name": self.repo_name})
+        self.create_code_mapping(
+            project=self.project,
+            repo=self.repo,
+            organization_integration=self.org_integration,
+            stack_root="/stack/root",
+            source_root="/source/root",
+            default_branch="main",
+        )
+        stub_installation_token()
+        responses.add(
+            "GET",
+            f"https://api.github.com/repos/{self.repo_name}/commits/{LATER_COMMIT_SHA}",
+            json=json.loads(GET_COMMIT_EXAMPLE),
+        )
+        responses.add(
+            "GET",
+            f"https://api.github.com/repos/{self.repo_name}/commits/{EARLIER_COMMIT_SHA}",
+            json=json.loads(GET_PRIOR_COMMIT_EXAMPLE),
+        )
+        self.dummy_commit_sha = "a" * 40
+        responses.add(
+            responses.GET,
+            f"https://api.github.com/repos/{self.repo_name}/compare/{self.dummy_commit_sha}...{LATER_COMMIT_SHA}",
+            json=json.loads(COMPARE_COMMITS_EXAMPLE_WITH_INTERMEDIATE),
+        )
+        responses.add(
+            responses.GET,
+            f"https://api.github.com/repos/{self.repo_name}/commits?sha={LATER_COMMIT_SHA}",
+            json=json.loads(GET_LAST_2_COMMITS_EXAMPLE),
+        )
+
+    def _create_first_release_commit(self):
+        # Create a release
+        release = self.create_release(project=self.project, version="abcabcabc")
+        # Create a commit
+        commit = self.create_commit(
+            repo=self.repo,
+            key=self.dummy_commit_sha,
+        )
+        # Make a release head commit
+        ReleaseHeadCommit.objects.create(
+            organization_id=self.project.organization.id,
+            repository_id=self.repo.id,
+            release=release,
+            commit=commit,
+        )
+
+    @mock.patch("sentry.integrations.github.client.get_jwt", return_value=b"jwt_token_1")
+    @responses.activate
+    def test_autoassign_commits_on_sha_release_version(self, get_jwt):
+        with self.feature("projects:auto-associate-commits-to-release"):
+
+            self._create_first_release_commit()
+            # Make a new release with SHA checksum
+            with self.tasks():
+                _ = self.make_release_event(LATER_COMMIT_SHA, self.project.id)
+
+            release2 = Release.objects.get(version=LATER_COMMIT_SHA)
+            commit_list = list(
+                Commit.objects.filter(releasecommit__release=release2).order_by(
+                    "releasecommit__order"
+                )
+            )
+
+            assert len(commit_list) == 2
+            assert commit_list[0].repository_id == self.repo.id
+            assert commit_list[0].organization_id == self.project.organization.id
+            assert commit_list[0].key == EARLIER_COMMIT_SHA
+            assert commit_list[1].repository_id == self.repo.id
+            assert commit_list[1].organization_id == self.project.organization.id
+            assert commit_list[1].key == LATER_COMMIT_SHA
+
+    @mock.patch("sentry.integrations.github.client.get_jwt", return_value=b"jwt_token_1")
+    @responses.activate
+    def test_autoassign_commits_first_release(self, get_jwt):
+        with self.feature("projects:auto-associate-commits-to-release"):
+            with self.tasks():
+                _ = self.make_release_event(LATER_COMMIT_SHA, self.project.id)
+
+            release2 = Release.objects.get(version=LATER_COMMIT_SHA)
+            commit_list = list(
+                Commit.objects.filter(releasecommit__release=release2).order_by(
+                    "releasecommit__order"
+                )
+            )
+
+            assert len(commit_list) == 2
+            assert commit_list[0].repository_id == self.repo.id
+            assert commit_list[0].organization_id == self.project.organization.id
+            assert commit_list[0].key == EARLIER_COMMIT_SHA
+            assert commit_list[1].repository_id == self.repo.id
+            assert commit_list[1].organization_id == self.project.organization.id
+            assert commit_list[1].key == LATER_COMMIT_SHA
+
+    @mock.patch("sentry.integrations.github.client.get_jwt", return_value=b"jwt_token_1")
+    @responses.activate
+    def test_autoassign_commits_not_a_sha(self, get_jwt):
+        SHA = "not-a-sha"
+        with self.feature("projects:auto-associate-commits-to-release"):
+            with self.tasks():
+                _ = self.make_release_event(SHA, self.project.id)
+
+            release2 = Release.objects.get(version=SHA)
+            commit_list = list(
+                Commit.objects.filter(releasecommit__release=release2).order_by(
+                    "releasecommit__order"
+                )
+            )
+            assert len(commit_list) == 0
+
+    @mock.patch("sentry.integrations.github.client.get_jwt", return_value=b"jwt_token_1")
+    @responses.activate
+    def test_autoassign_commit_not_found(self, get_jwt):
+        SHA = "b" * 40
+        responses.add(
+            "GET",
+            f"https://api.github.com/repos/{self.repo_name}/commits/{SHA}",
+            status=HTTP_404_NOT_FOUND,
+        )
+        with self.feature("projects:auto-associate-commits-to-release"):
+            with self.tasks():
+                _ = self.make_release_event(SHA, self.project.id)
+
+            release2 = Release.objects.get(version=SHA)
+            commit_list = list(
+                Commit.objects.filter(releasecommit__release=release2).order_by(
+                    "releasecommit__order"
+                )
+            )
+            assert len(commit_list) == 0
+
+    @mock.patch("sentry.integrations.github.client.get_jwt", return_value=b"jwt_token_1")
+    @responses.activate
+    def test_autoassign_commits_release_conflict(self, get_jwt):
+        # Release is created but none of the commits, we should still associate commits
+        with self.feature("projects:auto-associate-commits-to-release"):
+            preexisting_release = self.create_release(
+                project=self.project, version=LATER_COMMIT_SHA
+            )
+            with self.tasks():
+                _ = self.make_release_event(LATER_COMMIT_SHA, self.project.id)
+
+            commit_releases = Release.objects.filter(version=LATER_COMMIT_SHA).all()
+            assert len(commit_releases) == 1
+            assert commit_releases[0].id == preexisting_release.id
+            commit_list = list(
+                Commit.objects.filter(releasecommit__release=preexisting_release).order_by(
+                    "releasecommit__order"
+                )
+            )
+
+            assert len(commit_list) == 2
+            assert commit_list[0].repository_id == self.repo.id
+            assert commit_list[0].organization_id == self.project.organization.id
+            assert commit_list[0].key == EARLIER_COMMIT_SHA
+            assert commit_list[1].repository_id == self.repo.id
+            assert commit_list[1].organization_id == self.project.organization.id
+            assert commit_list[1].key == LATER_COMMIT_SHA
+
+    @mock.patch("sentry.integrations.github.client.get_jwt", return_value=b"jwt_token_1")
+    @responses.activate
+    def test_autoassign_commits_commit_conflict(self, get_jwt):
+        # A commit tied to the release is somehow created before the release itself is created.
+        # autoassociation should tie the existing commit to the new release
+        with self.feature("projects:auto-associate-commits-to-release"):
+
+            author = CommitAuthor.objects.create(
+                organization_id=self.organization.id,
+                email="support@github.com",
+                name="Monalisa Octocat",
+            )
+
+            # Values taken from commit generated from GH response fixtures
+            preexisting_commit = self.create_commit(
+                repo=self.repo,
+                project=self.project,
+                author=author,
+                key=EARLIER_COMMIT_SHA,
+                message="Fix all the bugs",
+                date_added=datetime(2011, 4, 14, 16, 0, 49, tzinfo=timezone.utc),
+            )
+
+            with self.tasks():
+                self.make_release_event(LATER_COMMIT_SHA, self.project.id)
+
+            new_release = Release.objects.get(version=LATER_COMMIT_SHA)
+            commit_list = list(
+                Commit.objects.filter(releasecommit__release=new_release).order_by(
+                    "releasecommit__order"
+                )
+            )
+
+            assert len(commit_list) == 2
+            assert commit_list[0].id == preexisting_commit.id
+            assert commit_list[0].repository_id == self.repo.id
+            assert commit_list[0].organization_id == self.project.organization.id
+            assert commit_list[0].key == EARLIER_COMMIT_SHA
+            assert commit_list[1].repository_id == self.repo.id
+            assert commit_list[1].organization_id == self.project.organization.id
+            assert commit_list[1].key == LATER_COMMIT_SHA
+
+    @mock.patch("sentry.integrations.github.client.get_jwt", return_value=b"jwt_token_1")
+    @responses.activate
+    def test_autoassign_commits_feature_not_enabled(self, get_jwt):
+        with self.feature({"projects:auto-associate-commits-to-release": False}):
+            with self.tasks():
+                _ = self.make_release_event(LATER_COMMIT_SHA, self.project.id)
+
+            release2 = Release.objects.get(version=LATER_COMMIT_SHA)
+            commit_list = list(
+                Commit.objects.filter(releasecommit__release=release2).order_by(
+                    "releasecommit__order"
+                )
+            )
+
+            assert len(commit_list) == 0
+
+    @mock.patch("sentry.integrations.github.client.get_jwt", return_value=b"jwt_token_1")
+    @responses.activate
+    def test_autoassign_commits_duplicate_events(self, get_jwt):
+        with self.feature({"projects:auto-associate-commits-to-release": True}):
+            with self.tasks():
+                event1 = self.make_release_event(LATER_COMMIT_SHA, self.project.id)
+                event2 = self.make_release_event(LATER_COMMIT_SHA, self.project.id)
+
+            assert event1 != event2
+            assert event1.release == event2.release
+            releases = Release.objects.filter(version=LATER_COMMIT_SHA).all()
+            assert len(releases) == 1
+            commit_list = list(
+                Commit.objects.filter(releasecommit__release=releases[0]).order_by(
+                    "releasecommit__order"
+                )
+            )
+
+            assert len(commit_list) == 2
+            assert commit_list[0].repository_id == self.repo.id
+            assert commit_list[0].organization_id == self.project.organization.id
+            assert commit_list[0].key == EARLIER_COMMIT_SHA
+            assert commit_list[1].repository_id == self.repo.id
+            assert commit_list[1].organization_id == self.project.organization.id
+            assert commit_list[1].key == LATER_COMMIT_SHA
+
+
 class ReleaseIssueTest(TestCase):
     def setUp(self):
         self.project = self.create_project()

+ 2 - 2
tests/sentry/integrations/github/test_repository.py

@@ -14,11 +14,11 @@ from sentry.testutils.asserts import assert_commit_shape
 from sentry.utils import json
 
 
-def stub_installation_token():
+def stub_installation_token(external_id=654321):
     ten_hours = datetime.datetime.utcnow() + datetime.timedelta(hours=10)
     responses.add(
         responses.POST,
-        "https://api.github.com/app/installations/654321/access_tokens",
+        f"https://api.github.com/app/installations/{external_id}/access_tokens",
         json={"token": "v1.install-token", "expires_at": ten_hours.strftime("%Y-%m-%dT%H:%M:%SZ")},
     )