#!/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__