2020-01-01 13:55:28 +05:30
# frozen_string_literal: true
2021-04-17 20:07:23 +05:30
require 'gitlab/dangerfiles/title_linting'
2021-03-08 18:12:59 +05:30
2021-03-11 19:13:27 +05:30
module Tooling
2020-01-01 13:55:28 +05:30
module Danger
module Changelog
2020-07-28 23:09:34 +05:30
NO_CHANGELOG_LABELS = [
'tooling' ,
'tooling::pipelines' ,
'tooling::workflow' ,
'ci-build' ,
'meta'
] . freeze
2020-01-01 13:55:28 +05:30
NO_CHANGELOG_CATEGORIES = % i [ docs none ] . freeze
2021-06-08 01:23:25 +05:30
CHANGELOG_TRAILER_REGEX = / ^Changelog: \ s*(?<category>.+)$ / . freeze
2020-11-24 15:15:51 +05:30
CREATE_CHANGELOG_COMMAND = 'bin/changelog -m %<mr_iid>s "%<mr_title>s"'
CREATE_EE_CHANGELOG_COMMAND = 'bin/changelog --ee -m %<mr_iid>s "%<mr_title>s"'
2021-06-08 01:23:25 +05:30
CHANGELOG_MODIFIED_URL_TEXT = " **CHANGELOG.md was edited.** Please remove the additions and follow the [changelog guidelines](https://docs.gitlab.com/ee/development/changelog.html). \n \n "
2020-11-24 15:15:51 +05:30
CHANGELOG_MISSING_URL_TEXT = " **[CHANGELOG missing](https://docs.gitlab.com/ee/development/changelog.html)**: \n \n "
2020-01-01 13:55:28 +05:30
2021-04-29 21:17:54 +05:30
OPTIONAL_CHANGELOG_MESSAGE = {
local : " If this merge request [doesn't need a CHANGELOG entry](https://docs.gitlab.com/ee/development/changelog.html # what-warrants-a-changelog-entry), feel free to ignore this message. " ,
ci : << ~ MSG
If you want to create a changelog entry for GitLab FOSS , run the following :
2020-11-24 15:15:51 +05:30
2021-04-29 21:17:54 +05:30
#{CREATE_CHANGELOG_COMMAND}
2020-11-24 15:15:51 +05:30
2021-04-29 21:17:54 +05:30
If you want to create a changelog entry for GitLab EE , run the following instead :
2020-11-24 15:15:51 +05:30
2021-04-29 21:17:54 +05:30
#{CREATE_EE_CHANGELOG_COMMAND}
2020-11-24 15:15:51 +05:30
2021-04-29 21:17:54 +05:30
If this merge request [ doesn ' t need a CHANGELOG entry ] ( https : / / docs . gitlab . com / ee / development / changelog . html #what-warrants-a-changelog-entry), feel free to ignore this message.
MSG
} . freeze
2021-06-08 01:23:25 +05:30
CHANGELOG_NEW_WORKFLOW_MESSAGE = << ~ MSG
We are in the process of rolling out a new workflow for adding changelog entries . This new workflow uses Git commit subjects and Git trailers to generate changelogs . This new approach will soon replace the current YAML based approach .
To ease the transition process , we recommend you start using both the old and new approach in parallel . This is not required at this time , but will make it easier to transition to the new approach in the future . To do so , pick the commit that should go in the changelog and add a ` Changelog ` trailer to it . For example :
` ` `
This is my commit ' s subject line
This is the optional commit body .
Changelog : added
` ` `
The value of the ` Changelog ` trailer should be one of the following : added , fixed , changed , deprecated , removed , security , performance , other .
For more information , take a look at the following resources :
- ` https://gitlab.com/gitlab-com/gl-infra/delivery/-/issues/1564 `
- https : / / docs . gitlab . com / ee / api / repositories . html #generate-changelog-data
If you ' d like to see the new approach in action , take a look at the commits in [ the Omnibus repository ] ( https : / / gitlab . com / gitlab - org / omnibus - gitlab / - / commits / master ) .
MSG
SEE_DOC = " See the [changelog documentation](https://docs.gitlab.com/ee/development/changelog.html). "
SUGGEST_MR_COMMENT = << ~ SUGGEST_COMMENT
` ` ` suggestion
merge_request : %< mr_iid > s
` ` `
#{SEE_DOC}
SUGGEST_COMMENT
2020-11-24 15:15:51 +05:30
2021-04-17 20:07:23 +05:30
REQUIRED_CHANGELOG_REASONS = {
feature_flag_removed : 'removes a feature flag'
} . freeze
2021-04-29 21:17:54 +05:30
REQUIRED_CHANGELOG_MESSAGE = {
local : " This merge request requires a changelog entry because it [%<reason>s](https://docs.gitlab.com/ee/development/changelog.html # what-warrants-a-changelog-entry). " ,
ci : << ~ MSG
To create a changelog entry , run the following :
2020-11-24 15:15:51 +05:30
2021-04-29 21:17:54 +05:30
#{CREATE_CHANGELOG_COMMAND}
2020-11-24 15:15:51 +05:30
2021-04-29 21:17:54 +05:30
This merge request requires a changelog entry because it [ %< reason > s ] ( https : / / docs . gitlab . com / ee / development / changelog . html #what-warrants-a-changelog-entry).
MSG
} . freeze
2020-11-24 15:15:51 +05:30
2021-06-08 01:23:25 +05:30
CATEGORIES = YAML
. load_file ( File . expand_path ( '../../.gitlab/changelog_config.yml' , __dir__ ) )
. fetch ( 'categories' )
. keys
. freeze
class ChangelogCheckResult
attr_reader :errors , :warnings , :markdowns , :messages
def initialize ( errors : [ ] , warnings : [ ] , markdowns : [ ] , messages : [ ] )
@errors = errors
@warnings = warnings
@markdowns = markdowns
@messages = messages
end
private_class_method :new
def self . empty
new
end
def self . error ( error )
new ( errors : [ error ] )
end
def self . warning ( warning )
new ( warnings : [ warning ] )
end
def error ( error )
errors << error
end
def warning ( warning )
warnings << warning
end
def markdown ( markdown )
markdowns << markdown
end
def message ( message )
messages << message
end
end
# rubocop:disable Style/SignalException
def check!
if git . modified_files . include? ( " CHANGELOG.md " )
fail modified_text
end
if present?
add_danger_messages ( check_changelog_yaml )
add_danger_messages ( check_changelog_path )
elsif required?
required_texts . each { | _ , text | fail ( text ) } # rubocop:disable Lint/UnreachableLoop
elsif optional?
message optional_text
end
return unless helper . ci?
if required? || optional?
checked = 0
git . commits . each do | commit |
check_result = check_changelog_trailer ( commit )
next if check_result . nil?
checked += 1
add_danger_messages ( check_result )
end
if checked == 0
message CHANGELOG_NEW_WORKFLOW_MESSAGE
end
end
end
# rubocop:enable Style/SignalException
# rubocop:disable Style/SignalException
def add_danger_messages ( check_result )
check_result . errors . each { | error | fail ( error ) } # rubocop:disable Lint/UnreachableLoop
check_result . warnings . each { | warning | warn ( warning ) }
check_result . markdowns . each { | markdown_hash | markdown ( ** markdown_hash ) }
check_result . messages . each { | text | message ( text ) }
end
# rubocop:enable Style/SignalException
def check_changelog_trailer ( commit )
trailer = commit . message . match ( CHANGELOG_TRAILER_REGEX )
return if trailer . nil? || trailer [ :category ] . nil?
category = trailer [ :category ]
return ChangelogCheckResult . empty if CATEGORIES . include? ( category )
ChangelogCheckResult . error ( " Commit #{ commit . sha } uses an invalid changelog category: #{ category } " )
end
def check_changelog_yaml
check_result = ChangelogCheckResult . empty
return check_result unless present?
raw_file = read_file ( changelog_path )
yaml = YAML . safe_load ( raw_file )
yaml_merge_request = yaml [ " merge_request " ] . to_s
check_result . error ( " `title` should be set, in #{ helper . html_link ( changelog_path ) } ! #{ SEE_DOC } " ) if yaml [ " title " ] . nil?
check_result . error ( " `type` should be set, in #{ helper . html_link ( changelog_path ) } ! #{ SEE_DOC } " ) if yaml [ " type " ] . nil?
return check_result if helper . security_mr? || helper . mr_iid . empty?
check_changelog_yaml_merge_request ( raw_file : raw_file , yaml_merge_request : yaml_merge_request , check_result : check_result )
rescue Psych :: Exception
# YAML could not be parsed, fail the build.
ChangelogCheckResult . error ( " #{ helper . html_link ( changelog_path ) } isn't valid YAML! #{ SEE_DOC } " )
rescue StandardError = > e
ChangelogCheckResult . warning ( " There was a problem trying to check the Changelog. Exception: #{ e . class . name } - #{ e . message } " )
end
def check_changelog_yaml_merge_request ( raw_file : , yaml_merge_request : , check_result : )
cherry_pick_against_stable_branch = helper . cherry_pick_mr? && helper . stable_branch?
if yaml_merge_request . empty?
mr_line = raw_file . lines . find_index { | line | line =~ / merge_request: \ s* \ n / }
if mr_line
check_result . markdown ( msg : format ( SUGGEST_MR_COMMENT , mr_iid : helper . mr_iid ) , file : changelog_path , line : mr_line . succ )
else
check_result . message ( " Consider setting `merge_request` to #{ helper . mr_iid } in #{ helper . html_link ( changelog_path ) } . #{ SEE_DOC } " )
end
elsif yaml_merge_request != helper . mr_iid && ! cherry_pick_against_stable_branch
check_result . error ( " Merge request ID was not set to #{ helper . mr_iid } ! #{ SEE_DOC } " )
end
check_result
end
def check_changelog_path
check_result = ChangelogCheckResult . empty
return check_result unless present?
ee_changes = project_helper . all_ee_changes . dup
ee_changes . delete ( changelog_path )
if ee_changes . any? && ! ee_changelog? && ! required?
check_result . warning ( " This MR has a Changelog file outside `ee/`, but code changes in `ee/`. Consider moving the Changelog file into `ee/`. " )
end
if ee_changes . empty? && ee_changelog?
check_result . warning ( " This MR has a Changelog file in `ee/`, but no code changes in `ee/`. Consider moving the Changelog file outside `ee/`. " )
end
check_result
end
2021-04-17 20:07:23 +05:30
def required_reasons
[ ] . tap do | reasons |
reasons << :feature_flag_removed if project_helper . changes . deleted . has_category? ( :feature_flag )
end
end
2020-11-24 15:15:51 +05:30
def required?
2021-04-17 20:07:23 +05:30
required_reasons . any?
2020-11-24 15:15:51 +05:30
end
def optional?
2021-06-08 01:23:25 +05:30
categories_need_changelog? && mr_without_no_changelog_label?
2020-01-01 13:55:28 +05:30
end
2021-06-08 01:23:25 +05:30
def present?
! ! changelog_path
2020-01-01 13:55:28 +05:30
end
2020-04-22 19:07:51 +05:30
def ee_changelog?
2021-06-08 01:23:25 +05:30
changelog_path . start_with? ( 'ee/' )
end
def changelog_path
@changelog_path || = project_helper . changes . added . by_category ( :changelog ) . files . first
2020-01-01 13:55:28 +05:30
end
2020-11-24 15:15:51 +05:30
def modified_text
CHANGELOG_MODIFIED_URL_TEXT +
2021-04-29 21:17:54 +05:30
( helper . ci? ? format ( OPTIONAL_CHANGELOG_MESSAGE [ :ci ] , mr_iid : helper . mr_iid , mr_title : sanitized_mr_title ) : OPTIONAL_CHANGELOG_MESSAGE [ :local ] )
2020-11-24 15:15:51 +05:30
end
2021-04-17 20:07:23 +05:30
def required_texts
required_reasons . each_with_object ( { } ) do | required_reason , memo |
memo [ required_reason ] =
CHANGELOG_MISSING_URL_TEXT +
2021-04-29 21:17:54 +05:30
( helper . ci? ? format ( REQUIRED_CHANGELOG_MESSAGE [ :ci ] , reason : REQUIRED_CHANGELOG_REASONS . fetch ( required_reason ) , mr_iid : helper . mr_iid , mr_title : sanitized_mr_title ) : REQUIRED_CHANGELOG_MESSAGE [ :local ] )
2021-04-17 20:07:23 +05:30
end
2020-11-24 15:15:51 +05:30
end
def optional_text
CHANGELOG_MISSING_URL_TEXT +
2021-04-29 21:17:54 +05:30
( helper . ci? ? format ( OPTIONAL_CHANGELOG_MESSAGE [ :ci ] , mr_iid : helper . mr_iid , mr_title : sanitized_mr_title ) : OPTIONAL_CHANGELOG_MESSAGE [ :local ] )
2020-11-24 15:15:51 +05:30
end
2020-01-01 13:55:28 +05:30
private
2021-06-08 01:23:25 +05:30
def read_file ( path )
File . read ( path )
end
2020-11-24 15:15:51 +05:30
def sanitized_mr_title
2021-04-17 20:07:23 +05:30
Gitlab :: Dangerfiles :: TitleLinting . sanitize_mr_title ( helper . mr_title )
2020-11-24 15:15:51 +05:30
end
2020-01-01 13:55:28 +05:30
def categories_need_changelog?
2021-04-17 20:07:23 +05:30
( project_helper . changes . categories - NO_CHANGELOG_CATEGORIES ) . any?
2020-01-01 13:55:28 +05:30
end
2020-07-28 23:09:34 +05:30
2021-06-08 01:23:25 +05:30
def mr_without_no_changelog_label?
2021-04-17 20:07:23 +05:30
( helper . mr_labels & NO_CHANGELOG_LABELS ) . empty?
2020-07-28 23:09:34 +05:30
end
2020-01-01 13:55:28 +05:30
end
end
end