lib/release_tools/public_release/release.rb (155 lines of code) (raw):

# frozen_string_literal: true module ReleaseTools module PublicRelease # rubocop: disable Metrics/ModuleLength # An interface for API release classes. # # This interface/module provides a variety of default methods, and methods # that an including class must implement. By implementing these methods, # release classes can reuse behaviour (such as creating a tag) that is the # same for all release classes, while still being able to override behaviour # where necessary. # # This module must not dictate the implementation of releases, beyond # requiring some methods that have to be implemented to use the methods # provided by this module. # # For example, the `execute` method in this module is deliberately left # empty (besides raising an error). If this method were to instead call # other methods (e.g. a method for compiling a changelog), releases classes # would have to start overwriting methods the moment they want to change # this behaviour. Such tight coupling can make for code that is hard to # debug, understand, test, and extend. # # In other words, all this module should do is: # # 1. Provide a public interface (but _not_ an implementation) for release # classes. # 2. Provide some default methods that most release classes will need. As a # rule of thumb, don't add a method here unless it's used by three or # more release classes. module Release PRODUCTION_ENVIRONMENT = 'gprd' STAGING_ENVIRONMENT = 'gstg' DifferentCommitsError = Class.new(StandardError) def self.included(into) into.include(::SemanticLogger::Loggable) end def release_metadata raise NotImplementedError end def version raise NotImplementedError end def project raise NotImplementedError end def client raise NotImplementedError end def execute raise NotImplementedError end def project_path project.canonical_or_security_path end def tag_name version.tag end def source_for_target_branch last_production_commit end def target_branch version.stable_branch end def create_target_branch source = source_for_target_branch logger.info( 'Creating target branch', project: project_path, source: source, branch: target_branch ) unless SharedStatus.dry_run? client.find_or_create_branch(target_branch, source, project_path) end notify_stable_branch_creation end def notify_slack(project, version) path = project.canonical_or_security_path tag = omnibus?(project) ? version : version.tag logger.info('Notifying Slack', project: path, version: version) Slack::TagNotification.release(project, tag) end def omnibus?(project) project == Project::OmnibusGitlab end # Fetches the current production commit for a GitLab component, it uses two # sources for this: # # - Deployment tracker through `last_production_commit_deployments`, and, # - release/metadata through `last_production_commit_metadata`. # # The release/metadata is being rolled out on # https://gitlab.com/gitlab-com/gl-infra/delivery/-/issues/2607, and until # it's safely tested this method returns the sha from the deployment tracker. # # Changes under the release/metadata implementation are under a feature flag. def last_production_commit sha_deployments = last_production_commit_deployments return sha_deployments unless Feature.enabled?(:release_metadata_as_source) sha_metadata = last_production_commit_metadata if sha_metadata == sha_deployments logger.info('Commits are the same', sha_metadata: sha_metadata, sha_deployments: sha_deployments) else logger.error('Commits are not the same', sha_metadata: sha_metadata, sha_deployments: sha_deployments) raise DifferentCommitsError end sha_deployments rescue DifferentCommitsError sha_deployments end def last_production_commit_deployments env = PRODUCTION_ENVIRONMENT # Branches are to always be created on the public projects, using the # deployments tracked in those projects. To ensure this we use the # public project path. This way even during a patch release we fetch # this data from the public project. sha = client .deployments(project.path, env, status: 'success').first&.sha unless sha raise "The project #{project_path} has no deployments for #{env}. " \ 'If this project does not use GitLab deployments, re-define the ' \ 'source_for_target_branch instance method to return the source ' \ 'branch name' end logger.info('Last production commit via project deployments', project: project.path, sha: sha) sha end def last_production_commit_metadata sha = current_gprd_product_version[project.metadata_project_name].sha logger.info('Last production commit via metadata', project: project.path, sha: sha) sha end # Update one or more VERSION files. # # The `versions` argument is a `Hash` that maps version file names (e.g. # `VERSION` or `GITALY_SERVER_VERSION`) to their new version identifiers. # For example: # # { 'VERSION' => '1.2.3', 'GITALY_SERVER_VERSION' => '2.3.4' } # # By default this method will trigger pipelines for the commit it creates, # as this is the safest default. To disable this, set `skip_ci` to `true`. # rubocop: disable Metrics/ParameterLists def commit_version_files( branch, versions, message: 'Update VERSION files', project: project_path, skip_ci: false, skip_merge_train: false ) actions = versions.each_with_object([]) do |(file, version), memo| action = begin # We strip newlines from both the old file contents and the new # contents. This ensures that if the old file contains a trailing # newline, and the new content does as well, we don't create empty # commits. This _does_ happen if we compare the existing stripped # content with the new content as-is, provided said new content # contains a trailing newline. if client.file_contents(project, file, branch).strip != version.strip 'update' end rescue Gitlab::Error::NotFound 'create' end next unless action logger.info( "Setting version file #{file} to #{version.tr("\n", ' ')}", project: project, action: action, file: file, version: version, branch: branch ) memo << { action: action, file_path: file, content: version } end return if actions.empty? tags = [] tags << '[ci skip]' if skip_ci tags << '[merge-train skip]' if skip_merge_train message += "\n\n#{tags.join("\n")}" if tags.any? client.create_commit(project, branch, message, actions) end # rubocop: enable Metrics/ParameterLists # Returns the GitLab components metadata based on the last successful # production deployment tracked in the release/metadata project in OPS. The # metadata is retrieved via ProductVersion def current_gprd_product_version ops_client = GitlabOpsClient release_metadata = Project::Release::Metadata last_deployment = ops_client .deployments( release_metadata.path, PRODUCTION_ENVIRONMENT, status: 'success' ) .first ProductVersion.from_metadata_sha(last_deployment.sha) end def notify_stable_branch_creation logger.info('Checking if project and version are eligible for stable branch creation notification', project: project, version: version) # We only want to notify projects under Managed Versioning. return unless ReleaseTools::ManagedVersioning::PROJECTS.include?(project) # Stable branches are created when the RC for the monthly release is created. return unless version.rc? stable_branch_name = ReleaseTools::ReleaseManagers::Schedule .new .active_version .stable_branch(ee: project.ee_branch?) logger.info('Checking if target branch is the stable branch for current monthly release', stable_branch_name: stable_branch_name, target_branch: target_branch) return unless target_branch == stable_branch_name # Make sure the new stable branch exists return unless GitlabClient.find_branch(stable_branch_name, project_path) Security::NotifyStableBranchCreation .new(Security::IssueCrawler.new, project, stable_branch_name) .execute end end # rubocop: enable Metrics/ModuleLength end end