diff --git a/app/finders/README.md b/app/finders/README.md
index a5a6fd8fe74070f1767f08a9ebd6f79159fb83d7..52f7378c4844a5e6979f4b7932333aa68435de45 100644
--- a/app/finders/README.md
+++ b/app/finders/README.md
@@ -1,10 +1,11 @@
 # Finders
 
-This type of classes responsible for collection items based on different conditions.
-To prevent lookup methods in models like this:
+These types of classes are responsible for retrieving collection items based on different conditions.
+They prevent lookup methods in models like this:
+
 
 ```ruby
-class Project
+class Project < ApplicationRecord
   def issues_for_user_filtered_by(user, filter)
     # A lot of logic not related to project model itself
   end
@@ -13,7 +14,7 @@ end
 issues = project.issues_for_user_filtered_by(user, params)
 ```
 
-Better use this:
+The GitLab approach is to use a Finder:
 
 ```ruby
 issues = IssuesFinder.new(project, user, filter).execute
diff --git a/lib/api/releases.rb b/lib/api/releases.rb
index 3c38721129f125e3485c931bfd20c32d2a8d7e8e..d648046eb4eb1e0f410847ecec758f918420d9f5 100644
--- a/lib/api/releases.rb
+++ b/lib/api/releases.rb
@@ -152,7 +152,7 @@ def authorize_download_code!
       end
 
       def authorize_create_evidence!
-        # This is a separate method so that EE can extend its behaviour
+        # extended in EE
       end
 
       def release
@@ -160,15 +160,15 @@ def release
       end
 
       def log_release_created_audit_event(release)
-        # This is a separate method so that EE can extend its behaviour
+        # extended in EE
       end
 
       def log_release_updated_audit_event
-        # This is a separate method so that EE can extend its behaviour
+        # extended in EE
       end
 
       def log_release_milestones_updated_audit_event
-        # This is a separate method so that EE can extend its behaviour
+        # extended in EE
       end
     end
   end
diff --git a/spec/requests/api/releases_spec.rb b/spec/requests/api/releases_spec.rb
index 01ead9eef54fcde8b52c29fec85e71e15cc1bd3d..8ae4b7e918544c4656e4b2cf4761300a01793306 100644
--- a/spec/requests/api/releases_spec.rb
+++ b/spec/requests/api/releases_spec.rb
@@ -598,7 +598,7 @@
           end
         end
 
-        context 'when create two assets' do
+        context 'when creating two assets' do
           let(:params) do
             base_params.merge({
               assets: {
@@ -758,6 +758,65 @@
         expect(response).to have_gitlab_http_status(:conflict)
       end
     end
+
+    context 'with milestones' do
+      let(:subject) { post api("/projects/#{project.id}/releases", maintainer), params: params }
+      let(:milestone) { create(:milestone, project: project, title: 'v1.0') }
+      let(:returned_milestones) { json_response['milestones'].map {|m| m['title']} }
+
+      before do
+        params.merge!(milestone_params)
+
+        subject
+      end
+
+      context 'with a project milestone' do
+        let(:milestone_params) { { milestones: [milestone.title] } }
+
+        it 'adds the milestone' do
+          expect(response).to have_gitlab_http_status(:created)
+          expect(returned_milestones).to match_array(['v1.0'])
+        end
+      end
+
+      context 'with multiple milestones' do
+        let(:milestone2) { create(:milestone, project: project, title: 'm2') }
+        let(:milestone_params) { { milestones: [milestone.title, milestone2.title] } }
+
+        it 'adds all milestones' do
+          expect(response).to have_gitlab_http_status(:created)
+          expect(returned_milestones).to match_array(['v1.0', 'm2'])
+        end
+      end
+
+      context 'with an empty milestone' do
+        let(:milestone_params) { { milestones: [] } }
+
+        it 'removes all milestones' do
+          expect(response).to have_gitlab_http_status(:created)
+          expect(json_response['milestones']).to be_nil
+        end
+      end
+
+      context 'with a non-existant milestone' do
+        let(:milestone_params) { { milestones: ['xyz'] } }
+
+        it 'returns a 400 error as milestone not found' do
+          expect(response).to have_gitlab_http_status(:bad_request)
+          expect(json_response['message']).to eq("Milestone(s) not found: xyz")
+        end
+      end
+
+      context 'with a milestone from a different project' do
+        let(:milestone) { create(:milestone, title: 'v1.0') }
+        let(:milestone_params) { { milestones: [milestone.title] } }
+
+        it 'returns a 400 error as milestone not found' do
+          expect(response).to have_gitlab_http_status(:bad_request)
+          expect(json_response['message']).to eq("Milestone(s) not found: v1.0")
+        end
+      end
+    end
   end
 
   describe 'PUT /projects/:id/releases/:tag_name' do
@@ -863,6 +922,83 @@
         end
       end
     end
+
+    context 'with milestones' do
+      let(:returned_milestones) { json_response['milestones'].map {|m| m['title']} }
+
+      subject { put api("/projects/#{project.id}/releases/v0.1", maintainer), params: params }
+
+      context 'when a milestone is passed in' do
+        let(:milestone) { create(:milestone, project: project, title: 'v1.0') }
+        let(:milestone_title) { milestone.title }
+        let(:params) { { milestones: [milestone_title] } }
+
+        before do
+          release.milestones << milestone
+        end
+
+        context 'a different milestone' do
+          let(:milestone_title) { 'v2.0' }
+          let!(:milestone2) { create(:milestone, project: project, title: milestone_title) }
+
+          it 'replaces the milestone' do
+            subject
+
+            expect(response).to have_gitlab_http_status(:ok)
+            expect(returned_milestones).to match_array(['v2.0'])
+          end
+        end
+
+        context 'an identical milestone' do
+          let(:milestone_title) { 'v1.0' }
+
+          it 'does not change the milestone' do
+            subject
+
+            expect(response).to have_gitlab_http_status(:ok)
+            expect(returned_milestones).to match_array(['v1.0'])
+          end
+        end
+
+        context 'an empty milestone' do
+          let(:milestone_title) { nil }
+
+          it 'removes the milestone' do
+            subject
+
+            expect(response).to have_gitlab_http_status(:ok)
+            expect(json_response['milestones']).to be_nil
+          end
+        end
+
+        context 'multiple milestones' do
+          context 'with one new' do
+            let!(:milestone2) { create(:milestone, project: project, title: 'milestone2') }
+            let(:params) { { milestones: [milestone.title, milestone2.title] } }
+
+            it 'adds the new milestone' do
+              subject
+
+              expect(response).to have_gitlab_http_status(:ok)
+              expect(returned_milestones).to match_array(['v1.0', 'milestone2'])
+            end
+          end
+
+          context 'with all new' do
+            let!(:milestone2) { create(:milestone, project: project, title: 'milestone2') }
+            let!(:milestone3) { create(:milestone, project: project, title: 'milestone3') }
+            let(:params) { { milestones: [milestone2.title, milestone3.title] } }
+
+            it 'replaces the milestones' do
+              subject
+
+              expect(response).to have_gitlab_http_status(:ok)
+              expect(returned_milestones).to match_array(%w(milestone2 milestone3))
+            end
+          end
+        end
+      end
+    end
   end
 
   describe 'DELETE /projects/:id/releases/:tag_name' do