diff --git a/CHANGELOG b/CHANGELOG
index 087a339f3f66a23caac706b30b95853cff918450..66950b0b0d11e3d6d43894715192c58d7b5f9f1e 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -15,6 +15,7 @@ v 8.8.0 (unreleased)
   - Make build status canceled if any of the jobs was canceled and none failed
   - Upgrade Sidekiq to 4.1.2
   - Added /health_check endpoint for checking service status
+  - Make 'upcoming' filter for milestones work better across projects
   - Sanitize repo paths in new project error message
   - Bump mail_room to 0.7.0 to fix stuck IDLE connections
   - Remove future dates from contribution calendar graph.
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index f00f3f709e99c7e9d4fd792209f2aba989d20d8f..5849e00662b9a1a59bd65bbeeaa3903f99fe4f82 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -252,8 +252,8 @@ class IssuableFinder
       if filter_by_no_milestone?
         items = items.where(milestone_id: [-1, nil])
       elsif filter_by_upcoming_milestone?
-        upcoming = Milestone.where(project_id: projects).upcoming
-        items = items.joins(:milestone).where(milestones: { title: upcoming.try(:title) })
+        upcoming_ids = Milestone.upcoming_ids_by_projects(projects)
+        items = items.joins(:milestone).where(milestone_id: upcoming_ids)
       else
         items = items.joins(:milestone).where(milestones: { title: params[:milestone_title] })
 
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index e4fdd23badb5d94f71e77769d8e786a77b411a37..fe9a281f3661d4719c2162fd7e79c08c36ee2d87 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -67,8 +67,18 @@ class Milestone < ActiveRecord::Base
     @link_reference_pattern ||= super("milestones", /(?<milestone>\d+)/)
   end
 
-  def self.upcoming
-    self.where('due_date > ?', Time.now).reorder(due_date: :asc).first
+  def self.upcoming_ids_by_projects(projects)
+    rel = unscoped.of_projects(projects).active.where('due_date > ?', Time.now)
+
+    if Gitlab::Database.postgresql?
+      rel.order(:project_id, :due_date).select('DISTINCT ON (project_id) id')
+    else
+      rel.
+        group(:project_id).
+        having('due_date = MIN(due_date)').
+        pluck(:id, :project_id, :due_date).
+        map(&:first)
+    end
   end
 
   def to_reference(from_project = nil)
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index bc607a29751d85639ab9c25f981ea8ccd672b465..ec8809e6926c3940f5dba90bc6d6dceaaba2f202 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -1,10 +1,10 @@
 require 'spec_helper'
 
 describe IssuesFinder do
-  let(:user) { create :user }
-  let(:user2) { create :user }
-  let(:project1) { create(:project) }
-  let(:project2) { create(:project) }
+  let(:user) { create(:user) }
+  let(:user2) { create(:user) }
+  let(:project1) { create(:empty_project) }
+  let(:project2) { create(:empty_project) }
   let(:milestone) { create(:milestone, project: project1) }
   let(:label) { create(:label, project: project2) }
   let(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone) }
@@ -16,101 +16,147 @@ describe IssuesFinder do
     project1.team << [user, :master]
     project2.team << [user, :developer]
     project2.team << [user2, :developer]
+
+    issue1
+    issue2
+    issue3
   end
 
-  describe :execute do
-    before :each do
-      issue1
-      issue2
-      issue3
-    end
+  describe '#execute' do
+    let(:search_user) { user }
+    let(:params) { {} }
+    let(:issues) { IssuesFinder.new(search_user, params.merge(scope: scope, state: 'opened')).execute }
 
     context 'scope: all' do
-      it 'should filter by all' do
-        params = { scope: "all", state: 'opened' }
-        issues = IssuesFinder.new(user, params).execute
-        expect(issues.size).to eq(3)
+      let(:scope) { 'all' }
+
+      it 'returns all issues' do
+        expect(issues).to contain_exactly(issue1, issue2, issue3)
       end
 
-      it 'should filter by assignee id' do
-        params = { scope: "all", assignee_id: user.id, state: 'opened' }
-        issues = IssuesFinder.new(user, params).execute
-        expect(issues.size).to eq(2)
+      context 'filtering by assignee ID' do
+        let(:params) { { assignee_id: user.id } }
+
+        it 'returns issues assigned to that user' do
+          expect(issues).to contain_exactly(issue1, issue2)
+        end
       end
 
-      it 'should filter by author id' do
-        params = { scope: "all", author_id: user2.id, state: 'opened' }
-        issues = IssuesFinder.new(user, params).execute
-        expect(issues).to eq([issue3])
+      context 'filtering by author ID' do
+        let(:params) { { author_id: user2.id } }
+
+        it 'returns issues created by that user' do
+          expect(issues).to contain_exactly(issue3)
+        end
       end
 
-      it 'should filter by milestone id' do
-        params = { scope: "all", milestone_title: milestone.title, state: 'opened' }
-        issues = IssuesFinder.new(user, params).execute
-        expect(issues).to eq([issue1])
+      context 'filtering by milestone' do
+        let(:params) { { milestone_title: milestone.title } }
+
+        it 'returns issues assigned to that milestone' do
+          expect(issues).to contain_exactly(issue1)
+        end
       end
 
-      it 'should filter by no milestone id' do
-        params = { scope: "all", milestone_title: Milestone::None.title, state: 'opened' }
-        issues = IssuesFinder.new(user, params).execute
-        expect(issues).to match_array([issue2, issue3])
+      context 'filtering by no milestone' do
+        let(:params) { { milestone_title: Milestone::None.title } }
+
+        it 'returns issues with no milestone' do
+          expect(issues).to contain_exactly(issue2, issue3)
+        end
       end
 
-      it 'should filter by label name' do
-        params = { scope: "all", label_name: label.title, state: 'opened' }
-        issues = IssuesFinder.new(user, params).execute
-        expect(issues).to eq([issue2])
+      context 'filtering by upcoming milestone' do
+        let(:params) { { milestone_title: Milestone::Upcoming.name } }
+
+        let(:project_no_upcoming_milestones) { create(:empty_project, :public) }
+        let(:project_next_1_1) { create(:empty_project, :public) }
+        let(:project_next_8_8) { create(:empty_project, :public) }
+
+        let(:yesterday) { Date.today - 1.day }
+        let(:tomorrow) { Date.today + 1.day }
+        let(:two_days_from_now) { Date.today + 2.days }
+        let(:ten_days_from_now) { Date.today + 10.days }
+
+        let(:milestones) do
+          [
+            create(:milestone, :closed, project: project_no_upcoming_milestones),
+            create(:milestone, project: project_next_1_1, title: '1.1', due_date: two_days_from_now),
+            create(:milestone, project: project_next_1_1, title: '8.8', due_date: ten_days_from_now),
+            create(:milestone, project: project_next_8_8, title: '1.1', due_date: yesterday),
+            create(:milestone, project: project_next_8_8, title: '8.8', due_date: tomorrow)
+          ]
+        end
+
+        before do
+          milestones.each do |milestone|
+            create(:issue, project: milestone.project, milestone: milestone, author: user, assignee: user)
+          end
+        end
+
+        it 'returns issues in the upcoming milestone for each project' do
+          expect(issues.map { |issue| issue.milestone.title }).to contain_exactly('1.1', '8.8')
+          expect(issues.map { |issue| issue.milestone.due_date }).to contain_exactly(tomorrow, two_days_from_now)
+        end
       end
 
-      it 'returns unique issues when filtering by multiple labels' do
-        label2 = create(:label, project: project2)
+      context 'filtering by label' do
+        let(:params) { { label_name: label.title } }
 
-        create(:label_link, label: label2, target: issue2)
+        it 'returns issues with that label' do
+          expect(issues).to contain_exactly(issue2)
+        end
+      end
 
-        params = {
-          scope:      'all',
-          label_name: [label.title, label2.title].join(','),
-          state:      'opened'
-        }
+      context 'filtering by multiple labels' do
+        let(:params) { { label_name: [label.title, label2.title].join(',') } }
+        let(:label2) { create(:label, project: project2) }
 
-        issues = IssuesFinder.new(user, params).execute
+        before { create(:label_link, label: label2, target: issue2) }
 
-        expect(issues).to eq([issue2])
+        it 'returns the unique issues with any of those labels' do
+          expect(issues).to contain_exactly(issue2)
+        end
       end
 
-      it 'should filter by no label name' do
-        params = { scope: "all", label_name: Label::None.title, state: 'opened' }
-        issues = IssuesFinder.new(user, params).execute
-        expect(issues).to match_array([issue1, issue3])
+      context 'filtering by no label' do
+        let(:params) { { label_name: Label::None.title } }
+
+        it 'returns issues with no labels' do
+          expect(issues).to contain_exactly(issue1, issue3)
+        end
       end
 
-      it 'should be empty for unauthorized user' do
-        params = { scope: "all", state: 'opened' }
-        issues = IssuesFinder.new(nil, params).execute
-        expect(issues.size).to be_zero
+      context 'when the user is unauthorized' do
+        let(:search_user) { nil }
+
+        it 'returns no results' do
+          expect(issues).to be_empty
+        end
       end
 
-      it 'should not include unauthorized issues' do
-        params = { scope: "all", state: 'opened' }
-        issues = IssuesFinder.new(user2, params).execute
-        expect(issues.size).to eq(2)
-        expect(issues).not_to include(issue1)
-        expect(issues).to include(issue2)
-        expect(issues).to include(issue3)
+      context 'when the user can see some, but not all, issues' do
+        let(:search_user) { user2 }
+
+        it 'returns only issues they can see' do
+          expect(issues).to contain_exactly(issue2, issue3)
+        end
       end
     end
 
     context 'personal scope' do
-      it 'should filter by assignee' do
-        params = { scope: "assigned-to-me", state: 'opened' }
-        issues = IssuesFinder.new(user, params).execute
-        expect(issues.size).to eq(2)
+      let(:scope) { 'assigned-to-me' }
+
+      it 'returns issue assigned to the user' do
+        expect(issues).to contain_exactly(issue1, issue2)
       end
 
-      it 'should filter by project' do
-        params = { scope: "assigned-to-me", state: 'opened', project_id: project1.id }
-        issues = IssuesFinder.new(user, params).execute
-        expect(issues.size).to eq(1)
+      context 'filtering by project' do
+        let(:params) { { project_id: project1.id } }
+
+        it 'returns issues assigned to the user in that project' do
+          expect(issues).to contain_exactly(issue1)
+        end
       end
     end
   end
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index 247a9fa9910a9a013332fde277bc4a4c4232b8da..1e18c788b503d0c48f306bda2b05cb02fb9fb285 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -204,4 +204,37 @@ describe Milestone, models: true do
         to eq([milestone])
     end
   end
+
+  describe '.upcoming_ids_by_projects' do
+    let(:project_1) { create(:empty_project) }
+    let(:project_2) { create(:empty_project) }
+    let(:project_3) { create(:empty_project) }
+    let(:projects) { [project_1, project_2, project_3] }
+
+    let!(:past_milestone_project_1) { create(:milestone, project: project_1, due_date: Time.now - 1.day) }
+    let!(:current_milestone_project_1) { create(:milestone, project: project_1, due_date: Time.now + 1.day) }
+    let!(:future_milestone_project_1) { create(:milestone, project: project_1, due_date: Time.now + 2.days) }
+
+    let!(:past_milestone_project_2) { create(:milestone, project: project_2, due_date: Time.now - 1.day) }
+    let!(:closed_milestone_project_2) { create(:milestone, :closed, project: project_2, due_date: Time.now + 1.day) }
+    let!(:current_milestone_project_2) { create(:milestone, project: project_2, due_date: Time.now + 2.days) }
+
+    let!(:past_milestone_project_3) { create(:milestone, project: project_3, due_date: Time.now - 1.day) }
+
+    # The call to `#try` is because this returns a relation with a Postgres DB,
+    # and an array of IDs with a MySQL DB.
+    let(:milestone_ids) { Milestone.upcoming_ids_by_projects(projects).map { |id| id.try(:id) || id } }
+
+    it 'returns the next upcoming open milestone ID for each project' do
+      expect(milestone_ids).to contain_exactly(current_milestone_project_1.id, current_milestone_project_2.id)
+    end
+
+    context 'when the projects have no open upcoming milestones' do
+      let(:projects) { [project_3] }
+
+      it 'returns no results' do
+        expect(milestone_ids).to be_empty
+      end
+    end
+  end
 end