Commit e674e62a authored by Nick Thomas's avatar Nick Thomas

Merge branch 'pre-releases-38105a' into 'master'

Show Upcoming Status for Releases

See merge request gitlab-org/gitlab-ce!29577
parents d6391c65 7ecffe29
...@@ -28,7 +28,7 @@ export default { ...@@ -28,7 +28,7 @@ export default {
computed: { computed: {
releasedTimeAgo() { releasedTimeAgo() {
return sprintf(__('released %{time}'), { return sprintf(__('released %{time}'), {
time: this.timeFormated(this.release.created_at), time: this.timeFormated(this.release.released_at),
}); });
}, },
userImageAltDescription() { userImageAltDescription() {
...@@ -56,8 +56,8 @@ export default { ...@@ -56,8 +56,8 @@ export default {
<div class="card-body"> <div class="card-body">
<h2 class="card-title mt-0"> <h2 class="card-title mt-0">
{{ release.name }} {{ release.name }}
<gl-badge v-if="release.pre_release" variant="warning" class="align-middle">{{ <gl-badge v-if="release.upcoming_release" variant="warning" class="align-middle">{{
__('Pre-release') __('Upcoming Release')
}}</gl-badge> }}</gl-badge>
</h2> </h2>
...@@ -74,7 +74,7 @@ export default { ...@@ -74,7 +74,7 @@ export default {
<div class="append-right-4"> <div class="append-right-4">
&bull; &bull;
<span v-gl-tooltip.bottom :title="tooltipTitle(release.created_at)"> <span v-gl-tooltip.bottom :title="tooltipTitle(release.released_at)">
{{ releasedTimeAgo }} {{ releasedTimeAgo }}
</span> </span>
</div> </div>
......
...@@ -12,12 +12,16 @@ class Release < ApplicationRecord ...@@ -12,12 +12,16 @@ class Release < ApplicationRecord
has_many :links, class_name: 'Releases::Link' has_many :links, class_name: 'Releases::Link'
default_value_for :released_at, allows_nil: false do
Time.zone.now
end
accepts_nested_attributes_for :links, allow_destroy: true accepts_nested_attributes_for :links, allow_destroy: true
validates :description, :project, :tag, presence: true validates :description, :project, :tag, presence: true
validates :name, presence: true, on: :create validates :name, presence: true, on: :create
scope :sorted, -> { order(created_at: :desc) } scope :sorted, -> { order(released_at: :desc) }
delegate :repository, to: :project delegate :repository, to: :project
...@@ -44,6 +48,10 @@ class Release < ApplicationRecord ...@@ -44,6 +48,10 @@ class Release < ApplicationRecord
end end
end end
def upcoming_release?
released_at.present? && released_at > Time.zone.now
end
private private
def actual_sha def actual_sha
......
...@@ -22,6 +22,10 @@ module Releases ...@@ -22,6 +22,10 @@ module Releases
params[:description] params[:description]
end end
def released_at
params[:released_at]
end
def release def release
strong_memoize(:release) do strong_memoize(:release) do
project.releases.find_by_tag(tag_name) project.releases.find_by_tag(tag_name)
......
...@@ -58,6 +58,7 @@ module Releases ...@@ -58,6 +58,7 @@ module Releases
author: current_user, author: current_user,
tag: tag.name, tag: tag.name,
sha: tag.dereferenced_target.sha, sha: tag.dereferenced_target.sha,
released_at: released_at,
links_attributes: params.dig(:assets, 'links') || [] links_attributes: params.dig(:assets, 'links') || []
) )
end end
......
---
title: Show an Upcoming Status for Releases
merge_request: 29577
author:
type: added
# frozen_string_literal: true
class AddReleasedAtToReleasesTable < ActiveRecord::Migration[5.1]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column(:releases, :released_at, :datetime_with_timezone)
end
end
# frozen_string_literal: true
class BackfillAndAddNotNullConstraintToReleasedAtColumnOnReleasesTable < ActiveRecord::Migration[5.1]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
update_column_in_batches(:releases, :released_at, Arel.sql('created_at'))
change_column_null(:releases, :released_at, false)
end
def down
change_column_null(:releases, :released_at, true)
end
end
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20190628145246) do ActiveRecord::Schema.define(version: 20190628185004) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -2902,6 +2902,7 @@ ActiveRecord::Schema.define(version: 20190628145246) do ...@@ -2902,6 +2902,7 @@ ActiveRecord::Schema.define(version: 20190628145246) do
t.integer "author_id" t.integer "author_id"
t.string "name" t.string "name"
t.string "sha" t.string "sha"
t.datetime_with_timezone "released_at", null: false
t.index ["author_id"], name: "index_releases_on_author_id", using: :btree t.index ["author_id"], name: "index_releases_on_author_id", using: :btree
t.index ["project_id", "tag"], name: "index_releases_on_project_id_and_tag", using: :btree t.index ["project_id", "tag"], name: "index_releases_on_project_id_and_tag", using: :btree
t.index ["project_id"], name: "index_releases_on_project_id", using: :btree t.index ["project_id"], name: "index_releases_on_project_id", using: :btree
......
...@@ -1186,8 +1186,10 @@ module API ...@@ -1186,8 +1186,10 @@ module API
MarkupHelper.markdown_field(entity, :description) MarkupHelper.markdown_field(entity, :description)
end end
expose :created_at expose :created_at
expose :released_at
expose :author, using: Entities::UserBasic, if: -> (release, _) { release.author.present? } expose :author, using: Entities::UserBasic, if: -> (release, _) { release.author.present? }
expose :commit, using: Entities::Commit, if: lambda { |_, _| can_download_code? } expose :commit, using: Entities::Commit, if: lambda { |_, _| can_download_code? }
expose :upcoming_release?, as: :upcoming_release
expose :assets do expose :assets do
expose :assets_count, as: :count do |release, _| expose :assets_count, as: :count do |release, _|
......
...@@ -54,6 +54,7 @@ module API ...@@ -54,6 +54,7 @@ module API
requires :url, type: String requires :url, type: String
end end
end end
optional :released_at, type: DateTime, desc: 'The date when the release will be/was ready. Defaults to the current time.'
end end
post ':id/releases' do post ':id/releases' do
authorize_create_release! authorize_create_release!
...@@ -77,6 +78,7 @@ module API ...@@ -77,6 +78,7 @@ module API
requires :tag_name, type: String, desc: 'The name of the tag', as: :tag requires :tag_name, type: String, desc: 'The name of the tag', as: :tag
optional :name, type: String, desc: 'The name of the release' optional :name, type: String, desc: 'The name of the release'
optional :description, type: String, desc: 'Release notes with markdown support' optional :description, type: String, desc: 'Release notes with markdown support'
optional :released_at, type: DateTime, desc: 'The date when the release will be/was ready. Defaults to the current time.'
end end
put ':id/releases/:tag_name', requirements: RELEASE_ENDPOINT_REQUIREMETS do put ':id/releases/:tag_name', requirements: RELEASE_ENDPOINT_REQUIREMETS do
authorize_update_release! authorize_update_release!
......
...@@ -10,6 +10,7 @@ module Gitlab ...@@ -10,6 +10,7 @@ module Gitlab
name: raw_data.name, name: raw_data.name,
description: raw_data.body, description: raw_data.body,
created_at: raw_data.created_at, created_at: raw_data.created_at,
released_at: raw_data.published_at,
updated_at: raw_data.created_at updated_at: raw_data.created_at
} }
end end
......
...@@ -7574,9 +7574,6 @@ msgstr "" ...@@ -7574,9 +7574,6 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will." msgid "Please wait while we import the repository for you. Refresh at will."
msgstr "" msgstr ""
msgid "Pre-release"
msgstr ""
msgid "Preferences" msgid "Preferences"
msgstr "" msgstr ""
...@@ -11378,6 +11375,9 @@ msgstr "" ...@@ -11378,6 +11375,9 @@ msgstr ""
msgid "Upcoming" msgid "Upcoming"
msgstr "" msgstr ""
msgid "Upcoming Release"
msgstr ""
msgid "Update" msgid "Update"
msgstr "" msgstr ""
......
...@@ -6,6 +6,7 @@ FactoryBot.define do ...@@ -6,6 +6,7 @@ FactoryBot.define do
description "Awesome release" description "Awesome release"
project project
author author
released_at { Time.zone.parse('2018-10-20T18:00:00Z') }
trait :legacy do trait :legacy do
sha nil sha nil
......
...@@ -16,6 +16,7 @@ describe 'User views releases', :js do ...@@ -16,6 +16,7 @@ describe 'User views releases', :js do
expect(page).to have_content(release.name) expect(page).to have_content(release.name)
expect(page).to have_content(release.tag) expect(page).to have_content(release.tag)
expect(page).not_to have_content('Upcoming Release')
end end
context 'when there is a link as an asset' do context 'when there is a link as an asset' do
...@@ -43,4 +44,15 @@ describe 'User views releases', :js do ...@@ -43,4 +44,15 @@ describe 'User views releases', :js do
end end
end end
end end
context 'with an upcoming release' do
let(:tomorrow) { Time.zone.now + 1.day }
let!(:release) { create(:release, project: project, released_at: tomorrow ) }
it 'sees the upcoming tag' do
visit project_releases_path(project)
expect(page).to have_content('Upcoming Release')
end
end
end end
...@@ -12,8 +12,8 @@ describe ReleasesFinder do ...@@ -12,8 +12,8 @@ describe ReleasesFinder do
subject { described_class.new(project, user)} subject { described_class.new(project, user)}
before do before do
v1_0_0.update_attribute(:created_at, 2.days.ago) v1_0_0.update_attribute(:released_at, 2.days.ago)
v1_1_0.update_attribute(:created_at, 1.day.ago) v1_1_0.update_attribute(:released_at, 1.day.ago)
end end
describe '#execute' do describe '#execute' do
...@@ -30,7 +30,7 @@ describe ReleasesFinder do ...@@ -30,7 +30,7 @@ describe ReleasesFinder do
project.add_developer(user) project.add_developer(user)
end end
it 'sorts by creation date' do it 'sorts by release date' do
releases = subject.execute releases = subject.execute
expect(releases).to be_present expect(releases).to be_present
......
{ {
"type": "object", "type": "object",
"required": ["name", "tag_name", "commit"], "required": ["name", "tag_name", "commit", "released_at"],
"properties": { "properties": {
"name": { "type": "string" }, "name": { "type": "string" },
"tag_name": { "type": "string" }, "tag_name": { "type": "string" },
"description": { "type": "string" }, "description": { "type": "string" },
"description_html": { "type": "string" }, "description_html": { "type": "string" },
"created_at": { "type": "date" }, "created_at": { "type": "date" },
"released_at": { "type": "date" },
"upcoming_release": { "type": "boolean" },
"commit": { "commit": {
"oneOf": [{ "type": "null" }, { "$ref": "commit/basic.json" }] "oneOf": [{ "type": "null" }, { "$ref": "commit/basic.json" }]
}, },
......
{ {
"type": "object", "type": "object",
"required": ["name"], "required": ["name", "released_at"],
"properties": { "properties": {
"name": { "type": "string" }, "name": { "type": "string" },
"description": { "type": "string" }, "description": { "type": "string" },
"description_html": { "type": "string" }, "description_html": { "type": "string" },
"created_at": { "type": "date" }, "created_at": { "type": "date" },
"released_at": { "type": "date" },
"upcoming_release": { "type": "boolean" },
"author": { "author": {
"oneOf": [{ "type": "null" }, { "$ref": "../user/basic.json" }] "oneOf": [{ "type": "null" }, { "$ref": "../user/basic.json" }]
}, },
......
...@@ -14,7 +14,7 @@ describe('Release block', () => { ...@@ -14,7 +14,7 @@ describe('Release block', () => {
description_html: '<div><h2>changelog</h2><ul><li>line1</li<li>line 2</li></ul></div>', description_html: '<div><h2>changelog</h2><ul><li>line1</li<li>line 2</li></ul></div>',
author_name: 'Release bot', author_name: 'Release bot',
author_email: 'release-bot@example.com', author_email: 'release-bot@example.com',
created_at: '2012-05-28T05:00:00-07:00', released_at: '2012-05-28T05:00:00-07:00',
author: { author: {
avatar_url: 'uploads/-/system/user/avatar/johndoe/avatar.png', avatar_url: 'uploads/-/system/user/avatar/johndoe/avatar.png',
id: 482476, id: 482476,
...@@ -101,7 +101,7 @@ describe('Release block', () => { ...@@ -101,7 +101,7 @@ describe('Release block', () => {
}); });
it('renders release date', () => { it('renders release date', () => {
expect(vm.$el.textContent).toContain(timeagoMixin.methods.timeFormated(release.created_at)); expect(vm.$el.textContent).toContain(timeagoMixin.methods.timeFormated(release.released_at));
}); });
it('renders number of assets provided', () => { it('renders number of assets provided', () => {
...@@ -152,13 +152,13 @@ describe('Release block', () => { ...@@ -152,13 +152,13 @@ describe('Release block', () => {
}); });
}); });
describe('with pre_release flag', () => { describe('with upcoming_release flag', () => {
beforeEach(() => { beforeEach(() => {
vm = factory(Object.assign({}, release, { pre_release: true })); vm = factory(Object.assign({}, release, { upcoming_release: true }));
}); });
it('renders pre-release badge', () => { it('renders upcoming release badge', () => {
expect(vm.$el.textContent).toContain('Pre-release'); expect(vm.$el.textContent).toContain('Upcoming Release');
}); });
}); });
}); });
...@@ -123,6 +123,7 @@ Release: ...@@ -123,6 +123,7 @@ Release:
- project_id - project_id
- created_at - created_at
- updated_at - updated_at
- released_at
Releases::Link: Releases::Link:
- id - id
- release_id - release_id
......
...@@ -132,6 +132,7 @@ describe Gitlab::LegacyGithubImport::Importer do ...@@ -132,6 +132,7 @@ describe Gitlab::LegacyGithubImport::Importer do
body: 'Release v1.0.0', body: 'Release v1.0.0',
draft: false, draft: false,
created_at: created_at, created_at: created_at,
published_at: created_at,
updated_at: updated_at, updated_at: updated_at,
url: "#{api_root}/repos/octocat/Hello-World/releases/1" url: "#{api_root}/repos/octocat/Hello-World/releases/1"
) )
...@@ -144,6 +145,7 @@ describe Gitlab::LegacyGithubImport::Importer do ...@@ -144,6 +145,7 @@ describe Gitlab::LegacyGithubImport::Importer do
body: nil, body: nil,
draft: false, draft: false,
created_at: created_at, created_at: created_at,
published_at: created_at,
updated_at: updated_at, updated_at: updated_at,
url: "#{api_root}/repos/octocat/Hello-World/releases/2" url: "#{api_root}/repos/octocat/Hello-World/releases/2"
) )
......
...@@ -4,6 +4,7 @@ describe Gitlab::LegacyGithubImport::ReleaseFormatter do ...@@ -4,6 +4,7 @@ describe Gitlab::LegacyGithubImport::ReleaseFormatter do
let!(:project) { create(:project, namespace: create(:namespace, path: 'octocat')) } let!(:project) { create(:project, namespace: create(:namespace, path: 'octocat')) }
let(:octocat) { double(id: 123456, login: 'octocat') } let(:octocat) { double(id: 123456, login: 'octocat') }
let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
let(:published_at) { DateTime.strptime('2011-01-26T20:00:00Z') }
let(:base_data) do let(:base_data) do
{ {
...@@ -11,7 +12,7 @@ describe Gitlab::LegacyGithubImport::ReleaseFormatter do ...@@ -11,7 +12,7 @@ describe Gitlab::LegacyGithubImport::ReleaseFormatter do
name: 'First release', name: 'First release',
draft: false, draft: false,
created_at: created_at, created_at: created_at,
published_at: created_at, published_at: published_at,
body: 'Release v1.0.0' body: 'Release v1.0.0'
} }
end end
...@@ -28,6 +29,7 @@ describe Gitlab::LegacyGithubImport::ReleaseFormatter do ...@@ -28,6 +29,7 @@ describe Gitlab::LegacyGithubImport::ReleaseFormatter do
name: 'First release', name: 'First release',
description: 'Release v1.0.0', description: 'Release v1.0.0',
created_at: created_at, created_at: created_at,
released_at: published_at,
updated_at: created_at updated_at: created_at
} }
......
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20190628185004_backfill_and_add_not_null_constraint_to_released_at_column_on_releases_table.rb')
describe BackfillAndAddNotNullConstraintToReleasedAtColumnOnReleasesTable, :migration do
let(:releases) { table(:releases) }
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
subject(:migration) { described_class.new }
it 'fills released_at with the value of created_at' do
created_at_a = Time.zone.parse('2019-02-10T08:00:00Z')
created_at_b = Time.zone.parse('2019-03-10T18:00:00Z')
namespace = namespaces.create(name: 'foo', path: 'foo')
project = projects.create!(namespace_id: namespace.id)
release_a = releases.create!(project_id: project.id, created_at: created_at_a)
release_b = releases.create!(project_id: project.id, created_at: created_at_b)
disable_migrations_output { migration.up }
release_a.reload
release_b.reload
expect(release_a.released_at).to eq(created_at_a)
expect(release_b.released_at).to eq(created_at_b)
end
end
...@@ -64,4 +64,14 @@ RSpec.describe Release do ...@@ -64,4 +64,14 @@ RSpec.describe Release do
is_expected.to all(be_a(Releases::Source)) is_expected.to all(be_a(Releases::Source))
end end
end end
describe '#upcoming_release?' do
context 'during the backfill migration when released_at could be nil' do
it 'handles a nil released_at value and returns false' do
allow(release).to receive(:released_at).and_return nil
expect(release.upcoming_release?).to eq(false)
end
end
end
end end
...@@ -24,7 +24,7 @@ describe API::Releases do ...@@ -24,7 +24,7 @@ describe API::Releases do
project: project, project: project,
tag: 'v0.1', tag: 'v0.1',
author: maintainer, author: maintainer,
created_at: 2.days.ago) released_at: 2.days.ago)
end end
let!(:release_2) do let!(:release_2) do
...@@ -32,7 +32,7 @@ describe API::Releases do ...@@ -32,7 +32,7 @@ describe API::Releases do
project: project, project: project,
tag: 'v0.2', tag: 'v0.2',
author: maintainer, author: maintainer,
created_at: 1.day.ago) released_at: 1.day.ago)
end end
it 'returns 200 HTTP status' do it 'returns 200 HTTP status' do
...@@ -41,7 +41,7 @@ describe API::Releases do ...@@ -41,7 +41,7 @@ describe API::Releases do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
end end
it 'returns releases ordered by created_at' do it 'returns releases ordered by released_at' do
get api("/projects/#{project.id}/releases", maintainer) get api("/projects/#{project.id}/releases", maintainer)
expect(json_response.count).to eq(2) expect(json_response.count).to eq(2)
...@@ -56,6 +56,26 @@ describe API::Releases do ...@@ -56,6 +56,26 @@ describe API::Releases do
end end
end end
it 'returns an upcoming_release status for a future release' do
tomorrow = Time.now.utc + 1.day
create(:release, project: project, tag: 'v0.1', author: maintainer, released_at: tomorrow)
get api("/projects/#{project.id}/releases", maintainer)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.first['upcoming_release']).to eq(true)
end
it 'returns an upcoming_release status for a past release' do
yesterday = Time.now.utc - 1.day
create(:release, project: project, tag: 'v0.1', author: maintainer, released_at: yesterday)
get api("/projects/#{project.id}/releases", maintainer)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.first['upcoming_release']).to eq(false)
end
context 'when tag does not exist in git repository' do context 'when tag does not exist in git repository' do
let!(:release) { create(:release, project: project, tag: 'v1.1.5') } let!(:release) { create(:release, project: project, tag: 'v1.1.5') }
...@@ -316,6 +336,51 @@ describe API::Releases do ...@@ -316,6 +336,51 @@ describe API::Releases do
expect(project.releases.last.description).to eq('Super nice release') expect(project.releases.last.description).to eq('Super nice release')
end end
it 'sets the released_at to the current time if the released_at parameter is not provided' do
now = Time.zone.parse('2015-08-25 06:00:00Z')
Timecop.freeze(now) do
post api("/projects/#{project.id}/releases", maintainer), params: params
expect(project.releases.last.released_at).to eq(now)
end
end
it 'sets the released_at to the value in the parameters if specified' do
params = {
name: 'New release',
tag_name: 'v0.1',
description: 'Super nice release',
released_at: '2019-03-20T10:00:00Z'
}
post api("/projects/#{project.id}/releases", maintainer), params: params
expect(project.releases.last.released_at).to eq('2019-03-20T10:00:00Z')
end
it 'assumes the utc timezone for released_at if the timezone is not provided' do
params = {
name: 'New release',
tag_name: 'v0.1',
description: 'Super nice release',
released_at: '2019-03-25 10:00:00'
}
post api("/projects/#{project.id}/releases", maintainer), params: params
expect(project.releases.last.released_at).to eq('2019-03-25T10:00:00Z')
end
it 'allows specifying a released_at with a local time zone' do
params = {
name: 'New release',
tag_name: 'v0.1',
description: 'Super nice release',
released_at: '2019-03-25T10:00:00+09:00'
}
post api("/projects/#{project.id}/releases", maintainer), params: params
expect(project.releases.last.released_at).to eq('2019-03-25T01:00:00Z')
end
context 'when description is empty' do context 'when description is empty' do
let(:params) do let(:params) do
{ {
...@@ -540,6 +605,7 @@ describe API::Releases do ...@@ -540,6 +605,7 @@ describe API::Releases do
project: project, project: project,
tag: 'v0.1', tag: 'v0.1',
name: 'New release', name: 'New release',
released_at: '2018-03-01T22:00:00Z',
description: 'Super nice release') description: 'Super nice release')
end end
...@@ -560,6 +626,7 @@ describe API::Releases do ...@@ -560,6 +626,7 @@ describe API::Releases do
expect(project.releases.last.tag).to eq('v0.1') expect(project.releases.last.tag).to eq('v0.1')
expect(project.releases.last.name).to eq('New release') expect(project.releases.last.name).to eq('New release')
expect(project.releases.last.released_at).to eq('2018-03-01T22:00:00Z')
end end
it 'matches response schema' do it 'matches response schema' do
...@@ -568,6 +635,14 @@ describe API::Releases do ...@@ -568,6 +635,14 @@ describe API::Releases do
expect(response).to match_response_schema('public_api/v4/release') expect(response).to match_response_schema('public_api/v4/release')
end end
it 'updates released_at' do
params = { released_at: '2015-10-10T05:00:00Z' }
put api("/projects/#{project.id}/releases/v0.1", maintainer), params: params
expect(project.releases.last.released_at).to eq('2015-10-10T05:00:00Z')
end
context 'when user tries to update sha' do context 'when user tries to update sha' do
let(:params) { { sha: 'xxx' } } let(:params) { { sha: 'xxx' } }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment