Commit 3f4c19cd authored by Nick Thomas's avatar Nick Thomas

Merge branch '26019-evidence-collection' into 'master'

Provide Release JSON schema for Evidence collection

See merge request gitlab-org/gitlab!17217
parents 72e21af5 be70aa0e
# frozen_string_literal: true
class Evidence < ApplicationRecord
include ShaAttribute
belongs_to :release
before_validation :generate_summary_and_sha
default_scope { order(created_at: :asc) }
sha_attribute :summary_sha
def milestones
@milestones ||= release.milestones.includes(:issues)
end
private
def generate_summary_and_sha
summary = Evidences::EvidenceSerializer.new.represent(self) # rubocop: disable CodeReuse/Serializer
return unless summary
self.summary = summary
self.summary_sha = Gitlab::CryptoHelper.sha256(summary)
end
end
...@@ -14,6 +14,7 @@ class Release < ApplicationRecord ...@@ -14,6 +14,7 @@ class Release < ApplicationRecord
has_many :milestone_releases has_many :milestone_releases
has_many :milestones, through: :milestone_releases has_many :milestones, through: :milestone_releases
has_one :evidence
default_value_for :released_at, allows_nil: false do default_value_for :released_at, allows_nil: false do
Time.zone.now Time.zone.now
...@@ -28,6 +29,8 @@ class Release < ApplicationRecord ...@@ -28,6 +29,8 @@ class Release < ApplicationRecord
delegate :repository, to: :project delegate :repository, to: :project
after_commit :create_evidence!, on: :create
def commit def commit
strong_memoize(:commit) do strong_memoize(:commit) do
repository.commit(actual_sha) repository.commit(actual_sha)
...@@ -66,6 +69,10 @@ class Release < ApplicationRecord ...@@ -66,6 +69,10 @@ class Release < ApplicationRecord
repository.find_tag(tag) repository.find_tag(tag)
end end
end end
def create_evidence!
CreateEvidenceWorker.perform_async(self.id)
end
end end
Release.prepend_if_ee('EE::Release') Release.prepend_if_ee('EE::Release')
# frozen_string_literal: true
module Evidences
class EvidenceEntity < Grape::Entity
expose :release, using: Evidences::ReleaseEntity
end
end
# frozen_string_literal: true # frozen_string_literal: true
module Evidences module Evidences
class AuthorEntity < Grape::Entity class EvidenceSerializer < BaseSerializer
expose :id entity EvidenceEntity
expose :name
expose :email
end end
end end
...@@ -5,7 +5,6 @@ module Evidences ...@@ -5,7 +5,6 @@ module Evidences
expose :id expose :id
expose :title expose :title
expose :description expose :description
expose :author, using: AuthorEntity
expose :state expose :state
expose :iid expose :iid
expose :confidential expose :confidential
......
...@@ -9,6 +9,6 @@ module Evidences ...@@ -9,6 +9,6 @@ module Evidences
expose :iid expose :iid
expose :created_at expose :created_at
expose :due_date expose :due_date
expose :issues, using: IssueEntity expose :issues, using: Evidences::IssueEntity
end end
end end
...@@ -7,7 +7,7 @@ module Evidences ...@@ -7,7 +7,7 @@ module Evidences
expose :name expose :name
expose :description expose :description
expose :created_at expose :created_at
expose :project, using: ProjectEntity expose :project, using: Evidences::ProjectEntity
expose :milestones, using: MilestoneEntity expose :milestones, using: Evidences::MilestoneEntity
end end
end end
...@@ -173,3 +173,4 @@ ...@@ -173,3 +173,4 @@
- delete_stored_files - delete_stored_files
- import_issues_csv - import_issues_csv
- project_daily_statistics - project_daily_statistics
- create_evidence
# frozen_string_literal: true
class CreateEvidenceWorker
include ApplicationWorker
def perform(release_id)
release = Release.find_by_id(release_id)
return unless release
Evidence.create!(release: release)
end
end
---
title: Creation of Evidence collection of new releases.
merge_request: 17217
author:
type: added
...@@ -96,6 +96,7 @@ ...@@ -96,6 +96,7 @@
- [phabricator_import_import_tasks, 1] - [phabricator_import_import_tasks, 1]
- [update_namespace_statistics, 1] - [update_namespace_statistics, 1]
- [chaos, 2] - [chaos, 2]
- [create_evidence, 2]
# EE-specific queues # EE-specific queues
- [ldap_group_sync, 2] - [ldap_group_sync, 2]
......
# frozen_string_literal: true
class CreateEvidences < ActiveRecord::Migration[5.2]
DOWNTIME = false
def change
create_table :evidences do |t|
t.references :release, foreign_key: { on_delete: :cascade }, null: false
t.timestamps_with_timezone
t.binary :summary_sha
t.jsonb :summary, null: false, default: {}
end
end
end
...@@ -1424,6 +1424,15 @@ ActiveRecord::Schema.define(version: 2019_10_16_072826) do ...@@ -1424,6 +1424,15 @@ ActiveRecord::Schema.define(version: 2019_10_16_072826) do
t.index ["target_type", "target_id"], name: "index_events_on_target_type_and_target_id" t.index ["target_type", "target_id"], name: "index_events_on_target_type_and_target_id"
end end
create_table "evidences", force: :cascade do |t|
t.bigint "release_id", null: false
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.binary "summary_sha"
t.jsonb "summary", default: {}, null: false
t.index ["release_id"], name: "index_evidences_on_release_id"
end
create_table "external_pull_requests", force: :cascade do |t| create_table "external_pull_requests", force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false t.datetime_with_timezone "updated_at", null: false
...@@ -4079,6 +4088,7 @@ ActiveRecord::Schema.define(version: 2019_10_16_072826) do ...@@ -4079,6 +4088,7 @@ ActiveRecord::Schema.define(version: 2019_10_16_072826) do
add_foreign_key "events", "namespaces", column: "group_id", name: "fk_61fbf6ca48", on_delete: :cascade add_foreign_key "events", "namespaces", column: "group_id", name: "fk_61fbf6ca48", on_delete: :cascade
add_foreign_key "events", "projects", on_delete: :cascade add_foreign_key "events", "projects", on_delete: :cascade
add_foreign_key "events", "users", column: "author_id", name: "fk_edfd187b6f", on_delete: :cascade add_foreign_key "events", "users", column: "author_id", name: "fk_edfd187b6f", on_delete: :cascade
add_foreign_key "evidences", "releases", on_delete: :cascade
add_foreign_key "external_pull_requests", "projects", on_delete: :cascade add_foreign_key "external_pull_requests", "projects", on_delete: :cascade
add_foreign_key "fork_network_members", "fork_networks", on_delete: :cascade add_foreign_key "fork_network_members", "fork_networks", on_delete: :cascade
add_foreign_key "fork_network_members", "projects", column: "forked_from_project_id", name: "fk_b01280dae4", on_delete: :nullify add_foreign_key "fork_network_members", "projects", column: "forked_from_project_id", name: "fk_b01280dae4", on_delete: :nullify
......
# frozen_string_literal: true
FactoryBot.define do
factory :evidence do
release
end
end
{ {
"type": "object", "type": "object",
"required": [ "required": [
"id", "release"
"name",
"email"
], ],
"properties": { "properties": {
"id": { "type": "integer" }, "release": { "$ref": "release.json" }
"name": { "type": "string" },
"email": { "type": "string" }
}, },
"additionalProperties": false "additionalProperties": false
} }
...@@ -14,13 +14,12 @@ ...@@ -14,13 +14,12 @@
"properties": { "properties": {
"id": { "type": "integer" }, "id": { "type": "integer" },
"title": { "type": "string" }, "title": { "type": "string" },
"description": { "type": "string" }, "description": { "type": ["string", "null"] },
"author": { "$ref": "author.json" },
"state": { "type": "string" }, "state": { "type": "string" },
"iid": { "type": "integer" }, "iid": { "type": "integer" },
"confidential": { "type": "boolean" }, "confidential": { "type": "boolean" },
"created_at": { "type": "date" }, "created_at": { "type": "date" },
"due_date": { "type": "date" } "due_date": { "type": ["date", "null"] }
}, },
"additionalProperties": false "additionalProperties": false
} }
...@@ -13,11 +13,11 @@ ...@@ -13,11 +13,11 @@
"properties": { "properties": {
"id": { "type": "integer" }, "id": { "type": "integer" },
"title": { "type": "string" }, "title": { "type": "string" },
"description": { "type": "string" }, "description": { "type": ["string", "null"] },
"state": { "type": "string" }, "state": { "type": "string" },
"iid": { "type": "integer" }, "iid": { "type": "integer" },
"created_at": { "type": "date" }, "created_at": { "type": "date" },
"due_date": { "type": "date" }, "due_date": { "type": ["date", "null"] },
"issues": { "issues": {
"type": "array", "type": "array",
"items": { "$ref": "issue.json" } "items": { "$ref": "issue.json" }
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
"properties": { "properties": {
"id": { "type": "integer" }, "id": { "type": "integer" },
"name": { "type": "string" }, "name": { "type": "string" },
"description": { "type": "string" }, "description": { "type": ["string", "null"] },
"created_at": { "type": "date" } "created_at": { "type": "date" }
}, },
"additionalProperties": false "additionalProperties": false
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
"type": "object", "type": "object",
"required": [ "required": [
"id", "id",
"tag", "tag_name",
"name", "name",
"description", "description",
"created_at", "created_at",
...@@ -11,8 +11,8 @@ ...@@ -11,8 +11,8 @@
], ],
"properties": { "properties": {
"id": { "type": "integer" }, "id": { "type": "integer" },
"tag": { "type": "string" }, "tag_name": { "type": "string" },
"name": { "type": "string" }, "name": { "type": ["string", "null"] },
"description": { "type": "string" }, "description": { "type": "string" },
"created_at": { "type": "date" }, "created_at": { "type": "date" },
"project": { "$ref": "project.json" }, "project": { "$ref": "project.json" },
......
...@@ -81,6 +81,7 @@ releases: ...@@ -81,6 +81,7 @@ releases:
- links - links
- milestone_releases - milestone_releases
- milestones - milestones
- evidence
links: links:
- release - release
project_members: project_members:
...@@ -506,6 +507,8 @@ lists: ...@@ -506,6 +507,8 @@ lists:
milestone_releases: milestone_releases:
- milestone - milestone
- release - release
evidences:
- release
design: &design design: &design
- issue - issue
- actions - actions
......
...@@ -127,6 +127,12 @@ Release: ...@@ -127,6 +127,12 @@ Release:
- created_at - created_at
- updated_at - updated_at
- released_at - released_at
Evidence:
- id
- release_id
- summary
- created_at
- updated_at
Releases::Link: Releases::Link:
- id - id
- release_id - release_id
......
# frozen_string_literal: true
require 'spec_helper'
describe Evidence do
let_it_be(:project) { create(:project) }
let(:release) { create(:release, project: project) }
let(:schema_file) { 'evidences/evidence' }
let(:summary_json) { described_class.last.summary.to_json }
describe 'associations' do
it { is_expected.to belong_to(:release) }
end
describe 'summary_sha' do
it 'returns nil if summary is nil' do
expect(build(:evidence, summary: nil).summary_sha).to be_nil
end
end
describe '#generate_summary_and_sha' do
before do
described_class.create!(release: release)
end
context 'when a release name is not provided' do
let(:release) { create(:release, project: project, name: nil) }
it 'creates a valid JSON object' do
expect(release.name).to be_nil
expect(summary_json).to match_schema(schema_file)
end
end
context 'when a release is associated to a milestone' do
let(:milestone) { create(:milestone, project: project) }
let(:release) { create(:release, project: project, milestones: [milestone]) }
context 'when a milestone has no issue associated with it' do
it 'creates a valid JSON object' do
expect(milestone.issues).to be_empty
expect(summary_json).to match_schema(schema_file)
end
end
context 'when a milestone has no description' do
let(:milestone) { create(:milestone, project: project, description: nil) }
it 'creates a valid JSON object' do
expect(milestone.description).to be_nil
expect(summary_json).to match_schema(schema_file)
end
end
context 'when a milestone has no due_date' do
let(:milestone) { create(:milestone, project: project, due_date: nil) }
it 'creates a valid JSON object' do
expect(milestone.due_date).to be_nil
expect(summary_json).to match_schema(schema_file)
end
end
context 'when a milestone has an issue' do
context 'when the issue has no description' do
let(:issue) { create(:issue, project: project, description: nil, state: 'closed') }
before do
milestone.issues << issue
end
it 'creates a valid JSON object' do
expect(milestone.issues.first.description).to be_nil
expect(summary_json).to match_schema(schema_file)
end
end
end
end
context 'when a release is not associated to any milestone' do
it 'creates a valid JSON object' do
expect(release.milestones).to be_empty
expect(summary_json).to match_schema(schema_file)
end
end
end
end
...@@ -15,11 +15,13 @@ RSpec.describe Release do ...@@ -15,11 +15,13 @@ RSpec.describe Release do
it { is_expected.to have_many(:links).class_name('Releases::Link') } it { is_expected.to have_many(:links).class_name('Releases::Link') }
it { is_expected.to have_many(:milestones) } it { is_expected.to have_many(:milestones) }
it { is_expected.to have_many(:milestone_releases) } it { is_expected.to have_many(:milestone_releases) }
it { is_expected.to have_one(:evidence) }
end end
describe 'validation' do describe 'validation' do
it { is_expected.to validate_presence_of(:project) } it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:description) } it { is_expected.to validate_presence_of(:description) }
it { is_expected.to validate_presence_of(:tag) }
context 'when a release exists in the database without a name' do context 'when a release exists in the database without a name' do
it 'does not require name' do it 'does not require name' do
...@@ -89,4 +91,22 @@ RSpec.describe Release do ...@@ -89,4 +91,22 @@ RSpec.describe Release do
end end
end end
end end
describe 'evidence' do
describe '#create_evidence!' do
context 'when a release is created' do
it 'creates one Evidence object too' do
expect { release }.to change(Evidence, :count).by(1)
end
end
end
context 'when a release is deleted' do
it 'also deletes the associated evidence' do
release = create(:release)
expect { release.destroy }.to change(Evidence, :count).by(-1)
end
end
end
end end
...@@ -2,12 +2,13 @@ ...@@ -2,12 +2,13 @@
require 'spec_helper' require 'spec_helper'
describe Evidences::AuthorEntity do describe Evidences::EvidenceEntity do
let(:entity) { described_class.new(build(:author)) } let(:evidence) { build(:evidence) }
let(:entity) { described_class.new(evidence) }
subject { entity.as_json } subject { entity.as_json }
it 'exposes the expected fields' do it 'exposes the expected fields' do
expect(subject.keys).to contain_exactly(:id, :name, :email) expect(subject.keys).to contain_exactly(:release)
end end
end end
# frozen_string_literal: true
require 'spec_helper'
describe Evidences::EvidenceSerializer do
it 'represents an EvidenceEntity entity' do
expect(described_class.entity_class).to eq(Evidences::EvidenceEntity)
end
end
...@@ -8,6 +8,6 @@ describe Evidences::IssueEntity do ...@@ -8,6 +8,6 @@ describe Evidences::IssueEntity do
subject { entity.as_json } subject { entity.as_json }
it 'exposes the expected fields' do it 'exposes the expected fields' do
expect(subject.keys).to contain_exactly(:id, :title, :description, :author, :state, :iid, :confidential, :created_at, :due_date) expect(subject.keys).to contain_exactly(:id, :title, :description, :state, :iid, :confidential, :created_at, :due_date)
end end
end end
...@@ -12,7 +12,7 @@ describe Evidences::MilestoneEntity do ...@@ -12,7 +12,7 @@ describe Evidences::MilestoneEntity do
expect(subject.keys).to contain_exactly(:id, :title, :description, :state, :iid, :created_at, :due_date, :issues) expect(subject.keys).to contain_exactly(:id, :title, :description, :state, :iid, :created_at, :due_date, :issues)
end end
context 'when there issues linked to this milestone' do context 'when there are issues linked to this milestone' do
let(:issue_1) { build(:issue) } let(:issue_1) { build(:issue) }
let(:issue_2) { build(:issue) } let(:issue_2) { build(:issue) }
let(:milestone) { build(:milestone, issues: [issue_1, issue_2]) } let(:milestone) { build(:milestone, issues: [issue_1, issue_2]) }
......
# frozen_string_literal: true
shared_examples 'updated exposed field' do
it 'creates another Evidence object' do
model.send("#{updated_field}=", updated_value)
expect(model.evidence_summary_keys).to include(updated_field)
expect { model.save! }.to change(Evidence, :count).by(1)
expect(updated_json_field).to eq(updated_value)
end
end
shared_examples 'updated non-exposed field' do
it 'does not create any Evidence object' do
model.send("#{updated_field}=", updated_value)
expect(model.evidence_summary_keys).not_to include(updated_field)
expect { model.save! }.not_to change(Evidence, :count)
end
end
shared_examples 'updated field on non-linked entity' do
it 'does not create any Evidence object' do
model.send("#{updated_field}=", updated_value)
expect(model.evidence_summary_keys).to be_empty
expect { model.save! }.not_to change(Evidence, :count)
end
end
# frozen_string_literal: true
require 'spec_helper'
describe CreateEvidenceWorker do
let!(:release) { create(:release) }
it 'creates a new Evidence' do
expect { described_class.new.perform(release.id) }.to change(Evidence, :count).by(1)
end
end
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