diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index d71e6b4c004be871b0d33bb00610f476616b2d33..7ae00a70671edad4939b6492b0b928d103a28fac 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -70,6 +70,10 @@ def commit_url(entity, *args)
     project_commit_url(entity.project, entity.sha, *args)
   end
 
+  def release_url(entity, *args)
+    project_release_url(entity.project, entity, *args)
+  end
+
   def preview_markdown_path(parent, *args)
     return group_preview_markdown_path(parent, *args) if parent.is_a?(Group)
 
diff --git a/app/models/concerns/triggerable_hooks.rb b/app/models/concerns/triggerable_hooks.rb
index e01da3b3f361e0cd271c4859aeaf9bfc0053294f..325a5531926efe9d471989db048ce334a7114c2a 100644
--- a/app/models/concerns/triggerable_hooks.rb
+++ b/app/models/concerns/triggerable_hooks.rb
@@ -14,7 +14,8 @@ module TriggerableHooks
     pipeline_hooks:           :pipeline_events,
     wiki_page_hooks:          :wiki_page_events,
     deployment_hooks:         :deployment_events,
-    feature_flag_hooks:       :feature_flag_events
+    feature_flag_hooks:       :feature_flag_events,
+    release_hooks:            :releases_events
   }.freeze
 
   extend ActiveSupport::Concern
diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb
index fa3578cda18469ee142ad7a6ee0c674dda6ed87c..b625a70b4446d78dc3846862c9e79edb0b242a79 100644
--- a/app/models/hooks/project_hook.rb
+++ b/app/models/hooks/project_hook.rb
@@ -19,7 +19,8 @@ class ProjectHook < WebHook
     :pipeline_hooks,
     :wiki_page_hooks,
     :deployment_hooks,
-    :feature_flag_hooks
+    :feature_flag_hooks,
+    :release_hooks
   ]
 
   belongs_to :project
diff --git a/app/models/release.rb b/app/models/release.rb
index f2162a0f6742cd85d3dc573a809a4b847fef7f8b..c56df0a6aa349fd3d1df2c2eed4ed322d23b98d4 100644
--- a/app/models/release.rb
+++ b/app/models/release.rb
@@ -83,6 +83,15 @@ def milestone_titles
     self.milestones.map {|m| m.title }.sort.join(", ")
   end
 
+  def to_hook_data(action)
+    Gitlab::HookData::ReleaseBuilder.new(self).build(action)
+  end
+
+  def execute_hooks(action)
+    hook_data = to_hook_data(action)
+    project.execute_hooks(hook_data, :release_hooks)
+  end
+
   private
 
   def actual_sha
diff --git a/app/models/releases/link.rb b/app/models/releases/link.rb
index 82272f4857ad269ba10ee0ce34df9e65a218221c..fc2fa639f56d8679511a25ea88eb4fed8ab11620 100644
--- a/app/models/releases/link.rb
+++ b/app/models/releases/link.rb
@@ -30,5 +30,15 @@ def internal?
     def external?
       !internal?
     end
+
+    def hook_attrs
+      {
+        id: id,
+        external: external?,
+        link_type: link_type,
+        name: name,
+        url: url
+      }
+    end
   end
 end
diff --git a/app/models/releases/source.rb b/app/models/releases/source.rb
index 2f00d25d768a7a9b2857e7195359330b6bc9b151..44760541290840bdeec2c1039e86ea031c44e673 100644
--- a/app/models/releases/source.rb
+++ b/app/models/releases/source.rb
@@ -24,6 +24,13 @@ def url
                              format: format)
     end
 
+    def hook_attrs
+      {
+        format: format,
+        url: url
+      }
+    end
+
     private
 
     def archive_prefix
diff --git a/app/services/concerns/integrations/project_test_data.rb b/app/services/concerns/integrations/project_test_data.rb
index 4d5514303153e25eb1f2f8102a084631f25573e4..72c12cfb3944b47e99099e7e4b0fc1504fc8a770 100644
--- a/app/services/concerns/integrations/project_test_data.rb
+++ b/app/services/concerns/integrations/project_test_data.rb
@@ -58,5 +58,12 @@ def deployment_events_data
 
       Gitlab::DataBuilder::Deployment.build(deployment)
     end
+
+    def releases_events_data
+      release = project.releases.first
+      return { error: s_('TestHooks|Ensure the project has releases.') } unless release.present?
+
+      release.to_hook_data('create')
+    end
   end
 end
diff --git a/app/services/integrations/test/project_service.rb b/app/services/integrations/test/project_service.rb
index 941d70c2cc448738258af3a862be80aa902789e8..39471d373f9511453788c3e078d16093575532ba 100644
--- a/app/services/integrations/test/project_service.rb
+++ b/app/services/integrations/test/project_service.rb
@@ -35,6 +35,8 @@ def data
             wiki_page_events_data
           when 'deployment'
             deployment_events_data
+          when 'release'
+            releases_events_data
           else
             push_events_data
           end
diff --git a/app/services/releases/base_service.rb b/app/services/releases/base_service.rb
index 15d040287a3c563f3794117b3d21d8870a4937ea..38ef80ced5669d997b18026153eb2e595a912057 100644
--- a/app/services/releases/base_service.rb
+++ b/app/services/releases/base_service.rb
@@ -81,6 +81,10 @@ def param_for_milestone_titles_provided?
       params.key?(:milestones)
     end
 
+    def execute_hooks(release, action = 'create')
+      release.execute_hooks(action)
+    end
+
     # overridden in EE
     def project_group_id; end
   end
diff --git a/app/services/releases/create_service.rb b/app/services/releases/create_service.rb
index 887c2d355ee64096e5d25cf55a1094ae91af9bb0..deefe559d5dd994252944305d2558f6a2286dc8e 100644
--- a/app/services/releases/create_service.rb
+++ b/app/services/releases/create_service.rb
@@ -52,6 +52,8 @@ def create_release(tag, evidence_pipeline)
 
       notify_create_release(release)
 
+      execute_hooks(release, 'create')
+
       create_evidence!(release, evidence_pipeline)
 
       success(tag: tag, release: release)
diff --git a/app/services/releases/update_service.rb b/app/services/releases/update_service.rb
index 4786d35f31e5a4094b37108cb33a36260fb47a51..4e78120ac05debd23e0e75272eb65d69e40fbf6c 100644
--- a/app/services/releases/update_service.rb
+++ b/app/services/releases/update_service.rb
@@ -3,11 +3,9 @@
 module Releases
   class UpdateService < Releases::BaseService
     def execute
-      return error('Tag does not exist', 404) unless existing_tag
-      return error('Release does not exist', 404) unless release
-      return error('Access Denied', 403) unless allowed?
-      return error('params is empty', 400) if empty_params?
-      return error("Milestone(s) not found: #{inexistent_milestones.join(', ')}", 400) if inexistent_milestones.any?
+      if error = validate
+        return error
+      end
 
       if param_for_milestone_titles_provided?
         previous_milestones = release.milestones.map(&:title)
@@ -20,6 +18,7 @@ def execute
       # see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/43385
       ActiveRecord::Base.transaction do
         if release.update(params)
+          execute_hooks(release, 'update')
           success(tag: existing_tag, release: release, milestones_updated: milestones_updated?(previous_milestones))
         else
           error(release.errors.messages || '400 Bad request', 400)
@@ -31,6 +30,14 @@ def execute
 
     private
 
+    def validate
+      return error('Tag does not exist', 404) unless existing_tag
+      return error('Release does not exist', 404) unless release
+      return error('Access Denied', 403) unless allowed?
+      return error('params is empty', 400) if empty_params?
+      return error("Milestone(s) not found: #{inexistent_milestones.join(', ')}", 400) if inexistent_milestones.any?
+    end
+
     def allowed?
       Ability.allowed?(current_user, :update_release, release)
     end
diff --git a/app/services/test_hooks/project_service.rb b/app/services/test_hooks/project_service.rb
index 4e554dce357af51bb3a8ef1034ed74e9ef227ee5..dcd92ac2b8c6ad3d2acbbf200e3e3b43963c7766 100644
--- a/app/services/test_hooks/project_service.rb
+++ b/app/services/test_hooks/project_service.rb
@@ -30,6 +30,8 @@ def data
           pipeline_events_data
         when 'wiki_page_events'
           wiki_page_events_data
+        when 'releases_events'
+          releases_events_data
         end
       end
     end
diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml
index e9ce443782aef868879566d49eb8b8edafacc7f2..c5234f1409091f617a868398535f69125cf1bac6 100644
--- a/app/views/shared/web_hooks/_form.html.haml
+++ b/app/views/shared/web_hooks/_form.html.haml
@@ -84,6 +84,12 @@
         %strong= s_('Webhooks|Feature Flag events')
       %p.text-muted.ml-1
         = s_('Webhooks|This URL is triggered when a feature flag is turned on or off')
+    %li
+      = form.check_box :releases_events, class: 'form-check-input'
+      = form.label :releases_events, class: 'list-label form-check-label ml-1' do
+        %strong= s_('Webhooks|Releases events')
+      %p.text-muted.ml-1
+        = s_('Webhooks|This URL is triggered when a release is created/updated')
 .form-group
   = form.label :enable_ssl_verification, s_('Webhooks|SSL verification'), class: 'label-bold checkbox'
   .form-check
diff --git a/changelogs/unreleased/26873-add-webhooks-for-releases.yml b/changelogs/unreleased/26873-add-webhooks-for-releases.yml
new file mode 100644
index 0000000000000000000000000000000000000000..9ccb93b66bc9af5673625a4b3a9b184b8c021c86
--- /dev/null
+++ b/changelogs/unreleased/26873-add-webhooks-for-releases.yml
@@ -0,0 +1,5 @@
+---
+title: Add webhooks for creating and updating a release
+merge_request: 44881
+author: David Barr @davebarr
+type: added
diff --git a/db/migrate/20201008075620_add_releases_events_to_web_hooks.rb b/db/migrate/20201008075620_add_releases_events_to_web_hooks.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f6df213f1622c4b4052165f5a838f62590bc90cf
--- /dev/null
+++ b/db/migrate/20201008075620_add_releases_events_to_web_hooks.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddReleasesEventsToWebHooks < ActiveRecord::Migration[6.0]
+  DOWNTIME = false
+
+  def change
+    add_column :web_hooks, :releases_events, :boolean, null: false, default: false
+  end
+end
diff --git a/db/schema_migrations/20201008075620 b/db/schema_migrations/20201008075620
new file mode 100644
index 0000000000000000000000000000000000000000..a0a5d6a73142916c5f71e8ef33bb55e26e27d2f7
--- /dev/null
+++ b/db/schema_migrations/20201008075620
@@ -0,0 +1 @@
+f9bc943b61460b1a9a6db8189ab5b21eba46e14650c68658175299b14d48a030
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index a4fe8fb0f573196c2325c0a2ebd918b7858367cd..7cc3e1487d08557bdb38565d85cd3b5180114082 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -17364,6 +17364,7 @@ CREATE TABLE web_hooks (
     encrypted_url character varying,
     encrypted_url_iv character varying,
     deployment_events boolean DEFAULT false NOT NULL,
+    releases_events boolean DEFAULT false NOT NULL,
     feature_flag_events boolean DEFAULT false NOT NULL
 );
 
diff --git a/doc/api/groups.md b/doc/api/groups.md
index 312688202f63b7b4ddc80a43b4ce13bad09f7015..34b1c0f6d82d9b345a4019c17554f90c1429744e 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -1043,6 +1043,7 @@ GET /groups/:id/hooks/:hook_id
   "pipeline_events": true,
   "wiki_page_events": true,
   "deployment_events": true,
+  "releases_events": true,
   "enable_ssl_verification": true,
   "created_at": "2012-10-12T17:04:47Z"
 }
@@ -1071,6 +1072,7 @@ POST /groups/:id/hooks
 | `pipeline_events`            | boolean        | no       | Trigger hook on pipeline events |
 | `wiki_page_events`           | boolean        | no       | Trigger hook on wiki events |
 | `deployment_events`          | boolean        | no       | Trigger hook on deployment events |
+| `releases_events`            | boolean        | no       | Trigger hook on release events |
 | `enable_ssl_verification`    | boolean        | no       | Do SSL verification when triggering the hook |
 | `token`                      | string         | no       | Secret token to validate received payloads; this will not be returned in the response |
 
@@ -1098,6 +1100,7 @@ PUT /groups/:id/hooks/:hook_id
 | `pipeline_events`            | boolean        | no       | Trigger hook on pipeline events |
 | `wiki_events`                | boolean        | no       | Trigger hook on wiki events |
 | `deployment_events`          | boolean        | no       | Trigger hook on deployment events |
+| `releases_events`            | boolean        | no       | Trigger hook on release events |
 | `enable_ssl_verification`    | boolean        | no       | Do SSL verification when triggering the hook |
 | `token`                      | string         | no       | Secret token to validate received payloads; this will not be returned in the response |
 
diff --git a/doc/api/projects.md b/doc/api/projects.md
index cafad8b586bd3febf542ead18055d6fd794eb715..9c83d4adb1112f259be5edc769989e69a3e252ca 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -2006,6 +2006,7 @@ GET /projects/:id/hooks/:hook_id
   "pipeline_events": true,
   "wiki_page_events": true,
   "deployment_events": true,
+  "releases_events": true,
   "enable_ssl_verification": true,
   "created_at": "2012-10-12T17:04:47Z"
 }
@@ -2065,6 +2066,7 @@ PUT /projects/:id/hooks/:hook_id
 | `token`                      | string         | **{dotted-circle}** No | Secret token to validate received payloads; this isn't returned in the response. |
 | `url`                        | string         | **{check-circle}** Yes | The hook URL. |
 | `wiki_events`                | boolean        | **{dotted-circle}** No | Trigger hook on wiki events. |
+| `releases_events`            | boolean        | **{dotted-circle}** No | Trigger hook on release events. |
 
 ### Delete project hook
 
diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
index 94ed45d053e2edeff1ab886abaee41ff300c063a..6a436c5093eff5475a97e927eba1d2ff6bf4f752 100644
--- a/doc/user/project/integrations/webhooks.md
+++ b/doc/user/project/integrations/webhooks.md
@@ -1407,6 +1407,91 @@ X-Gitlab-Event: Feature Flag Hook
 }
 ```
 
+### Release events
+
+Triggered when a release is created or updated.
+
+**Request Header**:
+
+```plaintext
+X-Gitlab-Event: Release Hook
+```
+
+**Request Body**:
+
+```json
+{
+  "id": 1,
+  "created_at": "2020-11-02 12:55:12 UTC",
+  "description": "v1.0 has been released",
+  "name": "v1.1",
+  "released_at": "2020-11-02 12:55:12 UTC",
+  "tag": "v1.1",
+  "object_kind": "release",
+  "project": {
+    "id": 2,
+    "name": "release-webhook-example",
+    "description": "",
+    "web_url": "https://example.com/gitlab-org/release-webhook-example",
+    "avatar_url": null,
+    "git_ssh_url": "ssh://git@example.com/gitlab-org/release-webhook-example.git",
+    "git_http_url": "https://example.com/gitlab-org/release-webhook-example.git",
+    "namespace": "Gitlab",
+    "visibility_level": 0,
+    "path_with_namespace": "gitlab-org/release-webhook-example",
+    "default_branch": "master",
+    "ci_config_path": null,
+    "homepage": "https://example.com/gitlab-org/release-webhook-example",
+    "url": "ssh://git@example.com/gitlab-org/release-webhook-example.git",
+    "ssh_url": "ssh://git@example.com/gitlab-org/release-webhook-example.git",
+    "http_url": "https://example.com/gitlab-org/release-webhook-example.git"
+  },
+  "url": "https://example.com/gitlab-org/release-webhook-example/-/releases/v1.1",
+  "action": "create",
+  "assets": {
+    "count": 5,
+    "links": [
+      {
+        "id": 1,
+        "external": true,
+        "link_type": "other",
+        "name": "Changelog",
+        "url": "https://example.net/changelog"
+      }
+    ],
+    "sources": [
+      {
+        "format": "zip",
+        "url": "https://example.com/gitlab-org/release-webhook-example/-/archive/v1.1/release-webhook-example-v1.1.zip"
+      },
+      {
+        "format": "tar.gz",
+        "url": "https://example.com/gitlab-org/release-webhook-example/-/archive/v1.1/release-webhook-example-v1.1.tar.gz"
+      },
+      {
+        "format": "tar.bz2",
+        "url": "https://example.com/gitlab-org/release-webhook-example/-/archive/v1.1/release-webhook-example-v1.1.tar.bz2"
+      },
+      {
+        "format": "tar",
+        "url": "https://example.com/gitlab-org/release-webhook-example/-/archive/v1.1/release-webhook-example-v1.1.tar"
+      }
+    ]
+  },
+  "commit": {
+    "id": "ee0a3fb31ac16e11b9dbb596ad16d4af654d08f8",
+    "message": "Release v1.1",
+    "title": "Release v1.1",
+    "timestamp": "2020-10-31T14:58:32+11:00",
+    "url": "https://example.com/gitlab-org/release-webhook-example/-/commit/ee0a3fb31ac16e11b9dbb596ad16d4af654d08f8",
+    "author": {
+      "name": "Example User",
+      "email": "user@example.com"
+    }
+  }
+}
+```
+
 ## Image URL rewriting
 
 From GitLab 11.2, simple image references are rewritten to use an absolute URL
diff --git a/ee/app/models/hooks/group_hook.rb b/ee/app/models/hooks/group_hook.rb
index 2a5a5a2b8e5120360c8bfec06e5ee66a588a7f08..700a8ee156474620688b7e9a1557c061b6d1b850 100644
--- a/ee/app/models/hooks/group_hook.rb
+++ b/ee/app/models/hooks/group_hook.rb
@@ -19,7 +19,8 @@ class GroupHook < WebHook
     :job_hooks,
     :pipeline_hooks,
     :wiki_page_hooks,
-    :deployment_hooks
+    :deployment_hooks,
+    :release_hooks
   ]
 
   belongs_to :group
diff --git a/ee/lib/api/group_hooks.rb b/ee/lib/api/group_hooks.rb
index 33dc49e45d85a1a90a9e6e8ca2093ebabd6fa829..4d033465b89c185d555bd3258bf865c213c276b1 100644
--- a/ee/lib/api/group_hooks.rb
+++ b/ee/lib/api/group_hooks.rb
@@ -23,6 +23,7 @@ class GroupHooks < ::API::Base
         optional :pipeline_events, type: Boolean, desc: "Trigger hook on pipeline events"
         optional :wiki_page_events, type: Boolean, desc: "Trigger hook on wiki events"
         optional :deployment_events, type: Boolean, desc: "Trigger hook on deployment events"
+        optional :releases_events, type: Boolean, desc: "Trigger hook on release events"
         optional :enable_ssl_verification, type: Boolean, desc: "Do SSL verification when triggering the hook"
         optional :token, type: String, desc: "Secret token to validate received payloads; this will not be returned in the response"
       end
diff --git a/ee/lib/ee/api/entities/group_hook.rb b/ee/lib/ee/api/entities/group_hook.rb
index 19d17c0a3620e7e8592751ca031595d72986fd51..7c2cdadca3d2e0c7377bf08408235ba6e55f95cc 100644
--- a/ee/lib/ee/api/entities/group_hook.rb
+++ b/ee/lib/ee/api/entities/group_hook.rb
@@ -6,7 +6,7 @@ module Entities
       class GroupHook < ::API::Entities::Hook
         expose :group_id, :issues_events, :confidential_issues_events,
                :note_events, :confidential_note_events, :pipeline_events, :wiki_page_events,
-               :job_events, :deployment_events
+               :job_events, :deployment_events, :releases_events
       end
     end
   end
diff --git a/ee/spec/controllers/groups/hooks_controller_spec.rb b/ee/spec/controllers/groups/hooks_controller_spec.rb
index 74f04b42d17e51dd06e96c2e6a2e2c37b26d71ee..dce160bb1bbc7f44e62b25628653e006267869bd 100644
--- a/ee/spec/controllers/groups/hooks_controller_spec.rb
+++ b/ee/spec/controllers/groups/hooks_controller_spec.rb
@@ -80,7 +80,8 @@
             token: 'TEST TOKEN',
             url: 'http://example.com',
             wiki_page_events: true,
-            deployment_events: true
+            deployment_events: true,
+            releases_events: true
           }
         end
 
diff --git a/ee/spec/factories/group_hooks.rb b/ee/spec/factories/group_hooks.rb
index 7fabe86a7d1976ec59839a6a2af33ff61b2b5359..f55edf6880ad88a4356049ba6a902d7f7ddd0204 100644
--- a/ee/spec/factories/group_hooks.rb
+++ b/ee/spec/factories/group_hooks.rb
@@ -16,6 +16,7 @@
       job_events { true }
       pipeline_events { true }
       wiki_page_events { true }
+      releases_events { true }
     end
   end
 end
diff --git a/ee/spec/fixtures/api/schemas/public_api/v4/group_hook.json b/ee/spec/fixtures/api/schemas/public_api/v4/group_hook.json
index 199a2820cad2da87ad68e04bb55fb8f06965d8cc..f9881ae6cc1fd58c42bd005aa7f297106c9fac3e 100644
--- a/ee/spec/fixtures/api/schemas/public_api/v4/group_hook.json
+++ b/ee/spec/fixtures/api/schemas/public_api/v4/group_hook.json
@@ -17,7 +17,8 @@
     "pipeline_events",
     "wiki_page_events",
     "job_events",
-    "deployment_events"
+    "deployment_events",
+    "releases_events"
   ],
   "properties": {
     "id": { "type": "integer" },
@@ -36,7 +37,8 @@
     "pipeline_events": { "type": "boolean" },
     "wiki_page_events": { "type": "boolean" },
     "job_events": { "type": "boolean" },
-    "deployment_events": { "type": "boolean" }
+    "deployment_events": { "type": "boolean" },
+    "releases_events": { "type": "boolean" }
   },
   "additionalProperties": false
 }
diff --git a/ee/spec/requests/api/group_hooks_spec.rb b/ee/spec/requests/api/group_hooks_spec.rb
index b186d7dabb25837368e162a9d7515de574b5c228..5e7dd7c9316378676ceae6f72bbd10d17e241b51 100644
--- a/ee/spec/requests/api/group_hooks_spec.rb
+++ b/ee/spec/requests/api/group_hooks_spec.rb
@@ -119,7 +119,8 @@ def make_delete_group_hook_request(group_id, hook_id, user)
         job_events: true,
         pipeline_events: true,
         wiki_page_events: true,
-        deployment_events: true
+        deployment_events: true,
+        releases_events: true
       }
     end
 
@@ -144,6 +145,7 @@ def make_delete_group_hook_request(group_id, hook_id, user)
         expect(json_response['pipeline_events']).to eq(true)
         expect(json_response['wiki_page_events']).to eq(true)
         expect(json_response['deployment_events']).to eq(true)
+        expect(json_response['releases_events']).to eq(true)
         expect(json_response['enable_ssl_verification']).to eq(true)
       end
 
diff --git a/lib/api/entities/project_hook.rb b/lib/api/entities/project_hook.rb
index 751f95002527d5829f9be0d24d21bae366c81b28..6c71e5d317c67d983877aacc55b21443aefc60d9 100644
--- a/lib/api/entities/project_hook.rb
+++ b/lib/api/entities/project_hook.rb
@@ -5,7 +5,7 @@ module Entities
     class ProjectHook < Hook
       expose :project_id, :issues_events, :confidential_issues_events
       expose :note_events, :confidential_note_events, :pipeline_events, :wiki_page_events, :deployment_events
-      expose :job_events
+      expose :job_events, :releases_events
       expose :push_events_branch_filter
     end
   end
diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb
index 91bcc7e0257344a890d736137307bf13d849b97b..431ba199131bacc322adebf1e0e02742f1288ed0 100644
--- a/lib/api/project_hooks.rb
+++ b/lib/api/project_hooks.rb
@@ -23,6 +23,7 @@ class ProjectHooks < ::API::Base
         optional :pipeline_events, type: Boolean, desc: "Trigger hook on pipeline events"
         optional :wiki_page_events, type: Boolean, desc: "Trigger hook on wiki events"
         optional :deployment_events, type: Boolean, desc: "Trigger hook on deployment events"
+        optional :releases_events, type: Boolean, desc: "Trigger hook on release events"
         optional :enable_ssl_verification, type: Boolean, desc: "Do SSL verification when triggering the hook"
         optional :token, type: String, desc: "Secret token to validate received payloads; this will not be returned in the response"
         optional :push_events_branch_filter, type: String, desc: "Trigger hook on specified branch only"
diff --git a/lib/gitlab/hook_data/release_builder.rb b/lib/gitlab/hook_data/release_builder.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b15c260f4a85904ceb1775b1e891fa36b4926039
--- /dev/null
+++ b/lib/gitlab/hook_data/release_builder.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module HookData
+    class ReleaseBuilder < BaseBuilder
+      def self.safe_hook_attributes
+        %i[
+          id
+          created_at
+          description
+          name
+          released_at
+          tag
+        ].freeze
+      end
+
+      alias_method :release, :object
+
+      def build(action)
+        attrs = {
+          object_kind: object_kind,
+          project: release.project.hook_attrs,
+          description: absolute_image_urls(release.description),
+          url: Gitlab::UrlBuilder.build(release),
+          action: action,
+          assets: {
+              count: release.assets_count,
+              links: release.links.map(&:hook_attrs),
+              sources: release.sources.map(&:hook_attrs)
+          },
+          commit: release.commit.hook_attrs
+        }
+
+        release.attributes.with_indifferent_access.slice(*self.class.safe_hook_attributes)
+          .merge!(attrs)
+      end
+
+      private
+
+      def object_kind
+        release.class.name.underscore
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb
index 1e522ae63b6568d8365252216224e0287d2684f5..ce59e10241e631904c0e43cc8267af3f17c211e8 100644
--- a/lib/gitlab/url_builder.rb
+++ b/lib/gitlab/url_builder.rb
@@ -32,6 +32,8 @@ def build(object, **options)
           instance.milestone_url(object, **options)
         when Note
           note_url(object, **options)
+        when Release
+          instance.release_url(object, **options)
         when Project
           instance.project_url(object, **options)
         when Snippet
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index ad354bd2cfda92189353a60be54da22a298496bb..3e0fc4509b1a5d98a8161ff38abf8238152e6d97 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -26495,6 +26495,9 @@ msgstr ""
 msgid "TestHooks|Ensure the project has notes."
 msgstr ""
 
+msgid "TestHooks|Ensure the project has releases."
+msgstr ""
+
 msgid "TestHooks|Ensure the wiki is enabled and has pages."
 msgstr ""
 
@@ -30125,6 +30128,9 @@ msgstr ""
 msgid "Webhooks|Push events"
 msgstr ""
 
+msgid "Webhooks|Releases events"
+msgstr ""
+
 msgid "Webhooks|SSL verification"
 msgstr ""
 
@@ -30140,6 +30146,9 @@ msgstr ""
 msgid "Webhooks|This URL is triggered when a feature flag is turned on or off"
 msgstr ""
 
+msgid "Webhooks|This URL is triggered when a release is created/updated"
+msgstr ""
+
 msgid "Webhooks|This URL will be triggered by a push to the repository"
 msgstr ""
 
diff --git a/spec/factories/project_hooks.rb b/spec/factories/project_hooks.rb
index 88b5ff936fe7470c48999c1880706bc16b57096f..88c06b3857a9ba35f88459a97288e1d26b9fe9fb 100644
--- a/spec/factories/project_hooks.rb
+++ b/spec/factories/project_hooks.rb
@@ -23,6 +23,7 @@
       wiki_page_events { true }
       deployment_events { true }
       feature_flag_events { true }
+      releases_events { true }
     end
   end
 end
diff --git a/spec/features/projects/settings/webhooks_settings_spec.rb b/spec/features/projects/settings/webhooks_settings_spec.rb
index d184f08bd892320f15f14de2835ea340934b5434..528fd58cbe6a8956f21104c01f3459955606a446 100644
--- a/spec/features/projects/settings/webhooks_settings_spec.rb
+++ b/spec/features/projects/settings/webhooks_settings_spec.rb
@@ -45,6 +45,7 @@
         expect(page).to have_content('Merge requests events')
         expect(page).to have_content('Pipeline events')
         expect(page).to have_content('Wiki page events')
+        expect(page).to have_content('Releases events')
       end
 
       it 'create webhook' do
diff --git a/spec/helpers/gitlab_routing_helper_spec.rb b/spec/helpers/gitlab_routing_helper_spec.rb
index 0088f739879688c8d12d7f4d70922bc2d48c6737..f23ffcee35dff205ee1b724d763fd516a23ede5f 100644
--- a/spec/helpers/gitlab_routing_helper_spec.rb
+++ b/spec/helpers/gitlab_routing_helper_spec.rb
@@ -322,4 +322,14 @@
       end
     end
   end
+
+  context 'releases' do
+    let(:release) { create(:release) }
+
+    describe '#release_url' do
+      it 'returns the url for the release page' do
+        expect(release_url(release)).to eq("http://test.host/#{release.project.full_path}/-/releases/#{release.tag}")
+      end
+    end
+  end
 end
diff --git a/spec/lib/gitlab/hook_data/release_builder_spec.rb b/spec/lib/gitlab/hook_data/release_builder_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b630780b162354106e0d1e291fc13f8cb991a853
--- /dev/null
+++ b/spec/lib/gitlab/hook_data/release_builder_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::HookData::ReleaseBuilder do
+  let_it_be(:project) { create(:project, :public, :repository) }
+  let(:release) { create(:release, project: project) }
+  let(:builder) { described_class.new(release) }
+
+  describe '#build' do
+    let(:data) { builder.build('create') }
+
+    it 'includes safe attribute' do
+      %w[
+          id
+          created_at
+          description
+          name
+          released_at
+          tag
+      ].each do |key|
+        expect(data).to include(key)
+      end
+    end
+
+    it 'includes additional attrs' do
+      expect(data[:object_kind]).to eq('release')
+      expect(data[:project]).to eq(builder.release.project.hook_attrs.with_indifferent_access)
+      expect(data[:action]).to eq('create')
+      expect(data).to include(:assets)
+      expect(data).to include(:commit)
+    end
+
+    context 'when the Release has an image in the description' do
+      let(:release_with_description) do
+        create(:release, project: project, description: 'test![Release_Image](/uploads/abc/Release_Image.png)')
+      end
+
+      let(:builder) { described_class.new(release_with_description) }
+
+      it 'sets the image to use an absolute URL' do
+        expected_path = "#{release_with_description.project.path_with_namespace}/uploads/abc/Release_Image.png"
+
+        expect(data[:description])
+          .to eq("test![Release_Image](#{Settings.gitlab.url}/#{expected_path})")
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/import_export/project/relation_factory_spec.rb b/spec/lib/gitlab/import_export/project/relation_factory_spec.rb
index 50bc6a30044ae78bf68ebb43bf83004bcc6cf835..56ba730e893fb491715ab62ef970f11b2b478874 100644
--- a/spec/lib/gitlab/import_export/project/relation_factory_spec.rb
+++ b/spec/lib/gitlab/import_export/project/relation_factory_spec.rb
@@ -61,6 +61,7 @@ def values
         'enable_ssl_verification' => true,
         'job_events' => false,
         'wiki_page_events' => true,
+        'releases_events' => false,
         'token' => token
       }
     end
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 7dee8c26850cfd7859bfc06222ce245b343a40b0..8b254e82a926056018432ed37df3d995bdd7ee03 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -489,6 +489,7 @@ ProjectHook:
 - confidential_issues_events
 - confidential_note_events
 - repository_update_events
+- releases_events
 ProtectedBranch:
 - id
 - project_id
diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb
index b58b5a8466230200ef0eaf9e409d6159d4932484..c892f1f041069ab384335fc9cc95135b30c8fadf 100644
--- a/spec/lib/gitlab/url_builder_spec.rb
+++ b/spec/lib/gitlab/url_builder_spec.rb
@@ -24,6 +24,7 @@
       :project_milestone | ->(milestone)     { "/#{milestone.project.full_path}/-/milestones/#{milestone.iid}" }
       :project_snippet   | ->(snippet)       { "/#{snippet.project.full_path}/-/snippets/#{snippet.id}" }
       :project_wiki      | ->(wiki)          { "/#{wiki.container.full_path}/-/wikis/home" }
+      :release           | ->(release)       { "/#{release.project.full_path}/-/releases/#{release.tag}" }
       :ci_build          | ->(build)         { "/#{build.project.full_path}/-/jobs/#{build.id}" }
       :design            | ->(design)        { "/#{design.project.full_path}/-/design_management/designs/#{design.id}/raw_image" }
 
diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb
index 3b2a7895630591a764afdc7928551cdc61aa73fb..b5aedde2b2e1cf09b7490a8f618a09eb96c051a8 100644
--- a/spec/requests/api/project_hooks_spec.rb
+++ b/spec/requests/api/project_hooks_spec.rb
@@ -41,6 +41,7 @@
         expect(json_response.first['pipeline_events']).to eq(true)
         expect(json_response.first['wiki_page_events']).to eq(true)
         expect(json_response.first['deployment_events']).to eq(true)
+        expect(json_response.first['releases_events']).to eq(true)
         expect(json_response.first['enable_ssl_verification']).to eq(true)
         expect(json_response.first['push_events_branch_filter']).to eq('master')
       end
@@ -72,6 +73,7 @@
         expect(json_response['job_events']).to eq(hook.job_events)
         expect(json_response['pipeline_events']).to eq(hook.pipeline_events)
         expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events)
+        expect(json_response['releases_events']).to eq(hook.releases_events)
         expect(json_response['deployment_events']).to eq(true)
         expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification)
       end
@@ -97,7 +99,7 @@
         post(api("/projects/#{project.id}/hooks", user),
              params: { url: "http://example.com", issues_events: true,
                        confidential_issues_events: true, wiki_page_events: true,
-                       job_events: true, deployment_events: true,
+                       job_events: true, deployment_events: true, releases_events: true,
                        push_events_branch_filter: 'some-feature-branch' })
       end.to change {project.hooks.count}.by(1)
 
@@ -114,6 +116,7 @@
       expect(json_response['pipeline_events']).to eq(false)
       expect(json_response['wiki_page_events']).to eq(true)
       expect(json_response['deployment_events']).to eq(true)
+      expect(json_response['releases_events']).to eq(true)
       expect(json_response['enable_ssl_verification']).to eq(true)
       expect(json_response['push_events_branch_filter']).to eq('some-feature-branch')
       expect(json_response).not_to include('token')
@@ -169,6 +172,7 @@
       expect(json_response['job_events']).to eq(hook.job_events)
       expect(json_response['pipeline_events']).to eq(hook.pipeline_events)
       expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events)
+      expect(json_response['releases_events']).to eq(hook.releases_events)
       expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification)
     end
 
diff --git a/spec/services/releases/create_service_spec.rb b/spec/services/releases/create_service_spec.rb
index 90648340b66aba2d4343d5b6169b1833f8a41f10..b9294182883446fb99bb86088932aff8340db8c4 100644
--- a/spec/services/releases/create_service_spec.rb
+++ b/spec/services/releases/create_service_spec.rb
@@ -22,6 +22,12 @@
       it 'creates a new release' do
         expected_job_count = MailScheduler::NotificationServiceWorker.jobs.size + 1
 
+        expect_next_instance_of(Release) do |release|
+          expect(release)
+            .to receive(:execute_hooks)
+            .with('create')
+        end
+
         result = service.execute
 
         expect(project.releases.count).to eq(1)
diff --git a/spec/services/releases/update_service_spec.rb b/spec/services/releases/update_service_spec.rb
index 00544b820cbf42e9cf1547584548ed44c1ab835e..932a7fab5ecb1c73850507513094fc10447f9583 100644
--- a/spec/services/releases/update_service_spec.rb
+++ b/spec/services/releases/update_service_spec.rb
@@ -32,6 +32,12 @@
       expect(result[:release].description).to eq(new_description)
     end
 
+    it 'executes hooks' do
+      expect(service.release).to receive(:execute_hooks).with('update')
+
+      service.execute
+    end
+
     context 'when the tag does not exists' do
       let(:tag_name) { 'foobar' }
 
diff --git a/spec/services/test_hooks/project_service_spec.rb b/spec/services/test_hooks/project_service_spec.rb
index e4cc3a2d65269e29e121baf309f421e6865f8e32..7470bdff52715d0488353d58ef8bc96a6826ccf9 100644
--- a/spec/services/test_hooks/project_service_spec.rb
+++ b/spec/services/test_hooks/project_service_spec.rb
@@ -186,5 +186,23 @@
         expect(service.execute).to include(success_result)
       end
     end
+
+    context 'releases_events' do
+      let(:trigger) { 'releases_events' }
+      let(:trigger_key) { :release_hooks }
+
+      it 'returns error message if not enough data' do
+        expect(hook).not_to receive(:execute)
+        expect(service.execute).to include({ status: :error, message: 'Ensure the project has releases.' })
+      end
+
+      it 'executes hook' do
+        allow(project).to receive(:releases).and_return([Release.new])
+        allow_any_instance_of(Release).to receive(:to_hook_data).and_return(sample_data)
+
+        expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
+        expect(service.execute).to include(success_result)
+      end
+    end
   end
 end