diff options
-rw-r--r-- | .prettierignore | 3 | ||||
-rw-r--r-- | Makefile | 4 | ||||
-rw-r--r-- | content/versions.json | 12 | ||||
-rw-r--r-- | doc/releases.md | 26 | ||||
-rw-r--r-- | lib/release.rb | 141 | ||||
-rw-r--r-- | lib/tasks/redirects.rake | 40 | ||||
-rw-r--r-- | lib/tasks/release.rake | 73 | ||||
-rw-r--r-- | lib/tasks/task_helpers.rb | 32 | ||||
-rw-r--r-- | spec/lib/release_spec.rb | 98 |
9 files changed, 323 insertions, 106 deletions
diff --git a/.prettierignore b/.prettierignore index eccbb6b6..ef99a543 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,7 +4,8 @@ /vendor/ /tmp/ /content/assets/javascripts/ +/content/versions.json # Ignore yml files since prettier formatting clashes with yamllint. # https://github.com/adrienverge/yamllint/issues/443 -*.yml
\ No newline at end of file +*.yml @@ -220,4 +220,8 @@ create-stable-branch: @printf "\n$(INFO)INFO: Creating stable branch...$(END)\n" @bundle exec rake release:single +.PHONY: update-versions-dropdown +update-versions-dropdown: + @bundle exec rake release:update_versions_dropdown + test: setup rspec-tests jest-tests eslint-tests prettier-tests stylelint-tests hadolint-tests yamllint-tests markdownlint-tests check-global-navigation diff --git a/content/versions.json b/content/versions.json index cb9d8c65..5598a7d6 100644 --- a/content/versions.json +++ b/content/versions.json @@ -2,7 +2,13 @@ { "next": "16.0", "current": "15.11", - "last_minor": ["15.10", "15.9"], - "last_major": ["14.10", "13.12"] + "last_minor": [ + "15.10", + "15.9" + ], + "last_major": [ + "14.10", + "13.12" + ] } -] +]
\ No newline at end of file diff --git a/doc/releases.md b/doc/releases.md index 94b41b7e..e3721f28 100644 --- a/doc/releases.md +++ b/doc/releases.md @@ -137,15 +137,19 @@ To create the release merge request for the release: git checkout -b release-15-0 ``` -1. Edit `content/versions.json` and update the lists of versions to reflect the new release in the Versions menu: +1. Update `content/versions.json` to reflect the new release in the Versions menu: - - Set `next` to the version number of the next release. For example, if you're releasing `15.2`, set `next` to `15.3`. - - Set `current` to the version number of the release you're releasing. For example, if you're releasing `15.2`, set - `current` to `15.2`. + ```shell + make update-versions-dropdown + ``` + + - `next` is set to the version number of the next release. For example, if you're releasing `15.2`, `next` is set to `15.3`. + - `current` is set to the version number of the release you're releasing. For example, if you're releasing `15.2`, + `current` is set to `15.2`. + - `last_minor` is set to the last two most recent minor releases. For example, if you're + releasing `15.2`, `last_minor` is set to `15.1` and `15.0`. - Ensure `last_major` is set to the two most recent major versions. Do not include the current major version. For example, if you're releasing `15.2`, ensure `last_major` is `14.10` and `13.12`. - - Set `last_minor` to the last two most recent minor releases. For example, if you're - releasing `15.2`, set `last_minor` to `15.1` and `15.0`. As a complete example, the `content/versions.json` file for the `15.2` release is: @@ -154,8 +158,14 @@ To create the release merge request for the release: { "next": "15.3", "current": "15.2", - "last_minor": ["15.1", "15.0"], - "last_major": ["14.10", "13.12"] + "last_minor": [ + "15.1", + "15.0" + ], + "last_major": [ + "14.10", + "13.12" + ] } ] ``` diff --git a/lib/release.rb b/lib/release.rb new file mode 100644 index 00000000..d6b2627f --- /dev/null +++ b/lib/release.rb @@ -0,0 +1,141 @@ +# frozen_string_literal: true + +require_relative 'tasks/task_helpers' + +require 'fileutils' +require 'pathname' + +class Release + CURRENT_RELEASE_DATE = Date.today.strftime("%Y-%m-22") + NEXT_RELEASE_DATE = (Date.today >> 1).strftime("%Y-%m-22") + PREVIOUS_RELEASE_DATE = (Date.today << 1).strftime("%Y-%m-22") + PREVIOUS_PREVIOUS_RELEASE_DATE = (Date.today << 2).strftime("%Y-%m-22") + + ExplicitError = Class.new(StandardError) + + def initialize(version: nil, dry_run: ENV['DRY_RUN'] == 'true') + @version = version || task_helpers.milestone(CURRENT_RELEASE_DATE) + @dry_run = dry_run + end + + def single + # Disable lefthook because it causes PATH errors + # https://docs.gitlab.com/ee/development/contributing/style_guides.html#disable-lefthook-temporarily + ENV['LEFTHOOK'] = '0' + + raise ExplicitError, "Local branch already exists. Run `git branch --delete --force #{version}` and rerun the task." if !dry_run && task_helpers.local_branch_exist?(version) + raise ExplicitError, "'#{dockerfile}' already exists. Run `rm #{dockerfile}` and rerun the task." if dockerfile.exist? + + stash_changes + checkout_main_and_update + create_branch_for_version + create_dockerfile + commit_dockerfile + git_push_branch + end + + def update_versions_dropdown + if dry_run + info("DRY RUN: Not updating the version dropdown...") + else + current_version = task_helpers.milestone(CURRENT_RELEASE_DATE) + next_version = task_helpers.milestone(NEXT_RELEASE_DATE) + previous_version = task_helpers.milestone(PREVIOUS_RELEASE_DATE) + previous_previous_version = task_helpers.milestone(PREVIOUS_PREVIOUS_RELEASE_DATE) + + dropdown_json = JSON.load_file!('content/versions.json') + dropdown_json.first.merge!( + 'next' => next_version, + 'current' => current_version, + 'last_minor' => [previous_version, previous_previous_version] + ) + + info("Updating content/versions.json...") + File.write('content/versions.json', JSON.pretty_generate(dropdown_json)) + end + end + + private + + attr_reader :version, :dry_run + + def dockerfile + @dockerfile ||= Pathname.new("#{version}.Dockerfile") + end + + def stash_changes + if dry_run + info("DRY RUN: Not stashing local changes.") + else + info("Stashing local changes...") + exec_cmd("git stash -u") if task_helpers.git_workdir_dirty? + end + end + + def checkout_main_and_update + if dry_run + info("DRY RUN: Not checking out main branch and pulling updates.") + else + info("Checking out main branch and pulling updates...") + exec_cmd("git checkout main") + exec_cmd("git pull origin main") + end + end + + def create_branch_for_version + if dry_run + info("DRY RUN: Not creating branch #{version}.") + else + info("Creating branch #{version}...") + exec_cmd("git checkout -b #{version}") + end + end + + def create_dockerfile + single_dockerfile = Pathname.new('dockerfiles/single.Dockerfile') + + if dry_run + info("DRY RUN: Not creating file #{dockerfile}.") + else + info("Creating file #{dockerfile}...") + dockerfile.open('w') do |post| + post.write(single_dockerfile.read.gsub('ARG VER', "ARG VER=#{version}")) + end + end + end + + def commit_dockerfile + if dry_run + info("DRY RUN: Not adding file #{dockerfile} to branch #{version} or commiting changes.") + else + info("Adding file #{dockerfile} and commiting changes to branch #{version}...") + exec_cmd("git add #{version}.Dockerfile") + exec_cmd("git commit -m 'Release cut #{version}'") + end + end + + def git_push_branch + if dry_run + info("DRY RUN: Not pushing branch #{version}.") + else + info("Pushing branch #{version}. Don't create a merge request...") + exec_cmd("git push origin #{version}") + end + end + + def task_helpers + @task_helpers ||= TaskHelpers.new + end + + def exec_cmd(cmd) + `#{cmd}` + end + + def info(msg) + task_helpers.info("gitlab-docs", msg) + end + + def error(msg) + task_helpers.error("gitlab-docs", msg) + end +end diff --git a/lib/tasks/redirects.rake b/lib/tasks/redirects.rake index 13e07a42..aa815d2d 100644 --- a/lib/tasks/redirects.rake +++ b/lib/tasks/redirects.rake @@ -29,6 +29,8 @@ end namespace :docs do desc 'GitLab | Docs | Clean up old redirects' task :clean_redirects do + include TaskHelpers::Output + redirects_yaml = "#{task_helpers.project_root}/content/_data/redirects.yaml" today = Time.now.utc.to_date mr_description = "Monthly cleanup of docs redirects.</br><p>See https://about.gitlab.com/handbook/product/ux/technical-writing/#regularly-scheduled-tasks</p></br></hr></br><p>_Created automatically: https://gitlab.com/gitlab-org/gitlab-docs/-/blob/main/doc/raketasks.md#clean-up-redirects_</p>" @@ -43,9 +45,9 @@ namespace :docs do abort("\n#{TaskHelpers::COLOR_CODE_RED}ERROR: jq not found. Install jq and run task again.#{TaskHelpers::COLOR_CODE_RESET}") if `which jq`.empty? if ENV['DRY_RUN'] == 'true' - TaskHelpers.info("gitlab-docs", "Not stashing changes in gitlab-docs or syncing with upstream default branch because running in dry run mode.") + info("gitlab-docs", "Not stashing changes in gitlab-docs or syncing with upstream default branch because running in dry run mode.") else - TaskHelpers.info("gitlab-docs", "Stashing changes in gitlab-docs and syncing with upstream default branch...") + info("gitlab-docs", "Stashing changes in gitlab-docs and syncing with upstream default branch...") system("git stash --quiet -u") if task_helpers.git_workdir_dirty? system("git checkout --quiet main") system("git fetch --quiet origin main") @@ -93,9 +95,9 @@ namespace :docs do Dir.chdir(content_dir) do if ENV['DRY_RUN'] == 'true' - TaskHelpers.info(slug, "Running in dry run mode...") + info(slug, "Running in dry run mode...") else - TaskHelpers.info(slug, "Stashing changes and syncing with upstream default branch...") + info(slug, "Stashing changes and syncing with upstream default branch...") system("git", "stash", "--quiet", "-u") if task_helpers.git_workdir_dirty? system("git", "checkout", "--quiet", default_branch) system("git", "fetch", "--quiet", "origin", default_branch) @@ -135,12 +137,12 @@ namespace :docs do # next unless remove_date < today - TaskHelpers.info(slug, "In #{filename}, remove date: #{remove_date} is less than today (#{today}).") + info(slug, "In #{filename}, remove date: #{remove_date} is less than today (#{today}).") counter += 1 if ENV['DRY_RUN'] == 'true' - TaskHelpers.info(slug, "Not deleting #{filename} because running in dry run mode.") + info(slug, "Not deleting #{filename} because running in dry run mode.") else FileUtils.rm_f(filename) end @@ -150,7 +152,7 @@ namespace :docs do next if new_path(frontmatter['redirect_to'], filename, content_dir, slug).start_with?('http') if ENV['DRY_RUN'] == 'true' - TaskHelpers.info("gitlab-docs", "Not updating redirects.yaml because running in dry run mode.") + info("gitlab-docs", "Not updating redirects.yaml because running in dry run mode.") else File.open(redirects_yaml, 'a') do |post| post.puts " - from: #{old_path}" @@ -164,7 +166,7 @@ namespace :docs do next unless old_path.end_with?('index.html') if ENV['DRY_RUN'] == 'true' - TaskHelpers.info("gitlab-docs", "Not updating redirects.yaml because running in dry run mode.") + info("gitlab-docs", "Not updating redirects.yaml because running in dry run mode.") else File.open(redirects_yaml, 'a') do |post| post.puts " - from: #{old_path.gsub!('index.html', '')}" @@ -183,24 +185,24 @@ namespace :docs do # 4. Commit and push the branch to create the MR # - TaskHelpers.info(slug, "Found #{counter} redirect(s).") + info(slug, "Found #{counter} redirect(s).") next unless counter.positive? Dir.chdir(content_dir) do if ENV['DRY_RUN'] == 'true' - TaskHelpers.info(slug, "Not creating branch or commiting changes because running in dry run mode.") + info(slug, "Not creating branch or commiting changes because running in dry run mode.") else - TaskHelpers.info(slug, "Creating a new branch for the redirects merge request...") + info(slug, "Creating a new branch for the redirects merge request...") system("git", "checkout", "--quiet", "-b", redirects_branch, origin_default_branch) - TaskHelpers.info(slug, "Committing changes to branch...") + info(slug, "Committing changes to branch...") system("git", "add", ".") system("git", "commit", "--quiet", "-m", commit_message) end if ENV['DRY_RUN'] == 'true' - TaskHelpers.info(slug, "Not pushing branch because running in dry run mode.") + info(slug, "Not pushing branch because running in dry run mode.") else - TaskHelpers.info(slug, "Pushing branch to create a merge request...") + info(slug, "Pushing branch to create a merge request...") `git push --set-upstream origin #{redirects_branch} -o merge_request.create -o merge_request.remove_source_branch -o merge_request.title="#{mr_title}" -o merge_request.description="#{mr_description}" -o merge_request.label="Technical Writing" -o merge_request.label="documentation" -o merge_request.label="docs::improvement" -o merge_request.label="type::maintenance" -o merge_request.label="maintenance::refactor"` \ end end @@ -216,19 +218,19 @@ namespace :docs do # mr_title = "Clean up docs redirects - #{today}" if ENV['DRY_RUN'] == 'true' - TaskHelpers.info("gitlab-docs", "Not creating branch or commiting changes because running in dry run mode.") + info("gitlab-docs", "Not creating branch or commiting changes because running in dry run mode.") else - TaskHelpers.info("gitlab-docs", "Creating a new branch for the redirects merge request...") + info("gitlab-docs", "Creating a new branch for the redirects merge request...") system("git", "checkout", "--quiet", "-b", redirects_branch, "origin/main") - TaskHelpers.info("gitlab-docs", "Committing changes to branch...") + info("gitlab-docs", "Committing changes to branch...") system("git", "add", redirects_yaml) system("git", "commit", "--quiet", "-m", commit_message) end if ENV['DRY_RUN'] == 'true' - TaskHelpers.info("gitlab-docs", "Not pushing branch because running in dry run mode.") + info("gitlab-docs", "Not pushing branch because running in dry run mode.") else - TaskHelpers.info("gitlab-docs", "Pushing branch to create a merge request...") + info("gitlab-docs", "Pushing branch to create a merge request...") `git push --set-upstream origin #{redirects_branch} -o merge_request.create -o merge_request.remove_source_branch -o merge_request.title="#{mr_title}" -o merge_request.description="#{mr_description}" -o merge_request.label="Technical Writing" -o merge_request.label="redirects" -o merge_request.label="Category:Docs Site" -o merge_request.label="type::maintenance" -o merge_request.label="maintenance::refactor"` \ end end diff --git a/lib/tasks/release.rake b/lib/tasks/release.rake index 30c69b6e..cdd74620 100644 --- a/lib/tasks/release.rake +++ b/lib/tasks/release.rake @@ -1,74 +1,21 @@ # frozen_string_literal: true -require './lib/tasks/task_helpers' -require 'fileutils' -require 'pathname' - -task_helpers = TaskHelpers.new -DRY_RUN = ENV['DRY_RUN'] == 'true' +require_relative '../release' namespace :release do desc 'Creates a single release archive' task :single, :version do - require "highline/import" - version = task_helpers.current_milestone - - # Disable lefthook because it causes PATH errors - # https://docs.gitlab.com/ee/development/contributing/style_guides.html#disable-lefthook-temporarily - ENV['LEFTHOOK'] = '0' - - abort("\n#{TaskHelpers::COLOR_CODE_RED}ERROR: Rake aborted! Local branch already exists. Run `git branch --delete --force #{version}` and rerun the task.#{TaskHelpers::COLOR_CODE_RESET}") \ - if task_helpers.local_branch_exist?(version) - - if DRY_RUN - TaskHelpers.info("gitlab-docs", "DRY RUN: Not stashing local changes.") - else - TaskHelpers.info("gitlab-docs", "Stashing local changes...") - `git stash -u` if task_helpers.git_workdir_dirty? - end - - if DRY_RUN - TaskHelpers.info("gitlab-docs", "DRY RUN: Not checking out main branch and pulling updates.") - else - TaskHelpers.info("gitlab-docs", "Checking out main branch and pulling updates...") - `git checkout main` - `git pull origin main` - end + include TaskHelpers::Output - if DRY_RUN - TaskHelpers.info("gitlab-docs", "DRY RUN: Not creating branch #{version}.") - else - TaskHelpers.info("gitlab-docs", "Creating branch #{version}...") - `git checkout -b #{version}` - end - - dockerfile = Pathname.new("#{version}.Dockerfile") - single_dockerfile = Pathname.new('dockerfiles/single.Dockerfile') - - if DRY_RUN - TaskHelpers.info("gitlab-docs", "DRY RUN: Not creating file #{dockerfile}.") - elsif File.exist?(dockerfile) && ask("#{dockerfile} already exists. Do you want to overwrite?", %w[y n]) == 'n' - abort('rake aborted!') - else - TaskHelpers.info("gitlab-docs", "Creating file #{dockerfile}...") - dockerfile.open('w') do |post| - post.write(single_dockerfile.read.gsub('ARG VER', "ARG VER=#{version}")) - end - end - - if DRY_RUN - TaskHelpers.info("gitlab-docs", "DRY RUN: Not adding file #{dockerfile} to branch #{version} or commiting changes.") - else - TaskHelpers.info("gitlab-docs", "Adding file #{dockerfile} and commiting changes to branch #{version}...") - `git add #{version}.Dockerfile` - `git commit -m 'Release cut #{version}'` + begin + Release.new.single + rescue StandardError => e + abort(error_format('gitlab-docs', e.message)) end + end - if DRY_RUN - TaskHelpers.info("gitlab-docs", "DRY RUN: Not pushing branch #{version}.") - else - TaskHelpers.info("gitlab-docs", "Pushing branch #{version}. Don't create a merge request...") - `git push origin #{version}` - end + desc 'Updates the versions dropdown JSON file' + task :update_versions_dropdown do + Release.new.update_versions_dropdown end end diff --git a/lib/tasks/task_helpers.rb b/lib/tasks/task_helpers.rb index 7c33a3d3..28e99377 100644 --- a/lib/tasks/task_helpers.rb +++ b/lib/tasks/task_helpers.rb @@ -10,7 +10,22 @@ class TaskHelpers COLOR_CODE_RESET = "\e[0m" COLOR_CODE_RED = "\e[31m" COLOR_CODE_GREEN = "\e[32m" - CURRENT_RELEASE_DATE = Date.today.strftime("%Y-%m-22") + + module Output + def info(slug, message) + puts "#{COLOR_CODE_GREEN}INFO: (#{slug}): #{message} #{COLOR_CODE_RESET}" + end + + def error(slug, message) + puts error_format(slug, message) + end + + def error_format(slug, message) + "#{COLOR_CODE_RED}ERROR: (#{slug}): #{message} #{COLOR_CODE_RESET}" + end + end + + include Output def config # Parse the config file and create a hash. @@ -128,16 +143,9 @@ class TaskHelpers `curl --silent https://gitlab.com/api/v4/projects/#{url_encoded_path} | jq --raw-output .default_branch`.tr("\n", '') end - def self.info(slug, message) - puts "#{TaskHelpers::COLOR_CODE_GREEN}INFO: (#{slug}): #{message} #{TaskHelpers::COLOR_CODE_RESET}" - end - - def current_milestone - @current_milestone ||= begin - release_dates_json = File.read("#{project_root}/content/release_dates.json") - # Search in the relase dates hash for the upcoming release date - # and fetch the milestone title. - JSON.parse(release_dates_json).first[CURRENT_RELEASE_DATE] - end + # Search in the relase dates hash for the release date + # and fetch the milestone title. The date must be in the format of "<year>-<month>-22". + def milestone(date) + JSON.load_file!("#{project_root}/content/release_dates.json").first[date] end end diff --git a/spec/lib/release_spec.rb b/spec/lib/release_spec.rb new file mode 100644 index 00000000..50eaaa77 --- /dev/null +++ b/spec/lib/release_spec.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +require 'spec_helper' + +require_relative '../../lib/release' + +RSpec.describe Release do + let(:version) { 99.99 } + let(:dry_run) { nil } + let(:task_helpers) { TaskHelpers.new } + + subject { described_class.new(version: version, dry_run: dry_run) } + + before do + allow(TaskHelpers).to receive(:new).and_return(task_helpers) + end + + describe '#single' do + context 'when in DRY_RUN mode' do + let(:dry_run) { true } + + it 'does not push the branch' do + expect_info("DRY RUN: Not stashing local changes.") + expect_info("DRY RUN: Not checking out main branch and pulling updates.") + expect_info("DRY RUN: Not creating branch #{version}.") + expect_info("DRY RUN: Not creating file #{version}.Dockerfile.") + expect_info("DRY RUN: Not adding file #{version}.Dockerfile to branch #{version} or commiting changes.") + expect_info("DRY RUN: Not pushing branch #{version}.") + + subject.single + end + end + + context 'when not in DRY_RUN mode' do + let(:dry_run) { false } + + it 'pushes the branch' do + allow(task_helpers).to receive(:info).with('gitlab-docs', any_args) + + allow(task_helpers).to receive(:git_workdir_dirty?).and_return(true) + + expect_exec_cmd("git stash -u") + expect_exec_cmd("git checkout main") + expect_exec_cmd("git pull origin main") + expect_exec_cmd("git checkout -b #{version}") + + single_dockerfile_double = instance_double(Pathname, read: 'ARG VER') + expect(Pathname).to receive(:new).with('dockerfiles/single.Dockerfile').and_return(single_dockerfile_double) + + dockerfile_double = instance_double(Pathname, exist?: false) + expect(Pathname).to receive(:new).with("#{version}.Dockerfile").and_return(dockerfile_double) + expect(dockerfile_double).to receive(:open).with('w').and_yield(dockerfile_double) + expect(dockerfile_double).to receive(:write).with("ARG VER=#{version}") + + expect_exec_cmd("git add #{version}.Dockerfile") + expect_exec_cmd("git commit -m 'Release cut #{version}'") + + expect_exec_cmd("git push origin #{version}") + + subject.single + end + end + end + + describe '#update_versions_dropdown' do + context 'when in DRY_RUN mode' do + let(:dry_run) { true } + + it 'does not update the versions dropdown' do + expect_info("DRY RUN: Not updating the version dropdown...") + expect(File).not_to receive(:write) + + subject.update_versions_dropdown + end + end + + context 'when not in DRY_RUN mode' do + let(:dry_run) { false } + + it 'updates content/versions.json' do + expected_json = [{ "next" => "16.0", "current" => "15.11", "last_minor" => ["15.10", "15.9"], "last_major" => ["14.10", "13.12"] }] + + expect_info("Updating content/versions.json...") + expect(File).to receive(:write).with('content/versions.json', JSON.pretty_generate(expected_json)) + + subject.update_versions_dropdown + end + end + end + + def expect_info(msg) + expect(task_helpers).to receive(:info).with('gitlab-docs', msg) + end + + def expect_exec_cmd(cmd) + expect(subject).to receive(:exec_cmd).with(cmd) + end +end |