lib/release_tools/public_release/gitlab_release.rb (327 lines of code) (raw):
# frozen_string_literal: true
module ReleaseTools
module PublicRelease
# A release of GitLab CE and EE using the API.
class GitlabRelease
include Release
attr_reader :version, :client, :release_metadata
# The sleep intervals to use when waiting for the EE to CE sync.
#
# At 15 intervals of 60 seconds, we wait at most 15 minutes.
#
# As of June 2020, almost all sync pipelines finish in less than 6
# minutes. As such, 15 minutes should be more than enough.
WAIT_SYNC_INTERVALS = Array.new(15, 60)
# The file to check when waiting for the EE to CE sync.
SYNC_CHECK_FILE = 'CHANGELOG.md'
# The name of the default branch for CE and EE.
DEFAULT_BRANCH = Project::GitlabEe.default_branch
# The name of the version file storing the CE/EE version.
VERSION_FILE = 'VERSION'
# Error raised when a EE to CE sync pipeline is still running.
PipelineTooSlow = Class.new(StandardError) do
def message
'The EE to CE pipeline did not finish in a timely manner'
end
end
# Error raised when a EE to CE sync pipeline failed.
PipelineFailed = Class.new(StandardError) do
def message
'The EE to CE pipeline failed'
end
end
def initialize(
version,
client: GitlabClient,
release_metadata: ReleaseMetadata.new,
commit: nil
)
@version = version.to_ee
@client = client
@release_metadata = release_metadata
@commit = commit
end
def execute
logger.info(
'Starting release of GitLab CE and EE',
ee_version: version,
ce_version: version.to_ce
)
create_ce_target_branch
create_ee_target_branch
return if SharedStatus.dry_run?
update_managed_component_versions
compile_changelogs
update_versions
# Now that EE has been updated, we need to wait for the Merge Train to
# sync all changes from EE to CE. Not waiting for this could result in
# us tagging outdated commits/code.
wait_for_ee_to_ce_sync
create_ce_commit_to_run_ci
ce_tag = create_ce_tag
ee_tag = create_ee_tag
update_monthly_tag_metrics
start_new_minor_release
add_release_data_for_tags(ce_tag, ee_tag)
notify_slack(Project::GitlabCe, version.to_ce)
notify_slack(Project::GitlabEe, version)
end
def create_ce_target_branch
# CE stable branches must be created before we can sync the EE changes
# to them. Branching off from master would include commits we won't
# actually release, which is confusing. Thus, we create the initial
# branch from the last stable branch.
source = Version.new(version.previous_minor).stable_branch
logger.info(
'Creating CE target branch',
project: ce_project_path,
source: source,
branch: ce_target_branch
)
return if SharedStatus.dry_run?
client.find_or_create_branch(ce_target_branch, source, ce_project_path)
end
def create_ee_target_branch
source = source_for_target_branch
logger.info(
'Creating EE 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 update_monthly_tag_metrics
return unless version.rc?
return unless matching_monthly_version?
ReleaseTools::Metrics::MonthlyReleaseStatus.new(status: :tagged_rc).execute
end
def update_managed_component_versions
# When releasing an RC, there is no stable branch yet for the component
# (as Gitaly/Pages RCs don't create stable branches). This means the
# <project>_SERVER_VERSION should stay as-is.
return if version.rc?
component_versions = managed_versioning_projects.each_with_object({}) do |project_model, versions|
next unless project_model.respond_to?(:version_file)
versions[project_model.version_file] = managed_component_version(project_model)
end
# We skip CI tests here as we'll be comitting a few more files, making
# pipelines being created at this point redundant.
commit_version_files(
target_branch,
component_versions,
message: "Update managed components version to #{version.to_ce}",
skip_ci: true
)
end
def managed_versioning_projects
return ManagedVersioning::PROJECTS unless Feature.enabled?(:release_the_kas)
[ReleaseTools::Project::Kas].concat(ManagedVersioning::PROJECTS)
end
# Creates a Merge Train pipeline to sync EE to CE and waits for it to
# complete.
#
# We can't rely on the state of one or more files to determine if CE is in
# sync with EE, as these files (e.g. a changelog) may not change when
# operations are retried. This would result in this code thinking CE and
# EE are in sync, even when this may not be the case (e.g. not all code
# has been synced yet).
#
# To handle this, we create a Merge Train pipeline ourselves and wait for
# it to finish. In some cases code may have already been synced, but this
# approach is the only way we can guarantee that CE and EE are in sync.
def wait_for_ee_to_ce_sync
return if Feature.enabled?(:skip_foss_merge_train)
logger.info(
'Creating pipeline to sync EE to CE',
project: project_path,
target_branch: target_branch,
ce_project_path: ce_project_path,
ce_target_branch: ce_target_branch
)
initial_pipeline = GitlabOpsClient.create_pipeline(
Project::MergeTrain,
{
MERGE_FOSS: '1',
SOURCE_PROJECT: project_path,
SOURCE_BRANCH: target_branch,
TARGET_PROJECT: ce_project_path,
TARGET_BRANCH: ce_target_branch
}
)
url = initial_pipeline.web_url
logger.info('Created pipeline to sync EE to CE', pipeline: url)
Retriable.retriable(intervals: WAIT_SYNC_INTERVALS, on: PipelineTooSlow) do
pipeline = GitlabOpsClient
.pipeline(Project::MergeTrain, initial_pipeline.id)
case pipeline.status
when 'success'
logger.info('The EE to CE sync finished', pipeline: url)
return
when 'running', 'pending', 'created', 'waiting_for_resource'
logger.info('The EE to CE sync is still running', pipeline: url)
raise PipelineTooSlow
else
logger.error(
'The EE to CE sync did not succeed',
pipeline: url,
status: pipeline.status
)
raise PipelineFailed
end
end
end
def create_ce_commit_to_run_ci
# When bumping versions on CE, we skip pipelines for the version
# commits. We then sync the EE changes to CE.
#
# If there are no changes to sync to CE and we tag the latest commit,
# that tag will point to a commit with `ci skip`; preventing pipelines
# from running.
#
# To handle this, we check if the latest commit skips pipelines. If so,
# we create a dummy commit.
#
# This should only happen rarely, as most of the time the Merge Train
# _will_ have changes to sync.
path = ce_project_path
ref = ce_target_branch
commit = client.commit(path, ref: ref)
return unless commit.message.include?('[ci skip]')
message = <<~MSG.strip
Ensure pipelines run for #{version.to_ce}
The last commit synced to this repository includes the "ci skip" tag,
preventing tag pipelines from running. To ensure these pipelines do run,
we create this commit. This normally is only necessary if a Merge Train
sync didn't introduce any new changes.
For more information, refer to issue
https://gitlab.com/gitlab-com/gl-infra/delivery/-/issues/1355.
MSG
# When committing changes using the API, at least one action must be
# present. However, it's fine if that action doesn't actually change
# anything. Thus, to create a commit we update the VERSION file to what
# it's already set to, creating an empty commit in the process that
# allows tags to trigger a pipeline.
client.create_commit(
path,
ref,
message,
[
{
action: 'update',
file_path: 'VERSION',
content: version.to_ce.to_s
}
]
)
end
def compile_changelogs
return if version.rc?
logger.info('Compiling changelog', project: project_path)
ChangelogCompiler
.new(project_path, client: client)
.compile(version, branch: target_branch)
end
def update_versions
logger.info(
'Updating EE version file',
project: project_path,
version: version,
branch: target_branch
)
# On EE this will be the last commit on the stable branch. To ensure
# tagging pipelines run, we _don't_ skip any CI pipelines here. We
# explicily set `skip_ci: false` so that it doesn't look like we just
# forgot to set it at all.
#
# For CE it's fine to skip, because we will create a commit to sync EE
# changes to CE.
commit_version_files(
target_branch,
{ VERSION_FILE => version.to_s },
skip_ci: false,
skip_merge_train: true
)
logger.info(
'Updating CE version file',
project: ce_project_path,
version: version,
branch: ce_target_branch
)
commit_version_files(
ce_target_branch,
{ VERSION_FILE => version.to_ce.to_s },
project: ce_project_path,
skip_ci: true
)
end
def start_new_minor_release
return unless version.monthly?
ee_pre = version.to_upcoming_pre_release
ce_pre = version.to_ce.to_upcoming_pre_release
logger.info(
'Starting a new EE pre-release',
project: project_path,
version: ee_pre,
branch: DEFAULT_BRANCH
)
commit_version_files(
DEFAULT_BRANCH,
{ VERSION_FILE => ee_pre },
skip_ci: true
)
logger.info(
'Starting a new CE pre-release',
project: ce_project_path,
version: ce_pre,
branch: DEFAULT_BRANCH
)
commit_version_files(
DEFAULT_BRANCH,
{ VERSION_FILE => ce_pre },
project: ce_project_path,
skip_ci: true
)
end
def create_ee_tag
tag = version.tag
logger.info('Creating EE tag', tag: tag, project: project_path)
client.find_or_create_tag(
project_path,
tag,
target_branch,
message: "Version #{tag}"
)
end
def create_ce_tag
tag = version.to_ce.tag
logger.info('Creating CE tag', tag: tag, project: ce_project_path)
client.tag(ce_project_path, tag: tag)
rescue Gitlab::Error::NotFound
client_tag = client.create_tag(
ce_project_path,
tag,
ce_target_branch,
"Version #{tag}"
)
ReleaseTools::Metrics::TrackReleaseTagged.new(version).execute
client_tag
end
def add_release_data_for_tags(ce_tag, ee_tag)
meta_version = version.to_normalized_version
logger.info(
'Recording release data',
project: project_path,
version: meta_version,
ce_tag: ce_tag.name,
ee_tag: ee_tag.name
)
release_metadata.add_release(
name: 'gitlab-ee',
version: meta_version,
sha: ee_tag.commit.id,
ref: ee_tag.name,
tag: true
)
release_metadata.add_release(
name: 'gitlab-ce',
version: meta_version,
sha: ce_tag.commit.id,
ref: ce_tag.name,
tag: true
)
end
def source_for_target_branch
if @commit
logger.info('Using specific commit', project: project_path, commit: @commit)
end
@commit || super
end
def project
Project::GitlabEe
end
def ce_target_branch
version.to_ce.stable_branch
end
def ce_project_path
Project::GitlabCe.canonical_or_security_path
end
private
def managed_component_version(project_model)
# Feature flag to be removed with https://gitlab.com/gitlab-com/gl-infra/delivery/-/issues/2735
# if no warnings are logged
if Feature.enabled?(:fetch_managed_component_version) && project_model != ReleaseTools::Project::Kas
project = project_model.canonical_or_security_path
# Managed component branches don't use the -ee suffix.
branch = version.to_ce.stable_branch
commit_version = Retriable.with_context(:api) do
client.file_contents(project, 'VERSION', branch).strip
end
if commit_version != version.to_ce
logger.warn(
'Managed component version does not match version being released',
project: project_model.path,
component_version: commit_version,
release_version: version.to_ce
)
end
commit_version
else
version.to_ce
end
end
def matching_monthly_version?
current_monthly_minor_version = ReleaseTools::Version.new(ReleaseTools::GitlabReleasesGemClient.version_for_date(Date.today)).to_minor
tag_minor_version = version.to_minor
current_monthly_minor_version == tag_minor_version
end
end
end
end