-
由 Achilleas Pipinellis 创作于由 Achilleas Pipinellis 创作于
代码所有者
将用户和群组指定为特定文件更改的核准人。 了解更多。
lint-docs-redirects.rb 7.38 KiB
#!/usr/bin/env ruby
# frozen_string_literal: true
#
# https://docs.gitlab.com/ee/development/documentation/redirects.html
#
require 'net/http'
require 'uri'
require 'json'
require 'cgi'
require 'yaml'
class LintDocsRedirect
COLOR_CODE_RED = "\e[31m"
COLOR_CODE_RESET = "\e[0m"
# All the projects we want this script to run
PROJECT_PATHS = ['gitlab-org/gitlab',
'gitlab-org/gitlab-runner',
'gitlab-org/omnibus-gitlab',
'gitlab-org/charts/gitlab',
'gitlab-org/cloud-native/gitlab-operator'].freeze
def execute
return unless project_supported?
abort_unless_merge_request_iid_exists
check_renamed_deleted_files
check_for_circular_redirects
end
private
# Project slug based on project path
# Taken from https://gitlab.com/gitlab-org/gitlab/-/blob/daaa5b6f79049e5bb28cdafaa11d3a0a84d64ab3/scripts/trigger-build.rb#L298-313
def project_slug
case ENV['CI_PROJECT_PATH']
when 'gitlab-org/gitlab'
'ee'
when 'gitlab-org/gitlab-runner'
'runner'
when 'gitlab-org/omnibus-gitlab'
'omnibus'
when 'gitlab-org/charts/gitlab'
'charts'
when 'gitlab-org/cloud-native/gitlab-operator'
'operator'
end
end
def navigation_file
@navigation_file ||= begin
url = URI('https://gitlab.com/gitlab-org/gitlab-docs/-/raw/main/content/_data/navigation.yaml')
response = Net::HTTP.get_response(url)
raise "Could not download navigation.yaml. Response code: #{response.code}" if response.code != '200'
# response.body should be memoized in a method, so that it doesn't
# need to be downloaded multiple times in one CI job.
response.body
end
end
##
## Check if the deleted/renamed file exists in
## https://gitlab.com/gitlab-org/gitlab-docs/-/blob/main/content/_data/navigation.yaml.
##
## We need to first convert the Markdown file to HTML. There are two cases:
##
## - A source doc entry with index.md looks like: doc/administration/index.md
## The navigation.yaml equivalent is: ee/administration/
## - A source doc entry without index.md looks like: doc/administration/appearance.md
## The navigation.yaml equivalent is: ee/administration/appearance.html
##
def check_for_missing_nav_entry(file)
file_sub = file["old_path"].gsub('doc', project_slug).gsub('index.md', '').gsub('.md', '.html')
result = navigation_file.include?(file_sub)
return unless result
warning(file)
abort
end
def warning(file)
warn <<~WARNING
#{COLOR_CODE_RED}✖ ERROR: Missing redirect for a deleted or moved page#{COLOR_CODE_RESET}
The following file is linked in the global navigation for docs.gitlab.com:
=> #{file['old_path']}
Unless you add a redirect or remove the page from the global navigation,
this change will break pipelines in the 'gitlab/gitlab-docs' project.
#{rake_command(file)}
For more information, see:
- Create a redirect : https://docs.gitlab.com/ee/development/documentation/redirects.html
- Edit the global nav : https://docs.gitlab.com/ee/development/documentation/site_architecture/global_nav.html#add-a-navigation-entry
WARNING
end
# Rake task to use depending on the file being deleted or renamed
def rake_command(file)
# The Rake task is only available for gitlab-org/gitlab
return unless project_slug == 'ee'
if renamed_doc_file?(file)
rake = "bundle exec rake \"gitlab:docs:redirect[#{file['old_path']}, #{file['new_path']}]\""
msg = "It seems you renamed a page, run the following Rake task locally and commit the changes.\n"
elsif deleted_doc_file?(file)
rake = "bundle exec rake \"gitlab:docs:redirect[#{file['old_path']}, doc/new/path.md]\""
msg = "It seems you deleted a page. Run the following Rake task by replacing\n" \
"'doc/new/path.md' with the page to redirect to, and commit the changes.\n"
end
<<~MSG
#{msg}
#{rake}
MSG
end
# GitLab API URL
def gitlab_api_url
ENV.fetch('CI_API_V4_URL', 'https://gitlab.com/api/v4')
end
# Take the project path from the CI_PROJECT_PATH predefined variable.
def url_encoded_project_path
project_path = ENV.fetch('CI_PROJECT_PATH', nil)
return unless project_path
CGI.escape(project_path)
end
# Take the merge request ID from the CI_MERGE_REQUEST_IID predefined
# variable.
def merge_request_iid
ENV.fetch('CI_MERGE_REQUEST_IID', nil)
end
def abort_unless_merge_request_iid_exists
abort("Error: CI_MERGE_REQUEST_IID environment variable is missing") if merge_request_iid.nil?
end
# Skip if CI_PROJECT_PATH is not in the designated project paths
def project_supported?
PROJECT_PATHS.include? ENV['CI_PROJECT_PATH']
end
# Fetch the merge request diff JSON object
def merge_request_diff
@merge_request_diff ||= begin
uri = URI.parse(
"#{gitlab_api_url}/projects/#{url_encoded_project_path}/merge_requests/#{merge_request_iid}/diffs?per_page=30"
)
response = Net::HTTP.get_response(uri)
unless response.code == '200'
raise "API call to get MR diffs failed. Response code: #{response.code}. Response message: #{response.message}"
end
JSON.parse(response.body)
end
end
def doc_file?(file)
file['old_path'].start_with?('doc/') && file['old_path'].end_with?('.md')
end
def renamed_doc_file?(file)
file['renamed_file'] == true && doc_file?(file)
end
def deleted_doc_file?(file)
file['deleted_file'] == true && doc_file?(file)
end
# Create a list of hashes of the renamed documentation files
def check_renamed_deleted_files
renamed_files = merge_request_diff.select do |file|
renamed_doc_file?(file)
end
deleted_files = merge_request_diff.select do |file|
deleted_doc_file?(file)
end
# Merge the two arrays
all_files = renamed_files + deleted_files
return if all_files.empty?
all_files.each do |file|
status = deleted_doc_file?(file) ? 'deleted' : 'renamed'
puts "Checking #{status} file..."
puts "=> Old_path: #{file['old_path']}"
puts "=> New_path: #{file['new_path']}"
puts
check_for_missing_nav_entry(file)
end
end
# Search for '+redirect_to' in the diff to find the new value. It should
# return a string of "+redirect_to: 'file.md'", in which case, delete the
# '+' prefix. If not found, skip and go to next file.
def redirect_to(diff_file)
redirect_to = diff_file["diff"]
.lines
.find { |e| e.include?('+redirect_to') }
&.delete_prefix('+')
return if redirect_to.nil?
YAML.safe_load(redirect_to)['redirect_to']
end
def all_doc_files
merge_request_diff.select do |file|
doc_file?(file)
end
end
# Check if a page redirects to itself
def check_for_circular_redirects
all_doc_files.each do |file|
next if redirect_to(file).nil?
basename = File.basename(file['old_path'])
# Fail if the 'redirect_to' value is the same as the file's basename.
next unless redirect_to(file) == basename
warn <<~WARNING
#{COLOR_CODE_RED}✖ ERROR: Circular redirect detected. The 'redirect_to' value points to the same file.#{COLOR_CODE_RESET}
WARNING
puts
puts "File : #{file['old_path']}"
puts "Redirect to : #{redirect_to(file)}"
abort
end
end
end
LintDocsRedirect.new.execute if $PROGRAM_NAME == __FILE__