lib/release_tools/patch_release/blog_merge_request.rb (226 lines of code) (raw):
# frozen_string_literal: true
module ReleaseTools
module PatchRelease
class BlogMergeRequest < MergeRequest
include ::SemanticLogger::Loggable
include ReleaseTools::Security::IssueHelper
include ReleaseTools::Security::MergeRequestHelper
# Projects that are not included in the table of fixes because their
# fixes are often due to external CVEs
NON_TABLE_OF_FIXES_PROJECTS = [
Project::OmnibusGitlab.security_id,
Project::CNGImage.security_id
].freeze
def project
ReleaseTools::Project::WWWGitlabCom
end
def labels
'patch release post'
end
def title
"Draft: Adding #{versions_str} blog post"
end
def assignee_ids
if security_fixes?
appsec_release_managers.collect(&:id)
else
release_managers.collect(&:id)
end
end
def source_branch
"create-#{hyphenated_version}-post"
end
def create
create_blog_post_file
return if SharedStatus.dry_run?
super
end
def patch_issue_url
return 'test.gitlab.com' if SharedStatus.dry_run?
return unless patch_coordinator.single_version?
Issue.new(version: versions.first).url
end
def generate_blog_content
ERB.new(content_template_path, trim_mode: '-').result(binding)
end
def blog_post_filename
if security_fixes?
"#{Date.current + 1.day}-#{release_name_hyphen}-gitlab-#{hyphenated_version}-released.html.md"
else
"#{Date.current}-gitlab-#{hyphenated_version}-released.html.md"
end
end
def metadata_canonical_path
return if patch_coordinator.single_version?
date = Time.now.utc + 1.day
"/releases/#{date.strftime('%Y/%m/%d')}/#{release_name_hyphen}-gitlab-#{hyphenated_version}-released/"
end
def security_issue_header(security_issue)
security_issue&.cves_issue&.title || security_issue.issue.title
end
def security_issue_blog_content(security_issue)
cves_issue = security_issue.cves_issue
if cves_issue && cves_issue.valid_yaml?
<<~STR
#{cves_issue.vulnerability_description.gsub(/[.]+\z/, '')}.
This is a #{cves_issue.cvss_severity.downcase} severity issue ([`#{cves_issue.cvss_string}`](https://gitlab-com.gitlab.io/gl-security/product-security/appsec/cvss-calculator/explain#explain=#{cves_issue.cvss_string}), #{cves_issue.cvss_base_score}).
#{cves_id_link(cves_issue)}
#{cves_issue.credit.gsub(/[.]+\z/, '')}.
STR
else
logger.warn "The content of the security issue (#{security_issue.web_url}) was skipped due to invalid CVES YAML or because of missing CVES."
<<~STR
TODO: add description for this issue
STR
end
end
def cves_id_link(cves_issue)
if cves_issue.cve_id.blank?
"We have requested a CVE ID and will update this blog post when it is assigned."
else
"It is now mitigated in the latest release and is assigned [#{cves_issue.cve_id}](https://cve.mitre.org/cgi-bin/cvename.cgi?name=#{cves_issue.cve_id})."
end
end
def project_path
# If the blog post contains security content, the blog MR needs
# to be created in the security mirror.
return project.security_path if security_fixes?
super
end
def sorted_security_content
security_fixes
.sort_by do |issue|
if issue.cves_issue&.valid_yaml?
issue.cves_issue&.cvss_base_score || -1
else
-1
end
end
.reverse
end
def sorted_table_content
sorted_security_content.reject do |issue|
NON_TABLE_OF_FIXES_PROJECTS.include?(issue.project_id)
end
end
def security_issue_slug(security_issue)
security_issue_header(security_issue)
.downcase
.scan(/\w|\d| |-/)
.join
.tr(' ', '-')
end
def displayed_severity(security_issue)
return 'TODO' unless security_issue.cves_issue&.valid_yaml?
severity = security_issue.cves_issue&.cvss_severity
return 'TODO' if severity == 'None'
severity
end
def blog_metadata_title
"GitLab #{title_header} #{release_name}: #{versions_str}".squish
end
def blog_metadata_description
"Learn more about GitLab #{title_header} #{release_name}: #{versions_str} for GitLab Community Edition (CE) and Enterprise Edition (EE).".squish
end
def blog_metadata_tags
'security' unless patch_coordinator.single_version?
end
def exists?
if security_fixes?
security_blog_merge_request.present?
else
super
end
end
def bug_fixes
patch_coordinator.merge_requests(with_patch_version: true)
end
def security_fixes?
security_fixes.present?
end
def bug_fixes?
patch_coordinator.pressure.positive?
end
protected
def release_name
'Patch Release'
end
def release_name_hyphen
release_name.downcase.tr(' ', '-')
end
def hyphenated_version
versions.first.tr(', ', '--').tr('.', '-')
end
def template_path
File.expand_path('../../../templates/patch_blog_post_merge_request_description.md.erb', __dir__)
end
def content_template_path
file_name = if patch_coordinator.single_version?
'patch_release_single_version_template'
else
'patch_release_template'
end
File.read("templates/blog_posts/#{file_name}.html.md.erb")
end
def create_blog_post_file
file_content = generate_blog_content
filepath = blog_post_filepath + blog_post_filename
logger.info('Created blog post file')
unless SharedStatus.dry_run?
Retriable.with_context(:api) do
GitlabClient.find_or_create_branch(source_branch, target_branch, project_path)
end
end
logger.info('Created branch', branch_name: source_branch, project: project_path)
commit(file_content, filepath)
end
def commit(file_content, filepath, action = 'create', message = nil)
message ||= "Adding #{versions_str} blog post"
actions = [{
action: action,
file_path: filepath,
content: file_content
}]
logger.info('Committing blog content', project: project_path, branch: source_branch)
return if SharedStatus.dry_run?
Retriable.with_context(:api) do
GitlabClient.create_commit(
project_path,
source_branch,
message,
actions
)
end
end
def blog_post_filepath
"sites/uncategorized/source/releases/posts/"
end
def release_managers
schedule.active_release_managers
rescue ReleaseManagers::Schedule::VersionNotFoundError
logger.fatal('Could not find active release managers')
nil
end
def appsec_release_managers
schedule.active_appsec_release_managers
rescue ReleaseManagers::Schedule::VersionNotFoundError
logger.fatal('Could not find active appsec release managers')
nil
end
def schedule
@schedule ||= ReleaseManagers::Schedule.new
end
def versions
return [bug_fixes.first[:version]] if patch_coordinator.single_version?
if security_fixes?
bug_fixes.map { |release| release[:version] }
else
bug_fixes.filter_map do |release|
release[:version] if release[:pressure].positive?
end
end
end
def versions_str
versions.join(', ')
end
def critical_patch_release?
ReleaseTools::SharedStatus.critical_patch_release?
end
def title_header
critical_patch_release? ? 'Critical' : ''
end
end
end
end