From 8f06ee105d2ddbc7e1ae76ba1597f5cd6f486668 Mon Sep 17 00:00:00 2001 From: Furkan Ayhan Date: Wed, 15 Oct 2025 15:36:41 +0200 Subject: [PATCH 1/2] Implement version component context interpolation This is the third step of adding component context to CI interpolation. In the first step, we prepared the codebase by refactoring YAML context passing. In the second step, we added `component.name` and `component.sha` to interpolation. In this step, we're adding `component.version`. The version interpolation resolves to the actual semantic version tag when using shorthand versions (like `@1.0` resolves to `1.0.2`) or `~latest` (resolves to the latest semver tag). When using a direct SHA, branch name, or non-catalog-resource project, the version returns nil. This allows component authors to reference the same version in their templates as users specify when including the component, enabling use cases like matching container image tags with component versions. --- app/services/ci/components/fetch_service.rb | 3 +- .../catalog_resource_component_spec.json | 12 ++++ lib/gitlab/ci/components/instance_path.rb | 23 +++++- .../ci/config/external/file/component.rb | 3 +- lib/gitlab/ci/config/header/component.rb | 2 +- .../ci/components/instance_path_spec.rb | 68 ++++++++++++++++++ .../gitlab/ci/config/external/context_spec.rb | 2 +- .../ci/config/external/file/component_spec.rb | 9 ++- .../gitlab/ci/config/header/component_spec.rb | 16 ++--- spec/lib/gitlab/ci/config/header/root_spec.rb | 8 +-- spec/lib/gitlab/ci/config/header/spec_spec.rb | 8 +-- .../config/interpolation/interpolator_spec.rb | 14 ++-- .../lib/gitlab/ci/config/yaml/context_spec.rb | 4 +- spec/lib/gitlab/ci/config_spec.rb | 6 +- .../ci/components/fetch_service_spec.rb | 30 ++++---- .../component_interpolation_spec.rb | 72 ++++++++++++++++++- 16 files changed, 229 insertions(+), 51 deletions(-) diff --git a/app/services/ci/components/fetch_service.rb b/app/services/ci/components/fetch_service.rb index 3f793065d7dff9..fe734ab59d52db 100644 --- a/app/services/ci/components/fetch_service.rb +++ b/app/services/ci/components/fetch_service.rb @@ -31,7 +31,8 @@ def execute path: result.path, project: component_path.project, sha: component_path.sha, - name: component_path.component_name + name: component_path.component_name, + version: component_path.matched_version }) elsif component_path.invalid_usage_for_latest? ServiceResponse.error( diff --git a/app/validators/json_schemas/catalog_resource_component_spec.json b/app/validators/json_schemas/catalog_resource_component_spec.json index 62878edc133af3..b48a93a4521194 100644 --- a/app/validators/json_schemas/catalog_resource_component_spec.json +++ b/app/validators/json_schemas/catalog_resource_component_spec.json @@ -28,6 +28,18 @@ } } } + }, + "component": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "name", + "sha", + "version" + ] + }, + "uniqueItems": true } }, "additionalProperties": false diff --git a/lib/gitlab/ci/components/instance_path.rb b/lib/gitlab/ci/components/instance_path.rb index f598fdb673f104..5aac37605783a8 100644 --- a/lib/gitlab/ci/components/instance_path.rb +++ b/lib/gitlab/ci/components/instance_path.rb @@ -46,6 +46,11 @@ def sha end strong_memoize_attr :sha + def matched_version + find_matched_version(version)&.semver&.to_s + end + strong_memoize_attr :matched_version + def invalid_usage_for_latest? @version == LATEST && project && project.catalog_resource.nil? end @@ -64,6 +69,18 @@ def find_version_sha(version) sha_by_shorthand_semver(version) || sha_by_released_tag(version) || sha_by_ref(version) end + def find_matched_version(version) + return unless project.catalog_resource + + if version == LATEST + project.catalog_resource.versions.latest + elsif version.match?(SHORTHAND_SEMVER_PATTERN) + catalog_resource_version_by_short_semver(version) + else + project.catalog_resource.versions.by_name(version).first + end + end + def find_latest_sha return unless project.catalog_resource @@ -74,8 +91,12 @@ def sha_by_shorthand_semver(version) return unless version.match?(SHORTHAND_SEMVER_PATTERN) return unless project.catalog_resource + catalog_resource_version_by_short_semver(version)&.sha + end + + def catalog_resource_version_by_short_semver(version) major, minor = version.split(".") - project.catalog_resource.versions.latest(major, minor)&.sha + project.catalog_resource.versions.latest(major, minor) end def sha_by_released_tag(version) diff --git a/lib/gitlab/ci/config/external/file/component.rb b/lib/gitlab/ci/config/external/file/component.rb index 58cd017e79ddd4..77b835fdd27399 100644 --- a/lib/gitlab/ci/config/external/file/component.rb +++ b/lib/gitlab/ci/config/external/file/component.rb @@ -117,7 +117,8 @@ def component_attrs { project: component_payload.fetch(:project), sha: component_payload.fetch(:sha), - name: component_payload.fetch(:name) + name: component_payload.fetch(:name), + version: component_payload.fetch(:version) } end diff --git a/lib/gitlab/ci/config/header/component.rb b/lib/gitlab/ci/config/header/component.rb index 96ddbd750762ff..debbc5af750012 100644 --- a/lib/gitlab/ci/config/header/component.rb +++ b/lib/gitlab/ci/config/header/component.rb @@ -13,7 +13,7 @@ module Header class Component < ::Gitlab::Config::Entry::Node include ::Gitlab::Config::Entry::Validatable - ALLOWED_VALUES = %i[name sha].freeze + ALLOWED_VALUES = %i[name sha version].freeze ALLOWED_VALUES_TO_S = ALLOWED_VALUES.map(&:to_s).freeze validations do diff --git a/spec/lib/gitlab/ci/components/instance_path_spec.rb b/spec/lib/gitlab/ci/components/instance_path_spec.rb index 7ff045929f7ca5..0f5515e2fc7c63 100644 --- a/spec/lib/gitlab/ci/components/instance_path_spec.rb +++ b/spec/lib/gitlab/ci/components/instance_path_spec.rb @@ -297,6 +297,74 @@ end end + describe '#matched_version' do + let_it_be(:project) do + create( + :project, :custom_repo, + files: { + 'templates/secret-detection.yml' => 'image: alpine_1', + 'templates/dast/template.yml' => 'image: alpine_2', + 'templates/dast/another-template.yml' => 'image: alpine_3', + 'templates/dast/another-folder/template.yml' => 'image: alpine_4' + } + ) + end + + let_it_be(:catalog_resource) { create(:ci_catalog_resource, :published, project: project) } + let_it_be(:commit) { project.repository.commit } + let_it_be(:project_path) { project.full_path } + + before_all do + project.add_maintainer(user) + + create( + :release, :with_catalog_resource_version, + project: project, tag: '0.1.0', author: user, sha: commit.id + ) + + create( + :release, :with_catalog_resource_version, + project: project, tag: '0.1.2', author: user, sha: commit.id + ) + end + + subject(:matched_version) { path.matched_version } + + context 'when using a full semantic version' do + let(:address) { "acme.com/#{project_path}/secret-detection@0.1.0" } + + it { is_expected.to eq '0.1.0' } + end + + context 'when using a partial semantic version' do + let(:address) { "acme.com/#{project_path}/secret-detection@0.1" } + + it { is_expected.to eq '0.1.2' } + end + + context 'when using ~latest' do + let(:address) { "acme.com/#{project_path}/secret-detection@~latest" } + + it { is_expected.to eq '0.1.2' } + end + + context 'when using a SHA' do + let(:address) { "acme.com/#{project_path}/secret-detection@#{commit.id}" } + + it { is_expected.to be_nil } + end + + context 'when project is not a catalog resource' do + let(:address) { "acme.com/#{project_path}/secret-detection@0.1.0" } + + before do + catalog_resource.destroy! + end + + it { is_expected.to be_nil } + end + end + describe '#invalid_usage_for_latest?' do let_it_be(:project) { create(:project) } let(:project_path) { project.full_path } diff --git a/spec/lib/gitlab/ci/config/external/context_spec.rb b/spec/lib/gitlab/ci/config/external/context_spec.rb index 1ad7febaca81e7..c197901282ddc4 100644 --- a/spec/lib/gitlab/ci/config/external/context_spec.rb +++ b/spec/lib/gitlab/ci/config/external/context_spec.rb @@ -47,7 +47,7 @@ end context 'with component_data' do - let(:component_data) { { name: 'my-component', sha: 'abc123' } } + let(:component_data) { { name: 'my-component', sha: 'abc123', version: '1.0.0' } } let(:attributes) do { project: project, diff --git a/spec/lib/gitlab/ci/config/external/file/component_spec.rb b/spec/lib/gitlab/ci/config/external/file/component_spec.rb index 075bd7665b580c..3ba1ec0289ef2e 100644 --- a/spec/lib/gitlab/ci/config/external/file/component_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/component_spec.rb @@ -35,7 +35,8 @@ path: 'templates/my_component.yml', project: project, sha: 'my_component_sha', - name: 'my_component' + name: 'my_component', + version: nil }) end @@ -170,7 +171,8 @@ component: { project: project, sha: 'my_component_sha', - name: 'my_component' + name: 'my_component', + version: nil } ) end @@ -187,7 +189,8 @@ variables: context.variables, component_data: { name: 'my_component', - sha: 'my_component_sha' + sha: 'my_component_sha', + version: nil } ) end diff --git a/spec/lib/gitlab/ci/config/header/component_spec.rb b/spec/lib/gitlab/ci/config/header/component_spec.rb index 927e1f23d17817..2e890a2d9a5ec5 100644 --- a/spec/lib/gitlab/ci/config/header/component_spec.rb +++ b/spec/lib/gitlab/ci/config/header/component_spec.rb @@ -7,7 +7,7 @@ describe 'validations' do context 'when config is an array of valid strings' do - let(:config) { %w[name sha] } + let(:config) { %w[name sha version] } it 'passes validations' do expect(component).to be_valid @@ -25,7 +25,7 @@ end context 'when config contains invalid values' do - let(:config) { %w[name invalid_key] } + let(:config) { %w[name invalid_key version] } it 'fails validations' do expect(component).not_to be_valid @@ -52,7 +52,7 @@ end context 'when config contains non-string values' do - let(:config) { ['name', 123] } + let(:config) { ['name', 123, 'version'] } it 'fails validations' do expect(component).not_to be_valid @@ -65,19 +65,19 @@ subject(:value) { component.value } context 'when config is valid' do - let(:config) { %w[name sha] } + let(:config) { %w[name sha version] } - it { is_expected.to match_array([:name, :sha]) } + it { is_expected.to match_array([:name, :sha, :version]) } end context 'when config has duplicate values' do - let(:config) { %w[name name sha] } + let(:config) { %w[name version name sha] } - it { is_expected.to match_array([:name, :sha]) } + it { is_expected.to match_array([:name, :version, :sha]) } end context 'when config has invalid values' do - let(:config) { %w[name invalid] } + let(:config) { %w[name version invalid] } it { is_expected.to be_empty } end diff --git a/spec/lib/gitlab/ci/config/header/root_spec.rb b/spec/lib/gitlab/ci/config/header/root_spec.rb index 2fa9ab98d1385e..7b7d7d2b87f011 100644 --- a/spec/lib/gitlab/ci/config/header/root_spec.rb +++ b/spec/lib/gitlab/ci/config/header/root_spec.rb @@ -136,13 +136,13 @@ let(:header_hash) do { spec: { - component: %w[name sha] + component: %w[name sha version] } } end it 'returns the component value as symbols' do - expect(config.spec_component_value).to match_array([:name, :sha]) + expect(config.spec_component_value).to match_array([:name, :sha, :version]) end end @@ -183,14 +183,14 @@ inputs: { foo: { default: 'bar' } }, - component: %w[name] + component: %w[name version] } } end it 'returns both values correctly' do expect(config.spec_inputs_value).to eq({ foo: { default: 'bar' } }) - expect(config.spec_component_value).to match_array([:name]) + expect(config.spec_component_value).to match_array([:name, :version]) end end end diff --git a/spec/lib/gitlab/ci/config/header/spec_spec.rb b/spec/lib/gitlab/ci/config/header/spec_spec.rb index 8c1b081cc4caec..f318c39880d375 100644 --- a/spec/lib/gitlab/ci/config/header/spec_spec.rb +++ b/spec/lib/gitlab/ci/config/header/spec_spec.rb @@ -31,7 +31,7 @@ context 'when spec contains component configuration' do let(:spec_hash) do { - component: %w[name sha] + component: %w[name sha version] } end @@ -41,7 +41,7 @@ end it 'returns the component value' do - expect(config.component_value).to match_array([:name, :sha]) + expect(config.component_value).to match_array([:name, :sha, :version]) end end @@ -53,7 +53,7 @@ default: 'bar' } }, - component: %w[name] + component: %w[name version] } end @@ -64,7 +64,7 @@ it 'returns both values correctly' do expect(config.inputs_value).to eq({ foo: { default: 'bar' } }) - expect(config.component_value).to match_array([:name]) + expect(config.component_value).to match_array([:name, :version]) end end diff --git a/spec/lib/gitlab/ci/config/interpolation/interpolator_spec.rb b/spec/lib/gitlab/ci/config/interpolation/interpolator_spec.rb index 5d7be700073cfb..bba6c41aaccf15 100644 --- a/spec/lib/gitlab/ci/config/interpolation/interpolator_spec.rb +++ b/spec/lib/gitlab/ci/config/interpolation/interpolator_spec.rb @@ -176,17 +176,17 @@ let(:yaml_context) do ::Gitlab::Ci::Config::Yaml::Context.new( variables: [], - component: { name: 'my-component', sha: 'abc123' } + component: { name: 'my-component', sha: 'abc123', version: '1.0.0' } ) end context 'when component values are specified in spec' do let(:header) do - { spec: { component: %w[name sha] } } + { spec: { component: %w[name sha version] } } end let(:content) do - { test: 'Component $[[ component.name ]] at $[[ component.sha ]]' } + { test: 'Component $[[ component.name ]] at $[[ component.sha ]] version $[[ component.version ]]' } end let(:arguments) { {} } @@ -196,7 +196,7 @@ expect(subject).to be_interpolated expect(subject).to be_valid - expect(subject.to_hash).to eq({ test: 'Component my-component at abc123' }) + expect(subject.to_hash).to eq({ test: 'Component my-component at abc123 version 1.0.0' }) end end @@ -221,12 +221,12 @@ context 'when both inputs and component are used' do let(:header) do - { spec: { inputs: { env: nil }, component: %w[name] } } + { spec: { inputs: { env: nil }, component: %w[name version] } } end let(:content) do { - test: 'Deploy to $[[ inputs.env ]] using $[[ component.name ]]' + test: 'Deploy to $[[ inputs.env ]] using $[[ component.name ]] v$[[ component.version ]]' } end @@ -237,7 +237,7 @@ expect(subject).to be_interpolated expect(subject).to be_valid - expect(subject.to_hash).to eq({ test: 'Deploy to production using my-component' }) + expect(subject.to_hash).to eq({ test: 'Deploy to production using my-component v1.0.0' }) end end end diff --git a/spec/lib/gitlab/ci/config/yaml/context_spec.rb b/spec/lib/gitlab/ci/config/yaml/context_spec.rb index ccb535ba327c18..372a5a224bafde 100644 --- a/spec/lib/gitlab/ci/config/yaml/context_spec.rb +++ b/spec/lib/gitlab/ci/config/yaml/context_spec.rb @@ -29,7 +29,7 @@ context 'with component data provided' do let(:component_data) do - { name: 'my-component', sha: 'abc123' } + { name: 'my-component', sha: 'abc123', version: '1.0.0' } end subject(:context) { described_class.new(component: component_data) } @@ -56,7 +56,7 @@ describe '#component' do let(:component_data) do - { name: 'test-component', sha: 'def456' } + { name: 'test-component', sha: 'def456', version: '2.0.0' } end subject(:context) { described_class.new(component: component_data) } diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb index 29f2fc1b82d1c8..11d1d13e0773cf 100644 --- a/spec/lib/gitlab/ci/config_spec.rb +++ b/spec/lib/gitlab/ci/config_spec.rb @@ -211,8 +211,8 @@ it 'returns only components included with `include:component`' do expect(config.metadata[:includes].size).to eq(3) expect(included_components).to contain_exactly( - { project: project1, sha: project1.commit.sha, name: 'dast' }, - { project: project2, sha: project2.commit.sha, name: 'template' } + { project: project1, sha: project1.commit.sha, name: 'dast', version: nil }, + { project: project2, sha: project2.commit.sha, name: 'template', version: nil } ) end @@ -228,7 +228,7 @@ it 'returns only unique components' do expect(config.metadata[:includes].size).to eq(2) expect(included_components).to contain_exactly( - { project: project1, sha: project1.commit.sha, name: 'dast' } + { project: project1, sha: project1.commit.sha, name: 'dast', version: nil } ) end end diff --git a/spec/services/ci/components/fetch_service_spec.rb b/spec/services/ci/components/fetch_service_spec.rb index d487f6764c8f34..d62f37163fb651 100644 --- a/spec/services/ci/components/fetch_service_spec.rb +++ b/spec/services/ci/components/fetch_service_spec.rb @@ -26,10 +26,10 @@ } ) - project.repository.add_tag(project.creator, 'v0.1', project.repository.commit.sha) + project.repository.add_tag(project.creator, '0.1.2', project.repository.commit.sha) - create(:release, project: project, tag: 'v0.1', sha: project.repository.commit.sha) create(:ci_catalog_resource, project: project) + create(:release, :with_catalog_resource_version, project: project, tag: '0.1.2', sha: project.repository.commit.sha) project end @@ -83,10 +83,11 @@ 'templates/component/template.yml' => content } ) - project.repository.add_tag(project.creator, 'v0.1', project.repository.commit.sha) + project.repository.add_tag(project.creator, '0.1.3', project.repository.commit.sha) - create(:release, project: project, tag: 'v0.1', sha: project.repository.commit.sha) create(:ci_catalog_resource, project: project) + create(:release, :with_catalog_resource_version, + project: project, tag: '0.1.3', sha: project.repository.commit.sha) project end @@ -104,21 +105,26 @@ end context 'when version is a branch name' do - it_behaves_like 'component address' do - let(:version) { project.default_branch } - end + let(:version) { project.default_branch } + + it_behaves_like 'component address' end context 'when version is a tag name' do - it_behaves_like 'component address' do - let(:version) { project.repository.tags.first.name } + let(:version) { project.repository.tags.first.name } + + it_behaves_like 'component address' + + it 'returns the version' do + expect(result).to be_success + expect(result.payload[:version]).to eq(version) end end context 'when version is a commit sha' do - it_behaves_like 'component address' do - let(:version) { project.repository.tags.first.id } - end + let(:version) { project.repository.tags.first.id } + + it_behaves_like 'component address' end context 'when version is not provided' do diff --git a/spec/services/ci/create_pipeline_service/component_interpolation_spec.rb b/spec/services/ci/create_pipeline_service/component_interpolation_spec.rb index 39e9c1c645da26..93d3200cb1ff46 100644 --- a/spec/services/ci/create_pipeline_service/component_interpolation_spec.rb +++ b/spec/services/ci/create_pipeline_service/component_interpolation_spec.rb @@ -20,7 +20,7 @@ let_it_be(:component_yaml) do <<~YAML spec: - component: [name, sha] + component: [name, sha, version] inputs: compiler: default: gcc @@ -33,7 +33,7 @@ test: script: - echo "Building with $[[ inputs.compiler ]] and optimization level $[[ inputs.optimization_level ]]" - - echo "Component $[[ component.name ]] / $[[ component.sha ]]" + - echo "Component $[[ component.name ]] / $[[ component.sha ]] / $[[ component.version ]]" YAML end @@ -80,7 +80,73 @@ test_job = pipeline.builds.find { |build| build.name == 'test' } expect(test_job.options[:script]).to eq([ 'echo "Building with gcc and optimization level 2"', - "echo \"Component #{component_name} / #{component_sha}\"" + "echo \"Component #{component_name} / #{component_sha} / #{component_version}\"" + ]) + end + end + + context 'when the component path is with a partial version' do + let_it_be(:component_path) do + "#{Gitlab.config.gitlab.host}/#{components_project.full_path}/#{component_name}@0.1" + end + + it 'creates a pipeline with correct jobs' do + response = execute + pipeline = response.payload + + expect(response).to be_success + expect(pipeline).to be_created_successfully + + expect(pipeline.builds.map(&:name)).to contain_exactly('test') + + test_job = pipeline.builds.find { |build| build.name == 'test' } + expect(test_job.options[:script]).to eq([ + 'echo "Building with gcc and optimization level 2"', + "echo \"Component #{component_name} / #{component_sha} / #{component_version}\"" + ]) + end + end + + context 'when the component path is with latest' do + let_it_be(:component_path) do + "#{Gitlab.config.gitlab.host}/#{components_project.full_path}/#{component_name}@~latest" + end + + it 'creates a pipeline with correct jobs' do + response = execute + pipeline = response.payload + + expect(response).to be_success + expect(pipeline).to be_created_successfully + + expect(pipeline.builds.map(&:name)).to contain_exactly('test') + + test_job = pipeline.builds.find { |build| build.name == 'test' } + expect(test_job.options[:script]).to eq([ + 'echo "Building with gcc and optimization level 2"', + "echo \"Component #{component_name} / #{component_sha} / #{component_version}\"" + ]) + end + end + + context 'when the component path is with sha' do + let_it_be(:component_path) do + "#{Gitlab.config.gitlab.host}/#{components_project.full_path}/#{component_name}@#{component_sha}" + end + + it 'creates a pipeline with correct jobs without version' do + response = execute + pipeline = response.payload + + expect(response).to be_success + expect(pipeline).to be_created_successfully + + expect(pipeline.builds.map(&:name)).to contain_exactly('test') + + test_job = pipeline.builds.find { |build| build.name == 'test' } + expect(test_job.options[:script]).to eq([ + 'echo "Building with gcc and optimization level 2"', + "echo \"Component #{component_name} / #{component_sha} / \"" ]) end -- GitLab From 6a5ef56688ea80425e6068c4473dc63a27a804a9 Mon Sep 17 00:00:00 2001 From: Furkan Ayhan Date: Tue, 21 Oct 2025 10:17:49 +0200 Subject: [PATCH 2/2] Apply reviewer feedback --- lib/gitlab/ci/components/instance_path.rb | 36 +++++++++++++---------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/lib/gitlab/ci/components/instance_path.rb b/lib/gitlab/ci/components/instance_path.rb index 5aac37605783a8..56e84d292c7663 100644 --- a/lib/gitlab/ci/components/instance_path.rb +++ b/lib/gitlab/ci/components/instance_path.rb @@ -42,40 +42,40 @@ def project def sha return unless project - find_version_sha(version) + find_version_sha end strong_memoize_attr :sha def matched_version - find_matched_version(version)&.semver&.to_s + find_catalog_version&.semver&.to_s end strong_memoize_attr :matched_version def invalid_usage_for_latest? - @version == LATEST && project && project.catalog_resource.nil? + version == LATEST && project && project.catalog_resource.nil? end def invalid_usage_for_partial_semver? - @version.match?(SHORTHAND_SEMVER_PATTERN) && project && project.catalog_resource.nil? + version.match?(SHORTHAND_SEMVER_PATTERN) && project && project.catalog_resource.nil? end private attr_reader :version - def find_version_sha(version) + def find_version_sha return find_latest_sha if version == LATEST - sha_by_shorthand_semver(version) || sha_by_released_tag(version) || sha_by_ref(version) + sha_by_shorthand_semver || sha_by_released_tag || sha_by_ref end - def find_matched_version(version) + def find_catalog_version return unless project.catalog_resource if version == LATEST - project.catalog_resource.versions.latest + catalog_resource_version_latest elsif version.match?(SHORTHAND_SEMVER_PATTERN) - catalog_resource_version_by_short_semver(version) + catalog_resource_version_by_short_semver else project.catalog_resource.versions.by_name(version).first end @@ -84,26 +84,32 @@ def find_matched_version(version) def find_latest_sha return unless project.catalog_resource - project.catalog_resource.versions.latest&.sha + catalog_resource_version_latest&.sha end - def sha_by_shorthand_semver(version) + def sha_by_shorthand_semver return unless version.match?(SHORTHAND_SEMVER_PATTERN) return unless project.catalog_resource - catalog_resource_version_by_short_semver(version)&.sha + catalog_resource_version_by_short_semver&.sha end - def catalog_resource_version_by_short_semver(version) + def catalog_resource_version_latest + project.catalog_resource.versions.latest + end + strong_memoize_attr :catalog_resource_version_latest + + def catalog_resource_version_by_short_semver major, minor = version.split(".") project.catalog_resource.versions.latest(major, minor) end + strong_memoize_attr :catalog_resource_version_by_short_semver - def sha_by_released_tag(version) + def sha_by_released_tag project.releases.find_by_tag(version)&.sha end - def sha_by_ref(version) + def sha_by_ref project.commit(version)&.id end -- GitLab