lib/release_tools/product_version.rb (123 lines of code) (raw):

# frozen_string_literal: true module ReleaseTools # ProductVersion is a container for any type of GitLab - The DevOps Platform package. # Given a Product version we will be able to fetch its own metadata and find # what references to both components and packagers. # # A ProductVersion is always a normalized version (see: Version#to_normalized_version) class ProductVersion include ::SemanticLogger::Loggable include Comparable extend Forwardable MetadataInfo = Struct.new(:commit_id, :content, keyword_init: true) METADATA_PATH_REGEXP = %r[releases/(?<major>\d+)/(?<normalized_version>[^/]+).json] AUTO_DEPLOY_TAG_REGEX = %r{\A (?<major>\d+) \.(?<minor>\d+) \.(?<timestamp>\d+) \+ .* \z}x # delegate data accessors def_delegators :@version, :major, :minor, :patch, :rc, :to_s # delegate predicate functions for detecting the type of version def_delegators :@version, :rc?, :monthly?, :patch? # Instance of ::Version attr_reader :version # Creates a `ProductVersion` from an auto-deploy tag or a package version # # @param version [String, AutoDeploy::Version] the auto-deploy version # @return [ProductVersion, nil] the product version of the given auto-deploy # version or nil if not an auto-deploy version. def self.from_auto_deploy(version) version = AutoDeploy::Version.new(version) unless version.respond_to?(:to_package) from_package_version(version.to_package) rescue ArgumentError nil end # Creates a `ProductVersion` from an auto-deploy tag # # @param version [String] the auto-deploy tag # @return [ProductVersion, nil] the product version of the given auto-deploy # tag or nil if not an auto-deploy tag. def self.from_auto_deploy_tag(version) captures = AUTO_DEPLOY_TAG_REGEX.match(version) return nil unless captures new("#{captures['major']}.#{captures['minor']}.#{captures['timestamp']}") end # Creates a `ProductVersion` from an omnibus package version (i.e. $DEPLOY_VERSION) # # @param version [String, Version] a valid Omnibus package version # @return [ProductVersion, nil] the product version of the given version # or nil if not a valid version. def self.from_package_version(version) version = Version.new(version) unless version.is_a?(Version) return nil unless version.version? new(version.to_normalized_version) end # Creates a `ProductVersion` from a release/metadata commit id. # # @param sha [String] the commit id introducting a release metadata # @return [ProductVersion, nil] the product version of the given version # or nil if not a valid version. def self.from_metadata_sha(sha) Retriable.with_context(:api) do logger.info('Fetching release metadata commit', sha: sha) diffs = GitlabOpsClient.commit_diff(ReleaseMetadataUploader::PROJECT, sha) diff = diffs.detect do |d| d.new_file && METADATA_PATH_REGEXP.match(d.new_path) end return nil unless diff from_package_version(METADATA_PATH_REGEXP.match(diff.new_path)[:normalized_version]) rescue Gitlab::Error::NotFound nil end end # NOTE(nolith): here we implement Enumerable at class level to iterate over all the # product versions in release-metadata. It's implemented as a lazy enumerator in order # to only fetch commits one by one. Because release-metadata acts as a ledger, we can # navigate it commit by commit and build the product version history. class << self include Enumerable def each(&block) return lazy_enumerator unless block lazy_enumerator.each(&block) end def last_auto_deploy @last_auto_deploy ||= find(&:auto_deploy?) end private def lazy_enumerator Enumerator.new do |yielder| commits = Retriable.with_context(:api) do GitlabOpsClient.commits(ReleaseMetadataUploader::PROJECT, trailers: true) end loop do commits.each do |commit| version = commit.trailers.to_h['Product-Version'] # If a commit does not have the trailer, it is not a commit that # recorded a new version. next unless version product_version = new(version) yielder << product_version end break unless commits.has_next_page? Retriable.with_context(:api) do commits = commits.next_page end end end.lazy end end def initialize(version) @version = Version.new(version) end def ==(other) @version.to_s == other.to_s end def <=>(other) @version <=> other.version end def metadata full_metadata.content end def metadata_commit_id full_metadata.commit_id end # product version for auto_deploy is MAJOR.MINOR.TIMESTAMP<YYYYMMDDhhmm> # it's safe to assume that we will never have a 12 digit patch version # so we can identify auto_deploy packages by checking the patch value def auto_deploy? # YYYYMMDDhhmm first gitlab commit by DZ patch? && patch > 201110082230 # rubocop:disable Style/NumericLiterals end def auto_deploy_package metadata .dig('releases', Project::OmnibusGitlab.metadata_project_name, 'ref') .tr('+', '-') end # extract the release metadata for the given released_item # # @param released_item [String, Symbol, Project] the component # @return [ReleaseMetadata::Release, nil] the release information # or nil if the component does not exists def [](released_item) released_item = released_item.metadata_project_name if released_item.respond_to?(:metadata_project_name) name = released_item.to_s raw = metadata.dig('releases', name) return unless raw ReleaseMetadata::Release.new(name: name, **raw) end private def metadata_path "releases/#{major}/#{@version.to_normalized_version}.json" end def full_metadata @full_metadata ||= Retriable.with_context(:api) do logger.info('Fetching release metadata', version: to_s) meta_info = GitlabOpsClient.get_file( ReleaseMetadataUploader::PROJECT, metadata_path, ReleaseMetadataUploader::PROJECT.default_branch ) json = Base64.strict_decode64(meta_info.content) MetadataInfo.new( commit_id: meta_info.last_commit_id, content: JSON.parse(json) ) rescue Gitlab::Error::NotFound MetadataInfo.new(content: {}) end end end end