Commit 7ecffe29 authored by Jason Goodman's avatar Jason Goodman Committed by Nick Thomas

Show upcoming status for releases

Add released_at field to releases API
Add released_at column to releases table
Return releases to the API sorted by released_at
parent d6391c65
......@@ -28,7 +28,7 @@ export default {
computed: {
releasedTimeAgo() {
return sprintf(__('released %{time}'), {
time: this.timeFormated(this.release.created_at),
time: this.timeFormated(this.release.released_at),
});
},
userImageAltDescription() {
......@@ -56,8 +56,8 @@ export default {
<div class="card-body">
<h2 class="card-title mt-0">
{{ release.name }}
<gl-badge v-if="release.pre_release" variant="warning" class="align-middle">{{
__('Pre-release')
<gl-badge v-if="release.upcoming_release" variant="warning" class="align-middle">{{
__('Upcoming Release')
}}</gl-badge>
</h2>
......@@ -74,7 +74,7 @@ export default {
<div class="append-right-4">
&bull;
<span v-gl-tooltip.bottom :title="tooltipTitle(release.created_at)">
<span v-gl-tooltip.bottom :title="tooltipTitle(release.released_at)">
{{ releasedTimeAgo }}
</span>
</div>
......
......@@ -12,12 +12,16 @@ class Release < ApplicationRecord
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
validates :description, :project, :tag, presence: true
validates :name, presence: true, on: :create
scope :sorted, -> { order(created_at: :desc) }
scope :sorted, -> { order(released_at: :desc) }
delegate :repository, to: :project
......@@ -44,6 +48,10 @@ class Release < ApplicationRecord
end
end
def upcoming_release?
released_at.present? && released_at > Time.zone.now
end
private
def actual_sha
......
......@@ -22,6 +22,10 @@ module Releases
params[:description]
end
def released_at
params[:released_at]
end
def release
strong_memoize(:release) do
project.releases.find_by_tag(tag_name)
......
......@@ -58,6 +58,7 @@ module Releases
author: current_user,
tag: tag.name,
sha: tag.dereferenced_target.sha,
released_at: released_at,
links_attributes: params.dig(:assets, 'links') || []
)
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 @@
#
# 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
enable_extension "plpgsql"
......@@ -2902,6 +2902,7 @@ ActiveRecord::Schema.define(version: 20190628145246) do
t.integer "author_id"
t.string "name"
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 ["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
......
......@@ -1186,8 +1186,10 @@ module API
MarkupHelper.markdown_field(entity, :description)
end
expose :created_at
expose :released_at
expose :author, using: Entities::UserBasic, if: -> (release, _) { release.author.present? }
expose :commit, using: Entities::Commit, if: lambda { |_, _| can_download_code? }
expose :upcoming_release?, as: :upcoming_release
expose :assets do
expose :assets_count, as: :count do |release, _|
......
......@@ -54,6 +54,7 @@ module API
requires :url, type: String
end
end
optional :released_at, type: DateTime, desc: 'The date when the release will be/was ready. Defaults to the current time.'
end
post ':id/releases' do
authorize_create_release!
......@@ -77,6 +78,7 @@ module API
requires :tag_name, type: String, desc: 'The name of the tag', as: :tag
optional :name, type: String, desc: 'The name of the release'
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
put ':id/releases/:tag_name', requirements: RELEASE_ENDPOINT_REQUIREMETS do
authorize_update_release!
......
......@@ -10,6 +10,7 @@ module Gitlab
name: raw_data.name,
description: raw_data.body,
created_at: raw_data.created_at,
released_at: raw_data.published_at,
updated_at: raw_data.created_at
}
end
......
......@@ -7574,9 +7574,6 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
msgid "Pre-release"
msgstr ""
msgid "Preferences"
msgstr ""
......@@ -11378,6 +11375,9 @@ msgstr ""
msgid "Upcoming"
msgstr ""
msgid "Upcoming Release"
msgstr ""
msgid "Update"
msgstr ""
......
......@@ -6,6 +6,7 @@ FactoryBot.define do
description "Awesome release"
project
author
released_at { Time.zone.parse('2018-10-20T18:00:00Z') }
trait :legacy do
sha nil
......
......@@ -16,6 +16,7 @@ describe 'User views releases', :js do
expect(page).to have_content(release.name)
expect(page).to have_content(release.tag)
expect(page).not_to have_content('Upcoming Release')
end
context 'when there is a link as an asset' do
......@@ -43,4 +44,15 @@ describe 'User views releases', :js do
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
......@@ -12,8 +12,8 @@ describe ReleasesFinder do
subject { described_class.new(project, user)}
before do
v1_0_0.update_attribute(:created_at, 2.days.ago)
v1_1_0.update_attribute(:created_at, 1.day.ago)
v1_0_0.update_attribute(:released_at, 2.days.ago)
v1_1_0.update_attribute(:released_at, 1.day.ago)
end
describe '#execute' do
......@@ -30,7 +30,7 @@ describe ReleasesFinder do
project.add_developer(user)
end
it 'sorts by creation date' do
it 'sorts by release date' do
releases = subject.execute
expect(releases).to be_present
......
{
"type": "object",
"required": ["name", "tag_name", "commit"],
"required": ["name", "tag_name", "commit", "released_at"],
"properties": {
"name": { "type": "string" },
"tag_name": { "type": "string" },
"description": { "type": "string" },
"description_html": { "type": "string" },
"created_at": { "type": "date" },
"released_at": { "type": "date" },
"upcoming_release": { "type": "boolean" },
"commit": {
"oneOf": [{ "type": "null" }, { "$ref": "commit/basic.json" }]
},
......
{
"type": "object",
"required": ["name"],
"required": ["name", "released_at"],
"properties": {
"name": { "type": "string" },
"description": { "type": "string" },
"description_html": { "type": "string" },
"created_at": { "type": "date" },
"released_at": { "type": "date" },
"upcoming_release": { "type": "boolean" },
"author": {
"oneOf": [{ "type": "null" }, { "$ref": "../user/basic.json" }]
},
......
......@@ -14,7 +14,7 @@ describe('Release block', () => {
description_html: '<div><h2>changelog</h2><ul><li>line1</li<li>line 2</li></ul></div>',
author_name: 'Release bot',
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: {
avatar_url: 'uploads/-/system/user/avatar/johndoe/avatar.png',
id: 482476,
......@@ -101,7 +101,7 @@ describe('Release block', () => {
});
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', () => {
......@@ -152,13 +152,13 @@ describe('Release block', () => {
});
});
describe('with pre_release flag', () => {
describe('with upcoming_release flag', () => {
beforeEach(() => {
vm = factory(Object.assign({}, release, { pre_release: true }));
vm = factory(Object.assign({}, release, { upcoming_release: true }));
});
it('renders pre-release badge', () => {
expect(vm.$el.textContent).toContain('Pre-release');
it('renders upcoming release badge', () => {
expect(vm.$el.textContent).toContain('Upcoming Release');
});
});
});
......@@ -123,6 +123,7 @@ Release:
- project_id
- created_at
- updated_at
- released_at
Releases::Link:
- id
- release_id
......
......@@ -132,6 +132,7 @@ describe Gitlab::LegacyGithubImport::Importer do
body: 'Release v1.0.0',
draft: false,
created_at: created_at,
published_at: created_at,
updated_at: updated_at,
url: "#{api_root}/repos/octocat/Hello-World/releases/1"
)
......@@ -144,6 +145,7 @@ describe Gitlab::LegacyGithubImport::Importer do
body: nil,
draft: false,
created_at: created_at,
published_at: created_at,
updated_at: updated_at,
url: "#{api_root}/repos/octocat/Hello-World/releases/2"
)
......
......@@ -4,6 +4,7 @@ describe Gitlab::LegacyGithubImport::ReleaseFormatter do
let!(:project) { create(:project, namespace: create(:namespace, path: 'octocat')) }
let(:octocat) { double(id: 123456, login: 'octocat') }
let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
let(:published_at) { DateTime.strptime('2011-01-26T20:00:00Z') }
let(:base_data) do
{
......@@ -11,7 +12,7 @@ describe Gitlab::LegacyGithubImport::ReleaseFormatter do
name: 'First release',
draft: false,
created_at: created_at,
published_at: created_at,
published_at: published_at,
body: 'Release v1.0.0'
}
end
......@@ -28,6 +29,7 @@ describe Gitlab::LegacyGithubImport::ReleaseFormatter do
name: 'First release',
description: 'Release v1.0.0',
created_at: created_at,
released_at: published_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
is_expected.to all(be_a(Releases::Source))
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
......@@ -24,7 +24,7 @@ describe API::Releases do
project: project,
tag: 'v0.1',
author: maintainer,
created_at: 2.days.ago)
released_at: 2.days.ago)
end
let!(:release_2) do
......@@ -32,7 +32,7 @@ describe API::Releases do
project: project,
tag: 'v0.2',
author: maintainer,
created_at: 1.day.ago)
released_at: 1.day.ago)
end
it 'returns 200 HTTP status' do
......@@ -41,7 +41,7 @@ describe API::Releases do
expect(response).to have_gitlab_http_status(:ok)
end
it 'returns releases ordered by created_at' do
it 'returns releases ordered by released_at' do
get api("/projects/#{project.id}/releases", maintainer)
expect(json_response.count).to eq(2)
......@@ -56,6 +56,26 @@ describe API::Releases do
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
let!(:release) { create(:release, project: project, tag: 'v1.1.5') }
......@@ -316,6 +336,51 @@ describe API::Releases do
expect(project.releases.last.description).to eq('Super nice release')
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
let(:params) do
{
......@@ -540,6 +605,7 @@ describe API::Releases do
project: project,
tag: 'v0.1',
name: 'New release',
released_at: '2018-03-01T22:00:00Z',
description: 'Super nice release')
end
......@@ -560,6 +626,7 @@ describe API::Releases do
expect(project.releases.last.tag).to eq('v0.1')
expect(project.releases.last.name).to eq('New release')
expect(project.releases.last.released_at).to eq('2018-03-01T22:00:00Z')
end
it 'matches response schema' do
......@@ -568,6 +635,14 @@ describe API::Releases do
expect(response).to match_response_schema('public_api/v4/release')
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
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