decidim/decidim

View on GitHub
lib/decidim/gem_manager.rb

Summary

Maintainability
A
0 mins
Test Coverage
# frozen_string_literal: true

require "active_support/core_ext/module/delegation"
require "open3"

module Decidim
  #
  # Handles a decidim components.
  #
  # Allows to perform custom operations on all modules in a given folder, or on
  # specific module folders.
  #
  # Potential operations are:
  #
  # * Running custom commands, via the `run` method, such as releasing it, or
  #   installing it locally.
  #
  # * Updating version files from the main `.decidim-version` file in the root
  #   of the repository.
  #
  class GemManager
    PARTICIPATORY_SPACES = %w(
      participatory_processes
      assemblies
      conferences
    ).freeze

    COMPONENTS = %w(
      accountability
      budgets
      debates
      meetings
      pages
      proposals
      surveys
      sortitions
      blogs
    ).freeze

    def initialize(dir)
      @dir = File.expand_path(dir)
    end

    def run(command, out: $stdout)
      interpolated_in_folder(command) do |cmd|
        self.class.run(cmd, out:)
      end
    end

    def capture(command, env: {}, with_stderr: true)
      interpolated_in_folder(command) do |cmd|
        self.class.capture(cmd, env:, with_stderr:)
      end
    end

    def replace_gem_version
      Dir.chdir(@dir) do
        replace_file(
          "lib/#{name.tr("-", "/")}/version.rb",
          /def self\.version(\s*)"[^"]*"/,
          "def self.version\\1\"#{version}\""
        )
      end
    end

    def replace_package_version
      Dir.chdir(@dir) do
        replace_file(
          "package.json",
          /^  "version": "[^"]*"/,
          "  \"version\": \"#{semver_friendly_version(version)}\""
        )
      end
    end

    def short_name
      name.gsub(/decidim-/, "")
    end

    class << self
      def capture(cmd, env: {}, with_stderr: true)
        return Open3.capture2e(env, cmd) if with_stderr

        Open3.capture2(env, cmd)
      end

      def run(cmd, out: $stdout)
        system(cmd, out:)
      end

      def test_participatory_space
        new("decidim-#{PARTICIPATORY_SPACES.sample}").run("rake")
      end

      def test_component
        new("decidim-#{COMPONENTS.sample}").run("rake")
      end

      def replace_versions
        all_dirs do |dir|
          new(dir).replace_gem_version
        end

        package_dirs do |dir|
          new(dir).replace_package_version
        end

        replace_antora_version
      end

      def install_all(out: $stdout)
        run_all(
          "gem build %name && mv %name-%version.gem ..",
          include_root: false,
          out:
        )

        new(root).run(
          "gem build %name && gem install *.gem",
          out:
        )
      end

      def uninstall_all(out: $stdout)
        run_all(
          "gem uninstall %name -v %version --executables --force",
          out:
        )

        new(root).run(
          "rm decidim-*.gem",
          out:
        )
      end

      def run_all(command, out: $stdout, include_root: true)
        all_dirs(include_root:) do |dir|
          status = run_at(dir, command, out:)

          break if !status && fail_fast?
        end
      end

      def run_packages(command, out: $stdout)
        package_dirs do |dir|
          status = run_at(dir, command, out:)

          break if !status && fail_fast?
        end
      end

      def run_at(dir, command, out: $stdout)
        attempts = 0
        until (status = new(dir).run(command, out:))
          attempts += 1

          break if attempts > Decidim::GemManager.retry_times
        end

        status
      end

      def version
        @version ||= File.read(version_file).strip
      end

      def replace_file(name, regexp, replacement)
        new_content = File.read(name).gsub(regexp, replacement)

        File.write(name, new_content)
      end

      def all_dirs(include_root: true)
        dirs = plugins
        dirs << "./" if include_root

        dirs.each { |dir| yield(dir) }
      end

      def package_dirs
        dirs = Dir.glob("#{root}/packages/*")

        dirs.each { |dir| yield(dir) }
      end

      def plugins
        Dir.glob("#{root}/decidim-*/")
      end

      def semver_friendly_version(a_version)
        a_version.gsub(/\.pre/, "-pre").gsub(/\.dev/, "-dev").gsub(/.rc(\d*)/, "-rc\\1")
      end

      def fail_fast?
        ENV.fetch("FAIL_FAST", nil) != "false"
      end

      def retry_times
        ENV.fetch("RETRY_TIMES", 10).to_i
      end

      def patch_component_gemfile_generator
        content = %(# frozen_string_literal: true
<%# This file is automatically generated. Do not edit this file, it will be overwritten
If you want to modify it you can do it in the Decidim::GemManager.patch_component_gemfile_generator method %>
source "https://rubygems.org"

ruby RUBY_VERSION

gem "decidim", "~> #{version}"
gem "decidim-<%= component_name %>", path: "."

gem "puma", "#{fetch_gemfile_version("puma")}"
gem "bootsnap", "#{fetch_gemfile_version("bootsnap")}"

group :development, :test do
  gem "byebug", "#{fetch_gemfile_version("byebug")}", platform: :mri

  gem "decidim-dev", "~> #{version}"
end

group :development do
  gem "faker", "#{Gem.loaded_specs["decidim-dev"].dependencies.select { |a| a.name == "faker" }.first.requirement}"
  gem "letter_opener_web", "#{fetch_gemfile_version("letter_opener_web")}"
  gem "listen", "#{fetch_gemfile_version("listen")}"
  gem "spring", "#{fetch_gemfile_version("spring")}"
  gem "spring-watcher-listen", "#{fetch_gemfile_version("spring-watcher-listen")}"
  gem "web-console", "#{fetch_gemfile_version("web-console")}"
end
)

        file_path = File.join(root, "decidim-generators/lib/decidim/generators/component_templates/Gemfile.erb")
        File.write(file_path, content)
      end

      private

      def fetch_gemfile_version(bundle_name)
        Bundler.definition.dependencies.select { |a| a.name == bundle_name }.first.requirement.to_s
      end

      def root
        File.expand_path(File.join("..", ".."), __dir__)
      end

      def version_file
        File.join(root, ".decidim-version")
      end

      def replace_antora_version
        antora_conf = File.join(root, "docs/antora.yml")
        docs_version = version.match?(/\.dev$/) ? "develop" : "v#{version.match(/(\d*).(\d*)/)[0]}"
        File.write(antora_conf, File.read(antora_conf).sub(/^version: .+/, "version: #{docs_version}"))
      end
    end

    private

    delegate :plugins, :replace_file, :semver_friendly_version, :version, to: :class

    def interpolated_in_folder(command)
      Dir.chdir(@dir) do
        yield command.gsub("%version", version).gsub("%name", name)
      end
    end

    def folder_name
      File.basename(@dir)
    end

    def name
      plugins.map { |name| File.expand_path(name) }.include?(@dir) ? folder_name : "decidim"
    end
  end
end