diff options
Diffstat (limited to 'scripts/lint-docs-redirects.rb')
-rwxr-xr-x | scripts/lint-docs-redirects.rb | 203 |
1 files changed, 203 insertions, 0 deletions
diff --git a/scripts/lint-docs-redirects.rb b/scripts/lint-docs-redirects.rb new file mode 100755 index 00000000000..fb4ac19981d --- /dev/null +++ b/scripts/lint-docs-redirects.rb @@ -0,0 +1,203 @@ +#!/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' + +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 + 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 renamed_doc_file?(file) + file['renamed_file'] == true && file['old_path'].start_with?('doc') + end + + def deleted_doc_file?(file) + file['deleted_file'] == true && file['old_path'].start_with?('doc') + 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 +end + +LintDocsRedirect.new.execute if $PROGRAM_NAME == __FILE__ |