From 8af3ded67da2dade5e090bdf471ca55df59893a0 Mon Sep 17 00:00:00 2001 From: Laura Montemayor <lmontemayor@gitlab.com> Date: Thu, 15 Feb 2024 17:13:21 +0000 Subject: [PATCH] Add semantic version validation for catalog versions This MR sets up validation for semantic versioning when releasing a new vesion of the catalog resource. * Add migration for adding columns to support parsing of SemVer * Updates the Versions::CreateService to use `release.tag` for versioning * Updates specs that were not using semantic version, thus testing the semantic version validation further * Quarantines an out of date feature spec Changelog: added --- app/models/ci/catalog/resources/version.rb | 6 ++-- .../resources/versions/create_service.rb | 3 +- ...dd_sem_ver_to_catalog_resources_version.rb | 16 ++++++++++ ...log_resource_versions_semver_prerelease.rb | 15 +++++++++ db/schema_migrations/20240207115842 | 1 + db/schema_migrations/20240213113719 | 1 + db/structure.sql | 7 ++++- doc/ci/components/index.md | 2 +- doc/development/semver.md | 3 +- .../ci/catalog/resources/versions.rb | 2 ++ .../explore/catalog/catalog_releases_spec.rb | 2 +- .../catalog/resources/versions_finder_spec.rb | 4 +-- .../resources/versions_resolver_spec.rb | 4 +-- .../ci/catalog/resources/version_spec.rb | 31 ++++++++++++++++--- .../catalog/resources/release_service_spec.rb | 2 +- .../resources/versions/create_service_spec.rb | 4 +-- spec/services/releases/create_service_spec.rb | 2 +- .../resources/version_shared_context.rb | 24 +++++++------- 18 files changed, 98 insertions(+), 31 deletions(-) create mode 100644 db/migrate/20240207115842_add_sem_ver_to_catalog_resources_version.rb create mode 100644 db/migrate/20240213113719_add_text_limit_to_catalog_resource_versions_semver_prerelease.rb create mode 100644 db/schema_migrations/20240207115842 create mode 100644 db/schema_migrations/20240213113719 diff --git a/app/models/ci/catalog/resources/version.rb b/app/models/ci/catalog/resources/version.rb index 0d27bdecb63e2..c78adbafc95d5 100644 --- a/app/models/ci/catalog/resources/version.rb +++ b/app/models/ci/catalog/resources/version.rb @@ -7,6 +7,10 @@ module Resources # Only versions which contain valid CI components are included in this table. class Version < ::ApplicationRecord include BulkInsertableAssociations + include SemanticVersionable + + semver_method :version + validate_semver self.table_name = 'catalog_resource_versions' @@ -33,8 +37,6 @@ class Version < ::ApplicationRecord after_save :update_catalog_resource class << self - # In the future, we should support semantic versioning. - # See https://gitlab.com/gitlab-org/gitlab/-/issues/427286 def latest order_by_released_at_desc.first end diff --git a/app/services/ci/catalog/resources/versions/create_service.rb b/app/services/ci/catalog/resources/versions/create_service.rb index 9547db7bcf169..7d05a3085c11d 100644 --- a/app/services/ci/catalog/resources/versions/create_service.rb +++ b/app/services/ci/catalog/resources/versions/create_service.rb @@ -35,7 +35,8 @@ def build_catalog_resource_version @version = Ci::Catalog::Resources::Version.new( release: release, catalog_resource: project.catalog_resource, - project: project + project: project, + version: release.tag ) end diff --git a/db/migrate/20240207115842_add_sem_ver_to_catalog_resources_version.rb b/db/migrate/20240207115842_add_sem_ver_to_catalog_resources_version.rb new file mode 100644 index 0000000000000..b8a3aa5cb424d --- /dev/null +++ b/db/migrate/20240207115842_add_sem_ver_to_catalog_resources_version.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class AddSemVerToCatalogResourcesVersion < Gitlab::Database::Migration[2.2] + enable_lock_retries! + + milestone '16.10' + # rubocop:disable Migration/AddLimitToTextColumns -- limit is added in 20240213113719_add_text_limit_to_catalog_resource_versions_semver_prerelease + + def change + add_column :catalog_resource_versions, :semver_major, :integer + add_column :catalog_resource_versions, :semver_minor, :integer + add_column :catalog_resource_versions, :semver_patch, :integer + add_column :catalog_resource_versions, :semver_prerelease, :text + end + # rubocop:enable Migration/AddLimitToTextColumns +end diff --git a/db/migrate/20240213113719_add_text_limit_to_catalog_resource_versions_semver_prerelease.rb b/db/migrate/20240213113719_add_text_limit_to_catalog_resource_versions_semver_prerelease.rb new file mode 100644 index 0000000000000..452208c1b26a9 --- /dev/null +++ b/db/migrate/20240213113719_add_text_limit_to_catalog_resource_versions_semver_prerelease.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddTextLimitToCatalogResourceVersionsSemverPrerelease < Gitlab::Database::Migration[2.2] + disable_ddl_transaction! + + milestone '16.10' + + def up + add_text_limit :catalog_resource_versions, :semver_prerelease, 255 + end + + def down + remove_text_limit :catalog_resource_versions, :semver_prerelease + end +end diff --git a/db/schema_migrations/20240207115842 b/db/schema_migrations/20240207115842 new file mode 100644 index 0000000000000..e1d8361c38d42 --- /dev/null +++ b/db/schema_migrations/20240207115842 @@ -0,0 +1 @@ +70164c8c55ac94314a73074b04ec1fc1ad4aaed199347f22904e6691aee870d3 \ No newline at end of file diff --git a/db/schema_migrations/20240213113719 b/db/schema_migrations/20240213113719 new file mode 100644 index 0000000000000..6779bd9a4277a --- /dev/null +++ b/db/schema_migrations/20240213113719 @@ -0,0 +1 @@ +07e1a3a02552425f4a5345d9ed3eb7da7f4f09b9f6c9071b1527a1a0e5e3fd10 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index fd9027155d6a8..71e9cfa36abc4 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -5512,7 +5512,12 @@ CREATE TABLE catalog_resource_versions ( catalog_resource_id bigint NOT NULL, project_id bigint NOT NULL, created_at timestamp with time zone NOT NULL, - released_at timestamp with time zone DEFAULT '1970-01-01 00:00:00+00'::timestamp with time zone NOT NULL + released_at timestamp with time zone DEFAULT '1970-01-01 00:00:00+00'::timestamp with time zone NOT NULL, + semver_major integer, + semver_minor integer, + semver_patch integer, + semver_prerelease text, + CONSTRAINT check_701bdce47b CHECK ((char_length(semver_prerelease) <= 255)) ); CREATE SEQUENCE catalog_resource_versions_id_seq diff --git a/doc/ci/components/index.md b/doc/ci/components/index.md index 3980077cbc619..c791a4edb3508 100644 --- a/doc/ci/components/index.md +++ b/doc/ci/components/index.md @@ -260,7 +260,7 @@ To publish a new version of the component to the catalog: running the release job. After the release job completes successfully, the release is created and the new version -is published to the CI/CD catalog. +is published to the CI/CD catalog. Tags must use semantic versioning, for example `1.0.0`. ### Unpublish a component project diff --git a/doc/development/semver.md b/doc/development/semver.md index cd7aac5c5d7f1..f9a9c625efc1c 100644 --- a/doc/development/semver.md +++ b/doc/development/semver.md @@ -14,7 +14,8 @@ In order to use SemanticVersionable you must first create a database migration t ```ruby class AddVersionPartsToModelVersions < Gitlab::Database::Migration[2.2] - disable_ddl_transaction! + enable_lock_retries! + milestone '16.9' def up diff --git a/spec/factories/ci/catalog/resources/versions.rb b/spec/factories/ci/catalog/resources/versions.rb index 520708d9d585d..34fc7fe38b4e2 100644 --- a/spec/factories/ci/catalog/resources/versions.rb +++ b/spec/factories/ci/catalog/resources/versions.rb @@ -2,6 +2,8 @@ FactoryBot.define do factory :ci_catalog_resource_version, class: 'Ci::Catalog::Resources::Version' do + version { '1.0.0' } + catalog_resource factory: :ci_catalog_resource project { catalog_resource.project } release { association :release, project: project } diff --git a/spec/features/explore/catalog/catalog_releases_spec.rb b/spec/features/explore/catalog/catalog_releases_spec.rb index 27b7aa17551e1..427de1d263327 100644 --- a/spec/features/explore/catalog/catalog_releases_spec.rb +++ b/spec/features/explore/catalog/catalog_releases_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'CI/CD Catalog releases', :js, feature_category: :pipeline_composition do +RSpec.describe 'CI/CD Catalog releases', :js, feature_category: :pipeline_composition, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/432824' do let_it_be(:tag_name) { 'catalog_release_tag' } let_it_be(:user) { create(:user) } let_it_be_with_reload(:namespace) { create(:group) } diff --git a/spec/finders/ci/catalog/resources/versions_finder_spec.rb b/spec/finders/ci/catalog/resources/versions_finder_spec.rb index dbde77101eecf..48df50caad425 100644 --- a/spec/finders/ci/catalog/resources/versions_finder_spec.rb +++ b/spec/finders/ci/catalog/resources/versions_finder_spec.rb @@ -39,11 +39,11 @@ end context 'with name parameter' do - let(:name) { 'v1.0' } + let(:name) { '1.0.0' } it 'returns the version that matches the name' do expect(execute.count).to eq(1) - expect(execute.first.name).to eq('v1.0') + expect(execute.first.name).to eq('1.0.0') end context 'when no version matches the name' do diff --git a/spec/graphql/resolvers/ci/catalog/resources/versions_resolver_spec.rb b/spec/graphql/resolvers/ci/catalog/resources/versions_resolver_spec.rb index 4205259e5b9e5..3f15560553d89 100644 --- a/spec/graphql/resolvers/ci/catalog/resources/versions_resolver_spec.rb +++ b/spec/graphql/resolvers/ci/catalog/resources/versions_resolver_spec.rb @@ -21,11 +21,11 @@ end context 'when name argument is provided' do - let(:name) { 'v1.0' } + let(:name) { '1.0.0' } it 'returns the version that matches the name' do expect(result.items.size).to eq(1) - expect(result.items.first.name).to eq('v1.0') + expect(result.items.first.name).to eq('1.0.0') end context 'when no version matches the name' do diff --git a/spec/models/ci/catalog/resources/version_spec.rb b/spec/models/ci/catalog/resources/version_spec.rb index 64957b2363876..fb2a10d86bc6e 100644 --- a/spec/models/ci/catalog/resources/version_spec.rb +++ b/spec/models/ci/catalog/resources/version_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe Ci::Catalog::Resources::Version, type: :model, feature_category: :pipeline_composition do + using RSpec::Parameterized::TableSyntax + include_context 'when there are catalog resources with versions' it { is_expected.to belong_to(:release) } @@ -17,6 +19,27 @@ it { is_expected.to validate_presence_of(:release) } it { is_expected.to validate_presence_of(:catalog_resource) } it { is_expected.to validate_presence_of(:project) } + + describe 'semver validation' do + where(:version, :valid, :semver_major, :semver_minor, :semver_patch, :semver_prerelease) do + '1' | false | nil | nil | nil | nil + '1.2' | false | nil | nil | nil | nil + '1.2.3' | true | 1 | 2 | 3 | nil + '1.2.3-beta' | true | 1 | 2 | 3 | 'beta' + '1.2.3.beta' | false | nil | nil | nil | nil + end + + with_them do + let(:catalog_version) { build(:ci_catalog_resource_version, version: version) } + + it do + expect(catalog_version.semver_major).to be semver_major + expect(catalog_version.semver_minor).to be semver_minor + expect(catalog_version.semver_patch).to be semver_patch + expect(catalog_version.semver_prerelease).to eq semver_prerelease + end + end + end end describe '.for_catalog resources' do @@ -29,10 +52,10 @@ describe '.by_name' do it 'returns the version that matches the name' do - versions = described_class.by_name('v1.0') + versions = described_class.by_name('1.0.0') expect(versions.count).to eq(1) - expect(versions.first.name).to eq('v1.0') + expect(versions.first.name).to eq('1.0.0') end context 'when no version matches the name' do @@ -144,8 +167,8 @@ describe '#readme' do it 'returns the correct readme for the version' do - expect(v1_0.readme.data).to include('Readme v1.0') - expect(v1_1.readme.data).to include('Readme v1.1') + expect(v1_0.readme.data).to include('Readme 1.0.0') + expect(v1_1.readme.data).to include('Readme 1.1.0') end end diff --git a/spec/services/ci/catalog/resources/release_service_spec.rb b/spec/services/ci/catalog/resources/release_service_spec.rb index 60cd6cb5f96f3..790ec971e29c3 100644 --- a/spec/services/ci/catalog/resources/release_service_spec.rb +++ b/spec/services/ci/catalog/resources/release_service_spec.rb @@ -8,7 +8,7 @@ it 'validates the catalog resource and creates a version' do project = create(:project, :catalog_resource_with_components) catalog_resource = create(:ci_catalog_resource, project: project) - release = create(:release, project: project, sha: project.repository.root_ref_sha) + release = create(:release, project: project, sha: project.repository.root_ref_sha, tag: '1.0.0') response = described_class.new(release).execute diff --git a/spec/services/ci/catalog/resources/versions/create_service_spec.rb b/spec/services/ci/catalog/resources/versions/create_service_spec.rb index c92b317885a22..2ad091942859e 100644 --- a/spec/services/ci/catalog/resources/versions/create_service_spec.rb +++ b/spec/services/ci/catalog/resources/versions/create_service_spec.rb @@ -24,13 +24,13 @@ ) end - let(:release) { create(:release, project: project, sha: project.repository.root_ref_sha) } + let(:release) { create(:release, tag: '1.2.0', project: project, sha: project.repository.root_ref_sha) } let!(:catalog_resource) { create(:ci_catalog_resource, project: project) } context 'when the project is not a catalog resource' do it 'does not create a version' do project = create(:project, :repository) - release = create(:release, project: project, sha: project.repository.root_ref_sha) + release = create(:release, tag: '1.2.1', project: project, sha: project.repository.root_ref_sha) response = described_class.new(release).execute diff --git a/spec/services/releases/create_service_spec.rb b/spec/services/releases/create_service_spec.rb index 3504f00412c78..77bd0a2138dbe 100644 --- a/spec/services/releases/create_service_spec.rb +++ b/spec/services/releases/create_service_spec.rb @@ -56,7 +56,7 @@ end context 'when project is a catalog resource' do - let(:project) { create(:project, :catalog_resource_with_components, create_tag: 'final') } + let_it_be(:project) { create(:project, :catalog_resource_with_components, create_tag: '6.0.0') } let!(:ci_catalog_resource) { create(:ci_catalog_resource, project: project) } let(:ref) { 'master' } diff --git a/spec/support/shared_contexts/ci/catalog/resources/version_shared_context.rb b/spec/support/shared_contexts/ci/catalog/resources/version_shared_context.rb index a451608a5ccd1..b88675168ae57 100644 --- a/spec/support/shared_contexts/ci/catalog/resources/version_shared_context.rb +++ b/spec/support/shared_contexts/ci/catalog/resources/version_shared_context.rb @@ -6,33 +6,33 @@ RSpec.shared_context 'when there are catalog resources with versions' do let_it_be(:current_user) { create(:user) } - let_it_be(:project1) { create(:project, :custom_repo, files: { 'README.md' => 'Readme v1.0' }) } + let_it_be(:project1) { create(:project, :custom_repo, files: { 'README.md' => 'Readme 1.0.0' }) } let_it_be(:project2) { create(:project, :repository) } let_it_be_with_reload(:resource1) { create(:ci_catalog_resource, project: project1) } let_it_be_with_reload(:resource2) { create(:ci_catalog_resource, project: project2) } - let(:v1_0) { resource1.versions.by_name('v1.0').first } - let(:v1_1) { resource1.versions.by_name('v1.1').first } - let(:v2_0) { resource2.versions.by_name('v2.0').first } - let(:v2_1) { resource2.versions.by_name('v2.1').first } + let(:v1_0) { resource1.versions.by_name('1.0.0').first } + let(:v1_1) { resource1.versions.by_name('1.1.0').first } + let(:v2_0) { resource2.versions.by_name('2.0.0').first } + let(:v2_1) { resource2.versions.by_name('2.1.0').first } before_all do project1.repository.create_branch('branch_v1.1', project1.default_branch) project1.repository.update_file( - current_user, 'README.md', 'Readme v1.1', message: 'Update readme', branch_name: 'branch_v1.1') + current_user, 'README.md', 'Readme 1.1.0', message: 'Update readme', branch_name: 'branch_v1.1') - tag_v1_0 = project1.repository.add_tag(current_user, 'v1.0', project1.default_branch) - tag_v1_1 = project1.repository.add_tag(current_user, 'v1.1', 'branch_v1.1') + tag_v1_0 = project1.repository.add_tag(current_user, '1.0.0', project1.default_branch) + tag_v1_1 = project1.repository.add_tag(current_user, '1.1.0', 'branch_v1.1') - release_v1_0 = create(:release, project: project1, tag: 'v1.0', released_at: 4.days.ago, + release_v1_0 = create(:release, project: project1, tag: '1.0.0', released_at: 4.days.ago, sha: tag_v1_0.dereferenced_target.sha) - release_v1_1 = create(:release, project: project1, tag: 'v1.1', released_at: 3.days.ago, + release_v1_1 = create(:release, project: project1, tag: '1.1.0', released_at: 3.days.ago, sha: tag_v1_1.dereferenced_target.sha) - release_v2_0 = create(:release, project: project2, tag: 'v2.0', released_at: 2.days.ago) - release_v2_1 = create(:release, project: project2, tag: 'v2.1', released_at: 1.day.ago) + release_v2_0 = create(:release, project: project2, tag: '2.0.0', released_at: 2.days.ago) + release_v2_1 = create(:release, project: project2, tag: '2.1.0', released_at: 1.day.ago) create(:ci_catalog_resource_version, catalog_resource: resource1, release: release_v1_0, created_at: 1.day.ago) create(:ci_catalog_resource_version, catalog_resource: resource1, release: release_v1_1, created_at: 2.days.ago) -- GitLab