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') + 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") + 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