├── .rspec ├── .config └── cucumber.yml ├── spec ├── spec_helper.rb ├── helpers │ ├── faker.rb │ └── faker │ │ └── puppet_module_remote_repo.rb └── unit │ ├── module_sync_spec.rb │ └── module_sync │ ├── source_code_spec.rb │ ├── git_service │ ├── factory_spec.rb │ ├── github_spec.rb │ └── gitlab_spec.rb │ ├── settings_spec.rb │ └── git_service_spec.rb ├── .rubocop.yml ├── .gitignore ├── bin └── msync ├── contrib └── openstack-commit-msg-hook.sh ├── Gemfile ├── features ├── support │ └── env.rb ├── hook.feature ├── update │ ├── bad_context.feature │ ├── dot_sync.feature │ ├── bump_version.feature │ └── pull_request.feature ├── cli.feature ├── push.feature ├── execute.feature ├── reset.feature ├── step_definitions │ └── git_steps.rb └── update.feature ├── lib ├── modulesync │ ├── constants.rb │ ├── util.rb │ ├── git_service │ │ ├── factory.rb │ │ ├── github.rb │ │ ├── gitlab.rb │ │ └── base.rb │ ├── renderer.rb │ ├── hook.rb │ ├── cli │ │ └── thor.rb │ ├── puppet_module.rb │ ├── settings.rb │ ├── source_code.rb │ ├── git_service.rb │ ├── repository.rb │ └── cli.rb ├── monkey_patches.rb └── modulesync.rb ├── .github ├── dependabot.yml ├── release.yml └── workflows │ ├── ci.yml │ └── release.yml ├── modulesync.gemspec ├── Rakefile ├── .rubocop_todo.yml ├── HISTORY.md ├── LICENSE ├── README.md └── CHANGELOG.md /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format=documentation 3 | -------------------------------------------------------------------------------- /.config/cucumber.yml: -------------------------------------------------------------------------------- 1 | default: --publish-quiet 2 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'modulesync' 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | --- 2 | inherit_from: .rubocop_todo.yml 3 | 4 | inherit_gem: 5 | voxpupuli-rubocop: rubocop.yml 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle/ 3 | .ruby-version 4 | .vscode/ 5 | Gemfile.lock 6 | bin/bundle 7 | bin/rspec 8 | coverage/ 9 | modules/ 10 | tmp/ 11 | vendor/ 12 | .vendor/ 13 | -------------------------------------------------------------------------------- /bin/msync: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | lib = File.expand_path('../lib', __dir__) 5 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 6 | 7 | require 'modulesync/cli' 8 | 9 | ModuleSync::CLI::Base.start(ARGV) 10 | -------------------------------------------------------------------------------- /contrib/openstack-commit-msg-hook.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | commit_msg_path=$1/.git/hooks/commit-msg 4 | 5 | if [ ! -f $commit_msg_path ]; then 6 | curl -s -Lo $commit_msg_path http://review.openstack.org/tools/hooks/commit-msg 7 | chmod 775 $commit_msg_path 8 | fi 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source ENV['GEM_SOURCE'] || 'https://rubygems.org' 4 | 5 | gemspec 6 | 7 | group :release, optional: true do 8 | gem 'faraday-retry', '~> 2.1', require: false 9 | gem 'github_changelog_generator', '~> 1.16', require: false 10 | end 11 | -------------------------------------------------------------------------------- /features/support/env.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'aruba/cucumber' 4 | 5 | require_relative '../../spec/helpers/faker' 6 | 7 | ModuleSync::Faker.working_directory = File.expand_path('faker', Aruba.config.working_directory) 8 | 9 | Before do 10 | @aruba_timeout_seconds = 5 11 | 12 | aruba.config.activate_announcer_on_command_failure = %i[stdout stderr] 13 | end 14 | -------------------------------------------------------------------------------- /lib/modulesync/constants.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ModuleSync 4 | module Constants 5 | MODULE_FILES_DIR = 'moduleroot/' 6 | CONF_FILE = 'config_defaults.yml' 7 | MODULE_CONF_FILE = '.sync.yml' 8 | MODULESYNC_CONF_FILE = 'modulesync.yml' 9 | HOOK_FILE = '.git/hooks/pre-push' 10 | GLOBAL_DEFAULTS_KEY = :global 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # raise PRs for gem updates 4 | - package-ecosystem: bundler 5 | directory: "/" 6 | schedule: 7 | interval: daily 8 | time: "13:00" 9 | open-pull-requests-limit: 10 10 | 11 | # Maintain dependencies for GitHub Actions 12 | - package-ecosystem: github-actions 13 | directory: "/" 14 | schedule: 15 | interval: daily 16 | time: "13:00" 17 | open-pull-requests-limit: 10 18 | -------------------------------------------------------------------------------- /spec/helpers/faker.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ModuleSync 4 | # Faker is a top-level module to keep global faker config 5 | module Faker 6 | def self.working_directory=(path) 7 | @working_directory = path 8 | end 9 | 10 | def self.working_directory 11 | raise 'Working directory must be set' if @working_directory.nil? 12 | 13 | FileUtils.mkdir_p @working_directory 14 | @working_directory 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/monkey_patches.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module LibMonkeyPatch 5 | # Monkey patch set_custom_git_env_variables due to our ::Git::Error handling. 6 | # 7 | # We rescue on the Git::Error and proceed differently based on the output of git. 8 | # This way makes code language-dependent, so here we ensure that Git gem throw git commands with the "C" language 9 | def set_custom_git_env_variables 10 | super 11 | ENV['LANG'] = 'C.UTF-8' 12 | end 13 | end 14 | 15 | class Lib 16 | prepend LibMonkeyPatch 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/unit/module_sync_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe ModuleSync do 6 | context '::update' do 7 | it 'loads the managed modules from the specified :managed_modules_conf' do 8 | allow(described_class).to receive(:find_template_files).and_return([]) 9 | allow(ModuleSync::Util).to receive(:parse_config).with('./config_defaults.yml').and_return({}) 10 | expect(described_class).to receive(:managed_modules).with(no_args).and_return([]) 11 | 12 | options = { managed_modules_conf: 'test_file.yml' } 13 | described_class.update(options) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/unit/module_sync/source_code_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe ModuleSync::SourceCode do 6 | subject do 7 | described_class.new('namespace/name', nil) 8 | end 9 | 10 | before do 11 | options = ModuleSync.config_defaults.merge({ git_base: 'file:///tmp/dummy' }) 12 | ModuleSync.instance_variable_set :@options, options 13 | end 14 | 15 | it 'has a repository namespace sets to "namespace"' do 16 | expect(subject.repository_namespace).to eq 'namespace' 17 | end 18 | 19 | it 'has a repository name sets to "name"' do 20 | expect(subject.repository_name).to eq 'name' 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/unit/module_sync/git_service/factory_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'modulesync' 4 | require 'modulesync/git_service/factory' 5 | 6 | describe ModuleSync::GitService::Factory do 7 | context 'when instantiate a GitHub service without credentials' do 8 | it 'raises an error' do 9 | expect do 10 | described_class.instantiate(type: :github, endpoint: nil, token: nil) 11 | end.to raise_error(ModuleSync::GitService::MissingCredentialsError) 12 | end 13 | end 14 | 15 | context 'when instantiate a GitLab service without credentials' do 16 | it 'raises an error' do 17 | expect do 18 | described_class.instantiate(type: :gitlab, endpoint: nil, token: nil) 19 | end.to raise_error(ModuleSync::GitService::MissingCredentialsError) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/modulesync/util.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'yaml' 4 | 5 | module ModuleSync 6 | module Util 7 | def self.symbolize_keys(hash) 8 | hash.each_with_object({}) do |(k, v), memo| 9 | memo[k.to_sym] = v.is_a?(Hash) ? symbolize_keys(v) : v 10 | end 11 | end 12 | 13 | def self.parse_config(config_file) 14 | if File.exist?(config_file) 15 | YAML.load_file(config_file, aliases: true) || {} 16 | else 17 | puts "No config file under #{config_file} found, using default values" 18 | {} 19 | end 20 | end 21 | 22 | def self.parse_list(option_value) 23 | case option_value 24 | when String 25 | option_value.split(',') 26 | when Array 27 | option_value 28 | else 29 | [] 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /features/hook.feature: -------------------------------------------------------------------------------- 1 | Feature: hook 2 | ModuleSync needs to update git pre-push hooks 3 | 4 | Scenario: Activating a hook 5 | Given a directory named ".git/hooks" 6 | When I successfully run `msync hook activate` 7 | Then the file named ".git/hooks/pre-push" should contain "bash" 8 | 9 | Scenario: Deactivating a hook 10 | Given a file named ".git/hooks/pre-push" with: 11 | """ 12 | git hook 13 | """ 14 | When I successfully run `msync hook deactivate` 15 | Then the file ".git/hooks/pre-push" should not exist 16 | 17 | Scenario: Activating a hook with arguments 18 | Given a directory named ".git/hooks" 19 | When I successfully run `msync hook activate -a '--foo bar --baz quux' -b master` 20 | Then the file named ".git/hooks/pre-push" should contain: 21 | """ 22 | "$message" -n puppetlabs -b master --foo bar --baz quux 23 | """ 24 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes 3 | 4 | changelog: 5 | exclude: 6 | labels: 7 | - duplicate 8 | - invalid 9 | - modulesync 10 | - question 11 | - skip-changelog 12 | - wont-fix 13 | - wontfix 14 | - github_actions 15 | 16 | categories: 17 | - title: Breaking Changes 🛠 18 | labels: 19 | - backwards-incompatible 20 | 21 | - title: New Features 🎉 22 | labels: 23 | - enhancement 24 | 25 | - title: Bug Fixes 🐛 26 | labels: 27 | - bug 28 | - bugfix 29 | 30 | - title: Documentation Updates 📚 31 | labels: 32 | - documentation 33 | - docs 34 | 35 | - title: Dependency Updates ⬆️ 36 | labels: 37 | - dependencies 38 | 39 | - title: Other Changes 40 | labels: 41 | - "*" 42 | -------------------------------------------------------------------------------- /lib/modulesync/git_service/factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ModuleSync 4 | module GitService 5 | # Git service's factory 6 | module Factory 7 | def self.instantiate(type:, endpoint:, token:) 8 | raise MissingCredentialsError, <<~MESSAGE if token.nil? 9 | A token is required to use services from #{type}: 10 | Please set environment variable: "#{type.upcase}_TOKEN" or set the token entry in module options. 11 | MESSAGE 12 | 13 | klass(type: type).new token, endpoint 14 | end 15 | 16 | def self.klass(type:) 17 | case type 18 | when :github 19 | require 'modulesync/git_service/github' 20 | ModuleSync::GitService::GitHub 21 | when :gitlab 22 | require 'modulesync/git_service/gitlab' 23 | ModuleSync::GitService::GitLab 24 | else 25 | raise NotImplementedError, "Unknown git service: '#{type}'" 26 | end 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/unit/module_sync/settings_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe ModuleSync::Settings do 6 | subject do 7 | described_class.new( 8 | {}, 9 | {}, 10 | {}, 11 | { 'Rakefile' => { 'unmanaged' => true }, 12 | :global => { 'global' => 'value' }, 13 | 'Gemfile' => { 'key' => 'value' }, }, 14 | {}, 15 | ) 16 | end 17 | 18 | it { is_expected.not_to be_nil } 19 | it { expect(subject.managed?('Rakefile')).to be false } 20 | it { expect(subject.managed?('Rakefile/foo')).to be false } 21 | it { expect(subject.managed?('Gemfile')).to be true } 22 | it { expect(subject.managed?('Gemfile/foo')).to be true } 23 | it { expect(subject.managed_files([])).to eq ['Gemfile'] } 24 | it { expect(subject.managed_files(%w[Rakefile Gemfile other_file])).to eq %w[Gemfile other_file] } 25 | it { expect(subject.unmanaged_files([])).to eq ['Rakefile'] } 26 | it { expect(subject.unmanaged_files(%w[Rakefile Gemfile other_file])).to eq ['Rakefile'] } 27 | end 28 | -------------------------------------------------------------------------------- /lib/modulesync/renderer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'erb' 4 | require 'find' 5 | 6 | module ModuleSync 7 | module Renderer 8 | class ForgeModuleFile 9 | def initialize(configs = {}, metadata = {}) 10 | @configs = configs 11 | @metadata = metadata 12 | end 13 | end 14 | 15 | def self.build(template_file) 16 | template = File.read(template_file) 17 | erb_obj = ERB.new(template, trim_mode: '-') 18 | erb_obj.filename = template_file 19 | erb_obj.def_method(ForgeModuleFile, 'render()', template_file) 20 | erb_obj 21 | end 22 | 23 | def self.remove(file) 24 | FileUtils.rm_f(file) 25 | end 26 | 27 | def self.render(_template, configs = {}, metadata = {}) 28 | ForgeModuleFile.new(configs, metadata).render 29 | end 30 | 31 | def self.sync(template, target_name, mode = nil) 32 | FileUtils.mkdir_p(File.dirname(target_name)) 33 | File.write(target_name, template) 34 | File.chmod(mode, target_name) unless mode.nil? 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /features/update/bad_context.feature: -------------------------------------------------------------------------------- 1 | Feature: Run `msync update` without a good context 2 | 3 | Scenario: Run `msync update` without any module 4 | Given a directory named "moduleroot" 5 | When I run `msync update --message "In a bad context"` 6 | Then the exit status should be 1 7 | And the stderr should contain: 8 | """ 9 | No modules found 10 | """ 11 | 12 | Scenario: Run `msync update` without the "moduleroot" directory 13 | Given a basic setup with a puppet module "puppet-test" from "fakenamespace" 14 | When I run `msync update --message "In a bad context"` 15 | Then the exit status should be 1 16 | And the stderr should contain "moduleroot" 17 | 18 | Scenario: Run `msync update` without commit message 19 | Given a basic setup with a puppet module "puppet-test" from "fakenamespace" 20 | And a directory named "moduleroot" 21 | When I run `msync update` 22 | Then the exit status should be 1 23 | And the stderr should contain: 24 | """ 25 | No value provided for required option "--message" 26 | """ 27 | -------------------------------------------------------------------------------- /lib/modulesync/hook.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'modulesync' 4 | 5 | module ModuleSync 6 | class Hook 7 | attr_reader :hook_file, :namespace, :branch, :args 8 | 9 | def initialize(hook_file, options = []) 10 | @hook_file = hook_file 11 | @namespace = options[:namespace] 12 | @branch = options[:branch] 13 | @args = options[:hook_args] 14 | end 15 | 16 | def content(arguments) 17 | <<~CONTENT 18 | #!/usr/bin/env bash 19 | 20 | current_branch=`git symbolic-ref HEAD | sed -e 's,.*/(.*),\1,'` 21 | git_dir=`git rev-parse --show-toplevel` 22 | message=`git log -1 --format=%B` 23 | msync -m "$message" #{arguments} 24 | CONTENT 25 | end 26 | 27 | def activate 28 | hook_args = [] 29 | hook_args << "-n #{namespace}" if namespace 30 | hook_args << "-b #{branch}" if branch 31 | hook_args << args if args 32 | 33 | File.write(hook_file, content(hook_args.join(' '))) 34 | end 35 | 36 | def deactivate 37 | FileUtils.rm_f(hook_file) 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/modulesync/cli/thor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'thor' 4 | require 'modulesync/cli' 5 | 6 | module ModuleSync 7 | module CLI 8 | # Workaround some, still unfixed, Thor behaviors 9 | # 10 | # This class extends ::Thor class to 11 | # - exit with status code sets to `1` on Thor failure (e.g. missing required option) 12 | # - exit with status code sets to `1` when user calls `msync` (or a subcommand) without required arguments 13 | # - show subcommands help using `msync subcommand --help` 14 | class Thor < ::Thor 15 | def self.start(*args) 16 | if Thor::HELP_MAPPINGS.intersect?(ARGV) && subcommands.none? { |command| command.start_with?(ARGV[0]) } 17 | Thor::HELP_MAPPINGS.each do |cmd| 18 | if (match = ARGV.delete(cmd)) 19 | ARGV.unshift match 20 | end 21 | end 22 | end 23 | super 24 | end 25 | 26 | desc '_invalid_command_call', 'Invalid command', hide: true 27 | def _invalid_command_call 28 | self.class.new.help 29 | exit 1 30 | end 31 | default_task :_invalid_command_call 32 | 33 | def self.exit_on_failure? 34 | true 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/modulesync/puppet_module.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'puppet_blacksmith' 4 | 5 | require 'modulesync/source_code' 6 | 7 | module ModuleSync 8 | # Provide methods to manipulate puppet module code 9 | class PuppetModule < SourceCode 10 | def update_changelog(version, message) 11 | changelog = path('CHANGELOG.md') 12 | if File.exist?(changelog) 13 | puts "Updating #{changelog} for version #{version}" 14 | changes = File.readlines(changelog) 15 | File.open(changelog, 'w') do |f| 16 | date = Time.now.strftime('%Y-%m-%d') 17 | f.puts "## #{date} - Release #{version}\n\n" 18 | f.puts "#{message}\n\n" 19 | # Add old lines again 20 | f.puts changes 21 | end 22 | repository.git.add('CHANGELOG.md') 23 | else 24 | puts 'No CHANGELOG.md file found, not updating.' 25 | end 26 | end 27 | 28 | def bump(message, changelog = false) 29 | m = Blacksmith::Modulefile.new path('metadata.json') 30 | new = m.bump! 31 | puts "Bumped to version #{new}" 32 | repository.git.add('metadata.json') 33 | update_changelog(new, message) if changelog 34 | repository.git.commit("Release version #{new}") 35 | repository.git.push 36 | new 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /modulesync.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path('lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'modulesync' 8 | spec.version = '4.2.0' 9 | spec.authors = ['Vox Pupuli'] 10 | spec.email = ['voxpupuli@groups.io'] 11 | spec.summary = 'Puppet Module Synchronizer' 12 | spec.description = 'Utility to synchronize common files across puppet modules in Github.' 13 | spec.homepage = 'https://github.com/voxpupuli/modulesync' 14 | spec.license = 'Apache-2.0' 15 | spec.required_ruby_version = '>= 3.2.0' 16 | 17 | spec.files = `git ls-files -z`.split("\x0") 18 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 19 | spec.require_paths = ['lib'] 20 | 21 | spec.add_development_dependency 'aruba', '~>2.0' 22 | spec.add_development_dependency 'cucumber', '~> 10.1' 23 | spec.add_development_dependency 'rake', '~> 13.2', '>= 13.2.1' 24 | spec.add_development_dependency 'rspec', '~> 3.13' 25 | spec.add_development_dependency 'voxpupuli-rubocop', '~> 5.1.0' 26 | 27 | spec.add_dependency 'git', '>= 3.0', '< 5' 28 | spec.add_dependency 'gitlab', '>=4', '<7' 29 | spec.add_dependency 'octokit', '>=4', '<11' 30 | spec.add_dependency 'puppet-blacksmith', '>= 3.0', '< 10' 31 | spec.add_dependency 'thor', '~> 1.4' 32 | end 33 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rake/clean' 4 | require 'cucumber/rake/task' 5 | require 'rubocop/rake_task' 6 | 7 | begin 8 | require 'rspec/core/rake_task' 9 | RSpec::Core::RakeTask.new(:spec) 10 | rescue LoadError 11 | puts 'rspec not installed - skipping unit test task setup' 12 | end 13 | 14 | begin 15 | require 'voxpupuli/rubocop/rake' 16 | rescue LoadError 17 | # RuboCop is an optional group 18 | end 19 | 20 | CLEAN.include('pkg/', 'tmp/') 21 | 22 | Cucumber::Rake::Task.new do |t| 23 | t.cucumber_opts = '' 24 | t.cucumber_opts << '--format pretty' 25 | end 26 | 27 | task test: %i[clean spec cucumber rubocop] 28 | task default: %i[test] 29 | 30 | begin 31 | require 'github_changelog_generator/task' 32 | GitHubChangelogGenerator::RakeTask.new :changelog do |config| 33 | config.header = "# Changelog\n\nAll notable changes to this project will be documented in this file." 34 | config.exclude_labels = %w[duplicate question invalid wontfix wont-fix modulesync skip-changelog github_actions] 35 | config.user = 'voxpupuli' 36 | config.project = 'modulesync' 37 | config.future_release = Gem::Specification.load("#{config.project}.gemspec").version 38 | end 39 | 40 | # Workaround for https://github.com/github-changelog-generator/github-changelog-generator/issues/715 41 | require 'rbconfig' 42 | if RbConfig::CONFIG['host_os'].include?('linux') 43 | task :changelog do 44 | puts 'Fixing line endings...' 45 | changelog_file = File.join(__dir__, 'CHANGELOG.md') 46 | changelog_txt = File.read(changelog_file) 47 | new_contents = changelog_txt.gsub("\r\n", "\n") 48 | File.open(changelog_file, 'w') { |file| file.puts new_contents } 49 | end 50 | end 51 | rescue LoadError 52 | end 53 | -------------------------------------------------------------------------------- /features/cli.feature: -------------------------------------------------------------------------------- 1 | Feature: CLI 2 | ModuleSync needs to have a robust command line interface 3 | 4 | Scenario: When passing no arguments to the msync command 5 | When I run `msync` 6 | And the output should match /Commands:/ 7 | Then the exit status should be 1 8 | 9 | Scenario: When passing invalid arguments to the msync update command 10 | When I run `msync update` 11 | And the output should match /No value provided for required option/ 12 | Then the exit status should be 1 13 | 14 | Scenario: When passing invalid arguments to the msync hook command 15 | When I run `msync hook` 16 | And the output should match /Commands:/ 17 | Then the exit status should be 1 18 | 19 | Scenario: When running the help command 20 | When I successfully run `msync help` 21 | Then the output should match /Commands:/ 22 | 23 | Scenario: Use --help options on subcommand should show subcommand help 24 | When I successfully run `msync clone --help` 25 | Then the output should contain: 26 | """ 27 | Usage: 28 | msync clone 29 | """ 30 | 31 | Scenario: When overriding a setting from the config file on the command line 32 | Given a puppet module "puppet-test" from "fakenamespace" 33 | And a file named "managed_modules.yml" with: 34 | """ 35 | --- 36 | - puppet-test 37 | """ 38 | And a file named "modulesync.yml" with: 39 | """ 40 | --- 41 | namespace: default 42 | """ 43 | And a git_base option appended to "modulesync.yml" for local tests 44 | And a directory named "moduleroot" 45 | When I successfully run `msync update --verbose --noop --namespace fakenamespace --branch command-line-branch` 46 | Then the output should contain: 47 | """ 48 | Creating new branch command-line-branch from master 49 | """ 50 | -------------------------------------------------------------------------------- /features/push.feature: -------------------------------------------------------------------------------- 1 | Feature: push 2 | Push commits to remote 3 | 4 | Scenario: Push available commits to remote 5 | Given a mocked git configuration 6 | And a puppet module "puppet-test" from "awesome" 7 | And a file named "managed_modules.yml" with: 8 | """ 9 | --- 10 | puppet-test: 11 | namespace: awesome 12 | """ 13 | And a file named "modulesync.yml" with: 14 | """ 15 | --- 16 | branch: modulesync 17 | """ 18 | And a git_base option appended to "modulesync.yml" for local tests 19 | And I successfully run `msync reset` 20 | And I cd to "modules/awesome/puppet-test" 21 | And I run `touch hello` 22 | And I run `git add hello` 23 | And I run `git commit -m'Hello!'` 24 | And I cd to "~" 25 | Then the puppet module "puppet-test" from "awesome" should have no commits made by "Aruba" 26 | When I successfully run `msync push --verbose` 27 | Then the puppet module "puppet-test" from "awesome" should have 1 commit made by "Aruba" in branch "modulesync" 28 | 29 | Scenario: Push command without a branch sets 30 | Given a basic setup with a puppet module "puppet-test" from "awesome" 31 | When I run `msync push --verbose` 32 | Then the exit status should be 1 33 | And the stderr should contain: 34 | """ 35 | Error: 'branch' option is missing, please set it in configuration or in command line. 36 | """ 37 | 38 | Scenario: Report the need to clone repositories if sourcecode was not cloned before 39 | Given a basic setup with a puppet module "puppet-test" from "awesome" 40 | And the global option "branch" sets to "modulesync" 41 | When I run `msync push --verbose` 42 | Then the exit status should be 1 43 | And the stderr should contain: 44 | """ 45 | puppet-test: Repository must be locally available before trying to push 46 | """ 47 | -------------------------------------------------------------------------------- /lib/modulesync/settings.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ModuleSync 4 | # Encapsulate a configs for a module, providing easy access to its parts 5 | # All configs MUST be keyed by the relative target filename 6 | class Settings 7 | attr_reader :global_defaults, :defaults, :module_defaults, :module_configs, :additional_settings 8 | 9 | def initialize(global_defaults, defaults, module_defaults, module_configs, additional_settings) 10 | @global_defaults = global_defaults 11 | @defaults = defaults 12 | @module_defaults = module_defaults 13 | @module_configs = module_configs 14 | @additional_settings = additional_settings 15 | end 16 | 17 | def lookup_config(hash, target_name) 18 | hash[target_name] || {} 19 | end 20 | 21 | def build_file_configs(target_name) 22 | file_def = lookup_config(defaults, target_name) 23 | file_mc = lookup_config(module_configs, target_name) 24 | 25 | global_defaults.merge(file_def).merge(module_defaults).merge(file_mc).merge(additional_settings) 26 | end 27 | 28 | def managed?(target_name) 29 | Pathname.new(target_name).ascend do |v| 30 | configs = build_file_configs(v.to_s) 31 | return false if configs['unmanaged'] 32 | end 33 | true 34 | end 35 | 36 | # given a list of templates in the repo, return everything that we might want to act on 37 | def managed_files(target_name_list) 38 | (target_name_list | defaults.keys | module_configs.keys).select do |f| 39 | (f != ModuleSync::GLOBAL_DEFAULTS_KEY) && managed?(f) 40 | end 41 | end 42 | 43 | # returns a list of templates that should not be touched 44 | def unmanaged_files(target_name_list) 45 | (target_name_list | defaults.keys | module_configs.keys).select do |f| 46 | (f != ModuleSync::GLOBAL_DEFAULTS_KEY) && !managed?(f) 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: ⚒️ CI 3 | 4 | on: 5 | pull_request: {} 6 | push: 7 | branches: 8 | - master 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | rubocop_and_matrix: 15 | runs-on: ubuntu-24.04 16 | outputs: 17 | ruby: ${{ steps.ruby.outputs.versions }} 18 | steps: 19 | - uses: actions/checkout@v6 20 | - name: Setup ruby 21 | uses: ruby/setup-ruby@v1 22 | with: 23 | ruby-version: "3.4" 24 | bundler-cache: true 25 | - name: Run linter 26 | run: bundle exec rake rubocop 27 | - id: ruby 28 | uses: voxpupuli/ruby-version@v1 29 | 30 | test: 31 | name: "Ruby ${{ matrix.ruby }}" 32 | needs: rubocop_and_matrix 33 | runs-on: ubuntu-24.04 34 | strategy: 35 | fail-fast: false 36 | matrix: 37 | ruby: ${{ fromJSON(needs.rubocop_and_matrix.outputs.ruby) }} 38 | steps: 39 | - uses: actions/checkout@v6 40 | - name: Install Ruby ${{ matrix.ruby }} 41 | uses: ruby/setup-ruby@v1 42 | with: 43 | ruby-version: ${{ matrix.ruby }} 44 | bundler-cache: true 45 | - name: Run unit tests 46 | run: bundle exec rake spec 47 | - name: Run behavior tests 48 | run: bundle exec cucumber 49 | env: 50 | GIT_COMMITTER_NAME: 'Aruba' 51 | GIT_COMMITTER_EMAIL: 'aruba@example.com' 52 | GIT_AUTHOR_NAME: 'Aruba' 53 | GIT_AUTHOR_EMAIL: 'aruba@example.com' 54 | - name: Build gem 55 | run: gem build --strict --verbose *.gemspec 56 | 57 | tests: 58 | if: always() 59 | needs: 60 | - rubocop_and_matrix 61 | - test 62 | runs-on: ubuntu-24.04 63 | name: Test suite 64 | steps: 65 | - name: Decide whether the needed jobs succeeded or failed 66 | uses: re-actors/alls-green@release/v1 67 | with: 68 | jobs: ${{ toJSON(needs) }} 69 | -------------------------------------------------------------------------------- /lib/modulesync/git_service/github.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'modulesync/git_service' 4 | require 'modulesync/git_service/base' 5 | require 'octokit' 6 | 7 | module ModuleSync 8 | module GitService 9 | # GitHub creates and manages pull requests on github.com or GitHub 10 | # Enterprise installations. 11 | class GitHub < Base 12 | def initialize(token, endpoint) 13 | super() 14 | 15 | Octokit.configure do |c| 16 | c.api_endpoint = endpoint 17 | end 18 | @api = Octokit::Client.new(access_token: token) 19 | end 20 | 21 | private 22 | 23 | def _open_pull_request(repo_path:, namespace:, title:, message:, source_branch:, target_branch:, labels:, noop:) 24 | head = "#{namespace}:#{source_branch}" 25 | 26 | if noop 27 | $stdout.puts "Using no-op. Would submit PR '#{title}' to '#{repo_path}' " \ 28 | "- merges '#{source_branch}' into '#{target_branch}'" 29 | return 30 | end 31 | 32 | pull_requests = @api.pull_requests(repo_path, 33 | state: 'open', 34 | base: target_branch, 35 | head: head) 36 | unless pull_requests.empty? 37 | # Skip creating the PR if it exists already. 38 | $stdout.puts "Skipped! #{pull_requests.length} PRs found for branch '#{source_branch}'" 39 | return 40 | end 41 | 42 | pr = @api.create_pull_request(repo_path, 43 | target_branch, 44 | source_branch, 45 | title, 46 | message) 47 | $stdout.puts \ 48 | "Submitted PR '#{title}' to '#{repo_path}' " \ 49 | "- merges #{source_branch} into #{target_branch}" 50 | 51 | # We only assign labels to the PR if we've discovered a list > 1. The labels MUST 52 | # already exist. We DO NOT create missing labels. 53 | return if labels.empty? 54 | 55 | $stdout.puts "Attaching the following labels to PR #{pr['number']}: #{labels.join(', ')}" 56 | @api.add_labels_to_an_issue(repo_path, pr['number'], labels) 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/modulesync/git_service/gitlab.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'gitlab' 4 | require 'modulesync/git_service' 5 | require 'modulesync/git_service/base' 6 | 7 | module ModuleSync 8 | module GitService 9 | # GitLab creates and manages merge requests on gitlab.com or private GitLab 10 | # installations. 11 | class GitLab < Base 12 | def initialize(token, endpoint) 13 | super() 14 | 15 | @api = Gitlab::Client.new( 16 | endpoint: endpoint, 17 | private_token: token, 18 | ) 19 | end 20 | 21 | def self.guess_endpoint_from(remote:) 22 | endpoint = super 23 | return nil if endpoint.nil? 24 | 25 | endpoint += '/api/v4' 26 | endpoint 27 | end 28 | 29 | private 30 | 31 | def _open_pull_request(repo_path:, namespace:, title:, message:, source_branch:, target_branch:, labels:, noop:) # rubocop:disable Lint/UnusedMethodArgument 32 | if noop 33 | $stdout.puts "Using no-op. Would submit MR '#{title}' to '#{repo_path}' " \ 34 | "- merges #{source_branch} into #{target_branch}" 35 | return 36 | end 37 | 38 | merge_requests = @api.merge_requests(repo_path, 39 | state: 'opened', 40 | source_branch: source_branch, 41 | target_branch: target_branch) 42 | unless merge_requests.empty? 43 | # Skip creating the MR if it exists already. 44 | $stdout.puts "Skipped! #{merge_requests.length} MRs found for branch '#{source_branch}'" 45 | return 46 | end 47 | 48 | mr = @api.create_merge_request(repo_path, 49 | title, 50 | source_branch: source_branch, 51 | target_branch: target_branch, 52 | labels: labels) 53 | $stdout.puts \ 54 | "Submitted MR '#{title}' to '#{repo_path}' " \ 55 | "- merges '#{source_branch}' into '#{target_branch}'" 56 | 57 | return if labels.empty? 58 | 59 | $stdout.puts "Attached the following labels to MR #{mr.iid}: #{labels.join(', ')}" 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/modulesync/git_service/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ModuleSync 4 | module GitService 5 | # Generic class for git services 6 | class Base 7 | def open_pull_request(repo_path:, namespace:, title:, message:, source_branch:, target_branch:, labels:, noop:) 8 | unless source_branch != target_branch 9 | raise ModuleSync::Error, 10 | "Unable to open a pull request with the same source and target branch: '#{source_branch}'" 11 | end 12 | 13 | _open_pull_request( 14 | repo_path: repo_path, 15 | namespace: namespace, 16 | title: title, 17 | message: message, 18 | source_branch: source_branch, 19 | target_branch: target_branch, 20 | labels: labels, 21 | noop: noop, 22 | ) 23 | end 24 | 25 | # This method attempts to guess the git service endpoint based on remote 26 | def self.guess_endpoint_from(remote:) 27 | hostname = extract_hostname(remote) 28 | return nil if hostname.nil? 29 | 30 | "https://#{hostname}" 31 | end 32 | 33 | # This method extracts hostname from URL like: 34 | # 35 | # - ssh://[user@]host.xz[:port]/path/to/repo.git/ 36 | # - git://host.xz[:port]/path/to/repo.git/ 37 | # - [user@]host.xz:path/to/repo.git/ 38 | # - http[s]://host.xz[:port]/path/to/repo.git/ 39 | # - ftp[s]://host.xz[:port]/path/to/repo.git/ 40 | # 41 | # Returns nil if 42 | # - /path/to/repo.git/ 43 | # - file:///path/to/repo.git/ 44 | # - any invalid URL 45 | def self.extract_hostname(url) 46 | return nil if url.start_with?('/', 'file://') # local path (e.g. file:///path/to/repo) 47 | 48 | unless url.start_with?(%r{[a-z]+://}) # SSH notation does not contain protocol (e.g. user@server:path/to/repo/) 49 | pattern = /^(?.*@)?(?[\w|.]*):(?.*)$/ # SSH path (e.g. user@server:repo) 50 | return url.match(pattern)[:hostname] if url.match?(pattern) 51 | end 52 | 53 | URI.parse(url).host 54 | rescue URI::InvalidURIError 55 | nil 56 | end 57 | 58 | protected 59 | 60 | def _open_pull_request(repo_path:, namespace:, title:, message:, source_branch:, target_branch:, labels:, noop:) 61 | raise NotImplementedError 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /features/update/dot_sync.feature: -------------------------------------------------------------------------------- 1 | Feature: Update using a `.sync.yml` file 2 | ModuleSync needs to apply templates according to `.sync.yml` content 3 | 4 | Scenario: Updating a module with a .sync.yml file 5 | Given a basic setup with a puppet module "puppet-test" from "fakenamespace" 6 | And a file named "config_defaults.yml" with: 7 | """ 8 | --- 9 | :global: 10 | variable: 'global' 11 | template_with_specific_config: 12 | variable: 'specific' 13 | """ 14 | And a file named "moduleroot/template_with_specific_config.erb" with: 15 | """ 16 | --- 17 | <%= @configs['variable'] %> 18 | """ 19 | And a file named "moduleroot/template_with_only_global_config.erb" with: 20 | """ 21 | --- 22 | <%= @configs['variable'] %> 23 | """ 24 | And the puppet module "puppet-test" from "fakenamespace" has a branch named "target" 25 | And the puppet module "puppet-test" from "fakenamespace" has, in branch "target", a file named ".sync.yml" with: 26 | """ 27 | --- 28 | :global: 29 | variable: 'overwritten by globally defined value in .sync.yml' 30 | template_with_specific_config: 31 | variable: 'overwritten by file-specific defined value in .sync.yml' 32 | """ 33 | When I successfully run `msync update --message 'Apply ModuleSync templates to target source code' --branch 'target'` 34 | Then the file named "modules/fakenamespace/puppet-test/template_with_specific_config" should contain: 35 | """ 36 | overwritten by file-specific defined value in .sync.yml 37 | """ 38 | And the puppet module "puppet-test" from "fakenamespace" should have 1 commit made by "Aruba" 39 | When the puppet module "puppet-test" from "fakenamespace" has, in branch "target", a file named ".sync.yml" with: 40 | """ 41 | --- 42 | :global: 43 | variable: 'overwritten by globally defined value in .sync.yml' 44 | template_with_specific_config: 45 | variable: 'overwritten by newly file-specific defined value in .sync.yml' 46 | """ 47 | And I successfully run `msync update --message 'Apply ModuleSync templates to target source code' --branch 'target'` 48 | Then the file named "modules/fakenamespace/puppet-test/template_with_specific_config" should contain: 49 | """ 50 | overwritten by newly file-specific defined value in .sync.yml 51 | """ 52 | And the puppet module "puppet-test" from "fakenamespace" should have 2 commits made by "Aruba" 53 | -------------------------------------------------------------------------------- /features/execute.feature: -------------------------------------------------------------------------------- 1 | Feature: execute 2 | Use ModuleSync to execute a custom script on each repositories 3 | 4 | Scenario: Cloning sourcecodes before running command when modules/ dir is empty 5 | Given a basic setup with a puppet module "puppet-test" from "awesome" 6 | Then the file "modules/awesome/puppet-test/metadata.json" should not exist 7 | When I successfully run `msync exec --verbose -- /bin/true` 8 | Then the stdout should contain "Cloning from 'file://" 9 | And the file "modules/awesome/puppet-test/metadata.json" should exist 10 | 11 | @no-clobber 12 | Scenario: No clones before running command when sourcecode have already been cloned 13 | Then the file "modules/awesome/puppet-test/metadata.json" should exist 14 | When I successfully run `msync exec --verbose /bin/true` 15 | Then the stdout should not contain "Cloning from 'file://" 16 | 17 | @no-clobber 18 | Scenario: When command run fails, fail fast if option defined 19 | When I run `msync exec --verbose --fail-fast -- /bin/false` 20 | Then the exit status should be 1 21 | And the stderr should contain: 22 | """ 23 | Command execution failed 24 | """ 25 | 26 | @no-clobber 27 | Scenario: When command run fails, run all and summarize errors if option fail-fast is not set 28 | When I run `msync exec --verbose --no-fail-fast -- /bin/false` 29 | Then the exit status should be 1 30 | And the stderr should contain: 31 | """ 32 | Error(s) during `execute` command: 33 | * 34 | """ 35 | 36 | Scenario: Show fail-fast default value in help 37 | When I successfully run `msync help exec` 38 | Then the stdout should contain: 39 | """ 40 | [--fail-fast], [--no-fail-fast], [--skip-fail-fast] # Abort the run after a command execution failure 41 | # Default: true 42 | """ 43 | 44 | Scenario: Override fail-fast default value using config file 45 | Given the global option "fail_fast" sets to "false" 46 | When I successfully run `msync help exec` 47 | Then the stdout should contain: 48 | """ 49 | [--fail-fast], [--no-fail-fast], [--skip-fail-fast] # Abort the run after a command execution failure 50 | """ 51 | # NOTE: It seems there is a Thor bug here: default value is missing in help when sets to 'false' 52 | 53 | Scenario: Show preserved bundle env var. 54 | Given a basic setup with a puppet module "puppet-test" from "awesome" 55 | And I set the environment variables to: 56 | | variable | value | 57 | | BUNLE_PATH | /opt/msync/bundle | 58 | When I successfully run `msync exec --env BUNLE_PATH -- env` 59 | Then the stdout should contain: 60 | """ 61 | BUNLE_PATH=/opt/msync/bundle 62 | """ 63 | -------------------------------------------------------------------------------- /lib/modulesync/source_code.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'modulesync' 4 | require 'modulesync/git_service' 5 | require 'modulesync/git_service/factory' 6 | require 'modulesync/repository' 7 | require 'modulesync/util' 8 | 9 | module ModuleSync 10 | # Provide methods to retrieve source code attributes 11 | class SourceCode 12 | attr_reader :given_name, :options 13 | 14 | def initialize(given_name, options) 15 | @options = Util.symbolize_keys(options || {}) 16 | 17 | @given_name = given_name 18 | 19 | return unless given_name.include?('/') 20 | 21 | @repository_name = given_name.split('/').last 22 | @repository_namespace = given_name.split('/')[0...-1].join('/') 23 | end 24 | 25 | def repository 26 | @repository ||= Repository.new directory: working_directory, remote: repository_remote 27 | end 28 | 29 | def repository_name 30 | @repository_name ||= given_name 31 | end 32 | 33 | def repository_namespace 34 | @repository_namespace ||= @options[:namespace] || ModuleSync.options[:namespace] 35 | end 36 | 37 | def repository_path 38 | @repository_path ||= "#{repository_namespace}/#{repository_name}" 39 | end 40 | 41 | def repository_remote 42 | @repository_remote ||= @options[:remote] || _repository_remote 43 | end 44 | 45 | def working_directory 46 | @working_directory ||= File.join(ModuleSync.options[:project_root], repository_path) 47 | end 48 | 49 | def path(*parts) 50 | File.join(working_directory, *parts) 51 | end 52 | 53 | def git_service 54 | return nil if git_service_configuration.nil? 55 | 56 | @git_service ||= GitService::Factory.instantiate(**git_service_configuration) 57 | end 58 | 59 | def git_service_configuration 60 | @git_service_configuration ||= GitService.configuration_for(sourcecode: self) 61 | rescue GitService::UnguessableTypeError 62 | nil 63 | end 64 | 65 | def open_pull_request 66 | git_service.open_pull_request( 67 | repo_path: repository_path, 68 | namespace: repository_namespace, 69 | title: ModuleSync.options[:pr_title], 70 | message: ModuleSync.options[:message], 71 | source_branch: ModuleSync.options[:remote_branch] || ModuleSync.options[:branch] || repository.default_branch, 72 | target_branch: ModuleSync.options[:pr_target_branch] || repository.default_branch, 73 | labels: ModuleSync::Util.parse_list(ModuleSync.options[:pr_labels]), 74 | noop: ModuleSync.options[:noop], 75 | ) 76 | end 77 | 78 | private 79 | 80 | def _repository_remote 81 | git_base = ModuleSync.options[:git_base] 82 | git_base.start_with?('file://') ? "#{git_base}#{repository_path}" : "#{git_base}#{repository_path}.git" 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /spec/unit/module_sync/git_service/github_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | require 'modulesync/git_service/github' 6 | 7 | describe ModuleSync::GitService::GitHub do 8 | context '::open_pull_request' do 9 | before do 10 | @client = double 11 | allow(Octokit::Client).to receive(:new).and_return(@client) 12 | @it = described_class.new('test', 'https://api.github.com') 13 | end 14 | 15 | let(:args) do 16 | { 17 | repo_path: 'test/modulesync', 18 | namespace: 'test', 19 | title: 'Test PR is submitted', 20 | message: 'Hello world', 21 | source_branch: 'test', 22 | target_branch: 'master', 23 | labels: labels, 24 | noop: false, 25 | } 26 | end 27 | 28 | let(:labels) { [] } 29 | 30 | it 'submits PR when --pr is set' do 31 | allow(@client).to receive(:pull_requests) 32 | .with(args[:repo_path], 33 | state: 'open', 34 | base: 'master', 35 | head: "#{args[:namespace]}:#{args[:source_branch]}").and_return([]) 36 | expect(@client).to receive(:create_pull_request) 37 | .with(args[:repo_path], 38 | 'master', 39 | args[:source_branch], 40 | args[:title], 41 | args[:message]).and_return({ 'html_url' => 'http://example.com/pulls/22' }) 42 | expect { @it.open_pull_request(**args) }.to output(/Submitted PR/).to_stdout 43 | end 44 | 45 | it 'skips submitting PR if one has already been issued' do 46 | pr = { 47 | 'title' => 'Test title', 48 | 'html_url' => 'https://example.com/pulls/44', 49 | 'number' => '44', 50 | } 51 | 52 | expect(@client).to receive(:pull_requests) 53 | .with(args[:repo_path], 54 | state: 'open', 55 | base: 'master', 56 | head: "#{args[:namespace]}:#{args[:source_branch]}").and_return([pr]) 57 | expect { @it.open_pull_request(**args) }.to output("Skipped! 1 PRs found for branch 'test'\n").to_stdout 58 | end 59 | 60 | context 'when labels are set' do 61 | let(:labels) { %w[HELLO WORLD] } 62 | 63 | it 'adds labels to PR' do 64 | allow(@client).to receive(:create_pull_request).and_return({ 'html_url' => 'http://example.com/pulls/22', 65 | 'number' => '44', }) 66 | allow(@client).to receive(:pull_requests) 67 | .with(args[:repo_path], 68 | state: 'open', 69 | base: 'master', 70 | head: "#{args[:namespace]}:#{args[:source_branch]}").and_return([]) 71 | expect(@client).to receive(:add_labels_to_an_issue) 72 | .with(args[:repo_path], 73 | '44', 74 | %w[HELLO WORLD]) 75 | expect do 76 | @it.open_pull_request(**args) 77 | end.to output(/Attaching the following labels to PR 44: HELLO, WORLD/).to_stdout 78 | end 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /spec/unit/module_sync/git_service/gitlab_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | require 'modulesync/git_service/gitlab' 6 | 7 | describe ModuleSync::GitService::GitLab do 8 | context '::open_pull_request' do 9 | before do 10 | @client = double 11 | allow(Gitlab::Client).to receive(:new).and_return(@client) 12 | @it = described_class.new('test', 'https://gitlab.com/api/v4') 13 | end 14 | 15 | let(:args) do 16 | { 17 | repo_path: 'test/modulesync', 18 | namespace: 'test', 19 | title: 'Test MR is submitted', 20 | message: 'Hello world', 21 | source_branch: 'test', 22 | target_branch: 'master', 23 | labels: labels, 24 | noop: false, 25 | } 26 | end 27 | 28 | let(:labels) { [] } 29 | 30 | it 'submits MR when --pr is set' do 31 | allow(@client).to receive(:merge_requests) 32 | .with(args[:repo_path], 33 | state: 'opened', 34 | source_branch: args[:source_branch], 35 | target_branch: 'master').and_return([]) 36 | 37 | expect(@client).to receive(:create_merge_request) 38 | .with(args[:repo_path], 39 | args[:title], 40 | labels: [], 41 | source_branch: args[:source_branch], 42 | target_branch: 'master').and_return({ 'html_url' => 'http://example.com/pulls/22' }) 43 | 44 | expect { @it.open_pull_request(**args) }.to output(/Submitted MR/).to_stdout 45 | end 46 | 47 | it 'skips submitting MR if one has already been issued' do 48 | mr = { 49 | 'title' => 'Test title', 50 | 'html_url' => 'https://example.com/pulls/44', 51 | 'iid' => '44', 52 | } 53 | 54 | expect(@client).to receive(:merge_requests) 55 | .with(args[:repo_path], 56 | state: 'opened', 57 | source_branch: args[:source_branch], 58 | target_branch: 'master').and_return([mr]) 59 | 60 | expect { @it.open_pull_request(**args) }.to output("Skipped! 1 MRs found for branch 'test'\n").to_stdout 61 | end 62 | 63 | context 'when labels are set' do 64 | let(:labels) { %w[HELLO WORLD] } 65 | 66 | it 'adds labels to MR' do 67 | mr = double 68 | allow(mr).to receive(:iid).and_return('42') 69 | 70 | expect(@client).to receive(:create_merge_request) 71 | .with(args[:repo_path], 72 | args[:title], 73 | labels: %w[HELLO WORLD], 74 | source_branch: args[:source_branch], 75 | target_branch: 'master').and_return(mr) 76 | 77 | allow(@client).to receive(:merge_requests) 78 | .with(args[:repo_path], 79 | state: 'opened', 80 | source_branch: args[:source_branch], 81 | target_branch: 'master').and_return([]) 82 | 83 | expect do 84 | @it.open_pull_request(**args) 85 | end.to output(/Attached the following labels to MR 42: HELLO, WORLD/).to_stdout 86 | end 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /features/reset.feature: -------------------------------------------------------------------------------- 1 | Feature: reset 2 | Reset all repositories 3 | 4 | Scenario: Running first reset to clone repositories 5 | Given a basic setup with a puppet module "puppet-test" from "awesome" 6 | And the global option "branch" sets to "modulesync" 7 | When I successfully run `msync reset --verbose` 8 | Then the output should contain "Cloning from 'file://" 9 | And the output should not contain "Hard-resetting any local changes to repository in" 10 | 11 | @no-clobber 12 | Scenario: Reset when sourcecodes have already been cloned 13 | Given the file "modules/awesome/puppet-test/metadata.json" should exist 14 | And the global option "branch" sets to "modulesync" 15 | When I successfully run `msync reset --verbose` 16 | Then the output should not contain "Cloning from 'file://" 17 | And the output should contain "Hard-resetting any local changes to repository in 'modules/awesome/puppet-test' from branch 'origin/master'" 18 | 19 | Scenario: Reset after an upstream file addition 20 | Given a basic setup with a puppet module "puppet-test" from "awesome" 21 | And the global option "branch" sets to "modulesync" 22 | And I successfully run `msync reset` 23 | Then the file "modules/awesome/puppet-test/hello" should not exist 24 | When the puppet module "puppet-test" from "awesome" has a file named "hello" with: 25 | """ 26 | Hello 27 | """ 28 | When I successfully run `msync reset --verbose` 29 | Then the output should contain "Hard-resetting any local changes to repository in 'modules/awesome/puppet-test' from branch 'origin/master'" 30 | And the file "modules/awesome/puppet-test/hello" should exist 31 | 32 | Scenario: Reset after an upstream file addition in offline mode 33 | Given a basic setup with a puppet module "puppet-test" from "awesome" 34 | And the global option "branch" sets to "modulesync" 35 | And I successfully run `msync reset` 36 | Then the file "modules/awesome/puppet-test/hello" should not exist 37 | When the puppet module "puppet-test" from "awesome" has a branch named "execute" 38 | And the puppet module "puppet-test" from "awesome" has, in branch "execute", a file named "hello" with: 39 | """ 40 | Hello 41 | """ 42 | When I successfully run `msync reset --offline` 43 | Then the file "modules/awesome/puppet-test/hello" should not exist 44 | 45 | Scenario: Reset to a specified branch 46 | Given a basic setup with a puppet module "puppet-test" from "awesome" 47 | And the global option "branch" sets to "modulesync" 48 | When the puppet module "puppet-test" from "awesome" has a branch named "other-branch" 49 | And the puppet module "puppet-test" from "awesome" has, in branch "other-branch", a file named "hello" with: 50 | """ 51 | Hello 52 | """ 53 | And I successfully run `msync reset` 54 | Then the file "modules/awesome/puppet-test/hello" should not exist 55 | When I successfully run `msync reset --verbose --source-branch origin/other-branch` 56 | And the output should contain "Hard-resetting any local changes to repository in 'modules/awesome/puppet-test' from branch 'origin/other-branch'" 57 | Then the file "modules/awesome/puppet-test/hello" should exist 58 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Gem Release 3 | 4 | on: 5 | push: 6 | tags: 7 | - '*' 8 | 9 | permissions: {} 10 | 11 | jobs: 12 | build-release: 13 | # Prevent releases from forked repositories 14 | if: github.repository_owner == 'voxpupuli' 15 | name: Build the gem 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - uses: actions/checkout@v6 19 | - name: Install Ruby 20 | uses: ruby/setup-ruby@v1 21 | with: 22 | ruby-version: 'ruby' 23 | - name: Build gem 24 | shell: bash 25 | run: gem build --verbose *.gemspec 26 | - name: Upload gem to GitHub cache 27 | uses: actions/upload-artifact@v6 28 | with: 29 | name: gem-artifact 30 | path: '*.gem' 31 | retention-days: 1 32 | compression-level: 0 33 | 34 | create-github-release: 35 | needs: build-release 36 | name: Create GitHub release 37 | runs-on: ubuntu-24.04 38 | permissions: 39 | contents: write # clone repo and create release 40 | steps: 41 | - name: Download gem from GitHub cache 42 | uses: actions/download-artifact@v7 43 | with: 44 | name: gem-artifact 45 | - name: Create Release 46 | shell: bash 47 | env: 48 | GH_TOKEN: ${{ github.token }} 49 | run: gh release create --repo ${{ github.repository }} ${{ github.ref_name }} --generate-notes *.gem 50 | 51 | release-to-github: 52 | needs: build-release 53 | name: Release to GitHub 54 | runs-on: ubuntu-24.04 55 | permissions: 56 | packages: write # publish to rubygems.pkg.github.com 57 | steps: 58 | - name: Download gem from GitHub cache 59 | uses: actions/download-artifact@v7 60 | with: 61 | name: gem-artifact 62 | - name: Publish gem to GitHub packages 63 | run: gem push --host https://rubygems.pkg.github.com/${{ github.repository_owner }} *.gem 64 | env: 65 | GEM_HOST_API_KEY: ${{ secrets.GITHUB_TOKEN }} 66 | 67 | release-to-rubygems: 68 | needs: build-release 69 | name: Release gem to rubygems.org 70 | runs-on: ubuntu-24.04 71 | environment: release # recommended by rubygems.org 72 | permissions: 73 | id-token: write # rubygems.org authentication 74 | steps: 75 | - name: Download gem from GitHub cache 76 | uses: actions/download-artifact@v7 77 | with: 78 | name: gem-artifact 79 | - uses: rubygems/configure-rubygems-credentials@v1.0.0 80 | - name: Publish gem to rubygems.org 81 | shell: bash 82 | run: gem push *.gem 83 | 84 | release-verification: 85 | name: Check that all releases are done 86 | runs-on: ubuntu-24.04 87 | permissions: 88 | contents: read # minimal permissions that we have to grant 89 | needs: 90 | - create-github-release 91 | - release-to-github 92 | - release-to-rubygems 93 | steps: 94 | - name: Download gem from GitHub cache 95 | uses: actions/download-artifact@v7 96 | with: 97 | name: gem-artifact 98 | - name: Install Ruby 99 | uses: ruby/setup-ruby@v1 100 | with: 101 | ruby-version: 'ruby' 102 | - name: Wait for release to propagate 103 | shell: bash 104 | run: | 105 | gem install rubygems-await 106 | gem await *.gem 107 | -------------------------------------------------------------------------------- /lib/modulesync/git_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ModuleSync 4 | class Error < StandardError; end 5 | 6 | # Namespace for Git service classes (ie. GitHub, GitLab) 7 | module GitService 8 | class MissingCredentialsError < Error; end 9 | 10 | class UnguessableTypeError < Error; end 11 | 12 | def self.configuration_for(sourcecode:) 13 | type = type_for(sourcecode: sourcecode) 14 | 15 | { 16 | type: type, 17 | endpoint: endpoint_for(sourcecode: sourcecode, type: type), 18 | token: token_for(sourcecode: sourcecode, type: type), 19 | } 20 | end 21 | 22 | # This method attempts to guess git service's type (ie. gitlab or github) 23 | # It process in this order 24 | # 1. use module specific configuration entry (ie. a specific entry named `gitlab` or `github`) 25 | # 2. guess using remote url (ie. looking for `github` or `gitlab` string) 26 | # 3. use environment variables (ie. check if GITHUB_TOKEN or GITLAB_TOKEN is set) 27 | # 4. fail 28 | def self.type_for(sourcecode:) 29 | return :github unless sourcecode.options[:github].nil? 30 | return :gitlab unless sourcecode.options[:gitlab].nil? 31 | return :github if sourcecode.repository_remote.include? 'github' 32 | return :gitlab if sourcecode.repository_remote.include? 'gitlab' 33 | 34 | if ENV['GITLAB_TOKEN'].nil? && ENV['GITHUB_TOKEN'].nil? 35 | raise UnguessableTypeError, <<~MESSAGE 36 | Unable to guess Git service type without GITLAB_TOKEN or GITHUB_TOKEN sets. 37 | MESSAGE 38 | end 39 | 40 | unless ENV['GITLAB_TOKEN'].nil? || ENV['GITHUB_TOKEN'].nil? 41 | raise UnguessableTypeError, <<~MESSAGE 42 | Unable to guess Git service type with both GITLAB_TOKEN and GITHUB_TOKEN sets. 43 | 44 | Please set the wanted one in configuration (ie. add `gitlab:` or `github:` key) 45 | MESSAGE 46 | end 47 | 48 | return :github unless ENV['GITHUB_TOKEN'].nil? 49 | return :gitlab unless ENV['GITLAB_TOKEN'].nil? 50 | 51 | raise NotImplementedError 52 | end 53 | 54 | # This method attempts to find git service's endpoint based on sourcecode and type 55 | # It process in this order 56 | # 1. use module specific configuration (ie. `base_url`) 57 | # 2. use environment variable dependending on type (e.g. GITLAB_BASE_URL) 58 | # 3. guess using the git remote url 59 | # 4. fail 60 | def self.endpoint_for(sourcecode:, type:) 61 | endpoint = sourcecode.options.dig(type, :base_url) 62 | 63 | endpoint ||= case type 64 | when :github 65 | ENV['GITHUB_BASE_URL'] 66 | when :gitlab 67 | ENV['GITLAB_BASE_URL'] 68 | end 69 | 70 | endpoint ||= GitService::Factory.klass(type: type).guess_endpoint_from(remote: sourcecode.repository_remote) 71 | 72 | raise NotImplementedError, <<~MESSAGE if endpoint.nil? 73 | Unable to guess endpoint for remote: '#{sourcecode.repository_remote}' 74 | Please provide `base_url` option in configuration file 75 | MESSAGE 76 | 77 | endpoint 78 | end 79 | 80 | # This method attempts to find the token associated to provided sourcecode and type 81 | # It process in this order: 82 | # 1. use module specific configuration (ie. `token`) 83 | # 2. use environment variable depending on type (e.g. GITLAB_TOKEN) 84 | # 3. fail 85 | def self.token_for(sourcecode:, type:) 86 | token = sourcecode.options.dig(type, :token) 87 | 88 | token ||= case type 89 | when :github 90 | ENV['GITHUB_TOKEN'] 91 | when :gitlab 92 | ENV['GITLAB_TOKEN'] 93 | end 94 | 95 | token 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /features/update/bump_version.feature: -------------------------------------------------------------------------------- 1 | Feature: Bump a new version after an update 2 | Scenario: Bump the module version, update changelog and tag it after an update that produces changes 3 | Given a basic setup with a puppet module "puppet-test" from "fakenamespace" 4 | And the puppet module "puppet-test" from "fakenamespace" has a file named "CHANGELOG.md" with: 5 | """ 6 | ## 1965-04-14 - Release 0.4.2 7 | """ 8 | And a file named "config_defaults.yml" with: 9 | """ 10 | --- 11 | new-file: 12 | content: aruba 13 | """ 14 | And a directory named "moduleroot" 15 | And a file named "moduleroot/new-file.erb" with: 16 | """ 17 | <%= @configs['content'] %> 18 | """ 19 | When I successfully run `msync update --verbose --message "Add new-file" --bump --changelog --tag` 20 | Then the file named "modules/fakenamespace/puppet-test/new-file" should contain "aruba" 21 | And the stdout should contain: 22 | """ 23 | Bumped to version 0.4.3 24 | """ 25 | And the stdout should contain: 26 | """ 27 | Tagging with 0.4.3 28 | """ 29 | And the file named "modules/fakenamespace/puppet-test/CHANGELOG.md" should contain "0.4.3" 30 | And the puppet module "puppet-test" from "fakenamespace" should have 2 commits made by "Aruba" 31 | And the puppet module "puppet-test" from "fakenamespace" should have a tag named "0.4.3" 32 | 33 | Scenario: Bump the module version after an update that produces changes 34 | Given a basic setup with a puppet module "puppet-test" from "fakenamespace" 35 | And a file named "config_defaults.yml" with: 36 | """ 37 | --- 38 | new-file: 39 | content: aruba 40 | """ 41 | And a directory named "moduleroot" 42 | And a file named "moduleroot/new-file.erb" with: 43 | """ 44 | <%= @configs['content'] %> 45 | """ 46 | When I successfully run `msync update --message "Add new-file" --bump` 47 | Then the file named "modules/fakenamespace/puppet-test/new-file" should contain "aruba" 48 | And the stdout should contain: 49 | """ 50 | Bumped to version 0.4.3 51 | """ 52 | And the puppet module "puppet-test" from "fakenamespace" should have 2 commits made by "Aruba" 53 | And the puppet module "puppet-test" from "fakenamespace" should not have a tag named "0.4.3" 54 | 55 | Scenario: Bump the module version with changelog update when no CHANGELOG.md is available 56 | Given a basic setup with a puppet module "puppet-test" from "fakenamespace" 57 | And a file named "config_defaults.yml" with: 58 | """ 59 | --- 60 | new-file: 61 | content: aruba 62 | """ 63 | And a directory named "moduleroot" 64 | And a file named "moduleroot/new-file.erb" with: 65 | """ 66 | <%= @configs['content'] %> 67 | """ 68 | When I successfully run `msync update --message "Add new-file" --bump --changelog` 69 | Then the file named "modules/fakenamespace/puppet-test/new-file" should contain "aruba" 70 | And the stdout should contain: 71 | """ 72 | Bumped to version 0.4.3 73 | No CHANGELOG.md file found, not updating. 74 | """ 75 | And the file named "modules/fakenamespace/puppet-test/CHANGELOG.md" should not exist 76 | And the puppet module "puppet-test" from "fakenamespace" should have 2 commits made by "Aruba" 77 | 78 | Scenario: Dont bump the module version after an update that produces no changes 79 | Given a basic setup with a puppet module "puppet-test" from "fakenamespace" 80 | And a directory named "moduleroot" 81 | When I successfully run `msync update --message "Add new-file" --bump --tag` 82 | Then the puppet module "puppet-test" from "fakenamespace" should have no commits made by "Aruba" 83 | And the puppet module "puppet-test" from "fakenamespace" should not have a tag named "0.4.3" 84 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config --no-auto-gen-timestamp` 3 | # using RuboCop version 1.81.1. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 1 10 | # Configuration parameters: AllowComments, AllowNil. 11 | Lint/SuppressedException: 12 | Exclude: 13 | - 'Rakefile' 14 | 15 | # Offense count: 1 16 | Lint/UselessConstantScoping: 17 | Exclude: 18 | - 'spec/helpers/faker/puppet_module_remote_repo.rb' 19 | 20 | # Offense count: 2 21 | # Configuration parameters: Mode, AllowedMethods, AllowedPatterns, AllowBangMethods, WaywardPredicates. 22 | # AllowedMethods: call 23 | # WaywardPredicates: nonzero? 24 | Naming/PredicateMethod: 25 | Exclude: 26 | - 'lib/modulesync/repository.rb' 27 | 28 | # Offense count: 3 29 | # Configuration parameters: Prefixes, AllowedPatterns. 30 | # Prefixes: when, with, without 31 | RSpec/ContextWording: 32 | Exclude: 33 | - 'spec/unit/module_sync/git_service/github_spec.rb' 34 | - 'spec/unit/module_sync/git_service/gitlab_spec.rb' 35 | - 'spec/unit/module_sync_spec.rb' 36 | 37 | # Offense count: 11 38 | # Configuration parameters: CountAsOne. 39 | RSpec/ExampleLength: 40 | Max: 16 41 | 42 | # Offense count: 19 43 | # Configuration parameters: AssignmentOnly. 44 | RSpec/InstanceVariable: 45 | Exclude: 46 | - 'spec/unit/module_sync/git_service/github_spec.rb' 47 | - 'spec/unit/module_sync/git_service/gitlab_spec.rb' 48 | 49 | # Offense count: 7 50 | # Configuration parameters: . 51 | # SupportedStyles: have_received, receive 52 | RSpec/MessageSpies: 53 | EnforcedStyle: receive 54 | 55 | # Offense count: 10 56 | # Configuration parameters: EnforcedStyle, IgnoreSharedExamples. 57 | # SupportedStyles: always, named_only 58 | RSpec/NamedSubject: 59 | Exclude: 60 | - 'spec/unit/module_sync/settings_spec.rb' 61 | - 'spec/unit/module_sync/source_code_spec.rb' 62 | 63 | # Offense count: 9 64 | # Configuration parameters: AllowedGroups. 65 | RSpec/NestedGroups: 66 | Max: 5 67 | 68 | # Offense count: 2 69 | # Configuration parameters: CustomTransform, IgnoreMethods, IgnoreMetadata. 70 | RSpec/SpecFilePathFormat: 71 | Exclude: 72 | - '**/spec/routing/**/*' 73 | - 'spec/unit/module_sync/git_service/github_spec.rb' 74 | - 'spec/unit/module_sync/git_service/gitlab_spec.rb' 75 | 76 | # Offense count: 6 77 | RSpec/StubbedMock: 78 | Exclude: 79 | - 'spec/unit/module_sync/git_service/github_spec.rb' 80 | - 'spec/unit/module_sync/git_service/gitlab_spec.rb' 81 | - 'spec/unit/module_sync_spec.rb' 82 | 83 | # Offense count: 9 84 | # Configuration parameters: AllowedConstants. 85 | Style/Documentation: 86 | Exclude: 87 | - 'spec/**/*' 88 | - 'test/**/*' 89 | - 'lib/modulesync.rb' 90 | - 'lib/modulesync/cli.rb' 91 | - 'lib/modulesync/hook.rb' 92 | - 'lib/modulesync/renderer.rb' 93 | - 'lib/modulesync/util.rb' 94 | - 'lib/monkey_patches.rb' 95 | 96 | # Offense count: 4 97 | # This cop supports safe autocorrection (--autocorrect). 98 | # Configuration parameters: AllowedVars, DefaultToNil. 99 | Style/FetchEnvVar: 100 | Exclude: 101 | - 'lib/modulesync/git_service.rb' 102 | 103 | # Offense count: 1 104 | # Configuration parameters: AllowedMethods. 105 | # AllowedMethods: respond_to_missing? 106 | Style/OptionalBooleanParameter: 107 | Exclude: 108 | - 'lib/modulesync/puppet_module.rb' 109 | 110 | # Offense count: 7 111 | # This cop supports safe autocorrection (--autocorrect). 112 | # Configuration parameters: AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings. 113 | # URISchemes: http, https 114 | Layout/LineLength: 115 | Max: 169 116 | -------------------------------------------------------------------------------- /features/step_definitions/git_steps.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../../spec/helpers/faker/puppet_module_remote_repo' 4 | 5 | Given 'a basic setup with a puppet module {string} from {string}' do |name, namespace| 6 | steps %( 7 | Given a mocked git configuration 8 | And a puppet module "#{name}" from "#{namespace}" 9 | And a file named "managed_modules.yml" with: 10 | """ 11 | --- 12 | - #{name} 13 | """ 14 | And a file named "modulesync.yml" with: 15 | """ 16 | --- 17 | namespace: #{namespace} 18 | """ 19 | And a git_base option appended to "modulesync.yml" for local tests 20 | ) 21 | end 22 | 23 | Given 'a mocked git configuration' do 24 | steps %( 25 | Given a mocked home directory 26 | And I run `git config --global user.name Aruba` 27 | And I run `git config --global user.email aruba@example.com` 28 | ) 29 | end 30 | 31 | Given 'a puppet module {string} from {string}' do |name, namespace| 32 | pmrr = ModuleSync::Faker::PuppetModuleRemoteRepo.new(name, namespace) 33 | pmrr.populate 34 | end 35 | 36 | Given 'a git_base option appended to "modulesync.yml" for local tests' do 37 | step "the global option 'git_base' sets to '#{ModuleSync::Faker::PuppetModuleRemoteRepo.git_base}'" 38 | end 39 | 40 | Given 'the file {string} appended with:' do |filename, content| 41 | File.write filename, "\n#{content}", mode: 'a' 42 | end 43 | 44 | Given 'the global option {string} sets to {string}' do |key, value| 45 | steps %( 46 | Given the file "#{Aruba.config.working_directory}/modulesync.yml" appended with: 47 | """ 48 | #{key}: #{value} 49 | """ 50 | ) 51 | end 52 | 53 | Given 'the puppet module {string} from {string} is read-only' do |name, namespace| 54 | pmrr = ModuleSync::Faker::PuppetModuleRemoteRepo.new(name, namespace) 55 | pmrr.read_only = true 56 | end 57 | 58 | Then 'the puppet module {string} from {string} should have no commits between {string} and {string}' do |name, namespace, commit1, commit2| 59 | pmrr = ModuleSync::Faker::PuppetModuleRemoteRepo.new(name, namespace) 60 | expect(pmrr.commit_count_between(commit1, commit2)).to eq 0 61 | end 62 | 63 | Then 'the puppet module {string} from {string} should have( only) {int} commit(s) made by {string}' do |name, namespace, commit_count, author| 64 | pmrr = ModuleSync::Faker::PuppetModuleRemoteRepo.new(name, namespace) 65 | expect(pmrr.commit_count_by(author)).to eq commit_count 66 | end 67 | 68 | Then 'the puppet module {string} from {string} should have( only) {int} commit(s) made by {string} in branch {string}' do |name, namespace, commit_count, author, branch| 69 | pmrr = ModuleSync::Faker::PuppetModuleRemoteRepo.new(name, namespace) 70 | expect(pmrr.commit_count_by(author, branch)).to eq commit_count 71 | end 72 | 73 | Then 'the puppet module {string} from {string} should have no commits made by {string}' do |name, namespace, author| 74 | step "the puppet module \"#{name}\" from \"#{namespace}\" should have 0 commits made by \"#{author}\"" 75 | end 76 | 77 | Given 'the puppet module {string} from {string} has a file named {string} with:' do |name, namespace, filename, content| 78 | pmrr = ModuleSync::Faker::PuppetModuleRemoteRepo.new(name, namespace) 79 | pmrr.add_file(filename, content) 80 | end 81 | 82 | Given 'the puppet module {string} from {string} has, in branch {string}, a file named {string} with:' do |name, namespace, branch, filename, content| 83 | pmrr = ModuleSync::Faker::PuppetModuleRemoteRepo.new(name, namespace) 84 | pmrr.add_file(filename, content, branch) 85 | end 86 | 87 | Given 'the puppet module {string} from {string} has a branch named {string}' do |name, namespace, branch| 88 | pmrr = ModuleSync::Faker::PuppetModuleRemoteRepo.new(name, namespace) 89 | pmrr.create_branch(branch) 90 | end 91 | 92 | Then 'the puppet module {string} from {string} should have a branch {string} with a file named {string} which contains:' do |name, namespace, branch, filename, content| 93 | pmrr = ModuleSync::Faker::PuppetModuleRemoteRepo.new(name, namespace) 94 | expect(pmrr.read_file(filename, branch)).to include(content) 95 | end 96 | 97 | Given 'the puppet module {string} from {string} has the default branch named {string}' do |name, namespace, default_branch| 98 | pmrr = ModuleSync::Faker::PuppetModuleRemoteRepo.new(name, namespace) 99 | pmrr.default_branch = default_branch 100 | end 101 | 102 | Then('the puppet module {string} from {string} should have a tag named {string}') do |name, namespace, tag| 103 | pmrr = ModuleSync::Faker::PuppetModuleRemoteRepo.new(name, namespace) 104 | expect(pmrr.tags).to include(tag) 105 | end 106 | 107 | Then('the puppet module {string} from {string} should not have a tag named {string}') do |name, namespace, tag| 108 | pmrr = ModuleSync::Faker::PuppetModuleRemoteRepo.new(name, namespace) 109 | expect(pmrr.tags).not_to include(tag) 110 | end 111 | 112 | Given 'the branch {string} of the puppet module {string} from {string} is deleted' do |branch, name, namespace| 113 | pmrr = ModuleSync::Faker::PuppetModuleRemoteRepo.new(name, namespace) 114 | pmrr.delete_branch(branch) 115 | end 116 | -------------------------------------------------------------------------------- /spec/helpers/faker/puppet_module_remote_repo.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'open3' 4 | 5 | require_relative '../faker' 6 | 7 | module ModuleSync 8 | # Fake a remote git repository that holds a puppet module 9 | # 10 | # This module allows to fake a remote repositiory using: 11 | # - a bare repo 12 | # - a temporary cloned repo to operate on the remote before exposing to modulesync 13 | # 14 | # Note: This module needs to have working_directory sets before using it 15 | module Faker 16 | class PuppetModuleRemoteRepo 17 | class CommandExecutionError < StandardError; end 18 | 19 | attr_reader :name, :namespace 20 | 21 | def initialize(name, namespace) 22 | @name = name 23 | @namespace = namespace 24 | end 25 | 26 | def populate 27 | FileUtils.chdir(Faker.working_directory) do 28 | run %W[git init --bare #{bare_repo_dir}] 29 | run %W[git clone #{bare_repo_dir} #{tmp_repo_dir}] 30 | 31 | module_short_name = name.split('-').last 32 | 33 | FileUtils.chdir(tmp_repo_dir) do 34 | metadata = { 35 | name: "modulesync-#{module_short_name}", 36 | version: '0.4.2', 37 | author: 'ModuleSync team', 38 | } 39 | 40 | File.write 'metadata.json', metadata.to_json 41 | run %w[git add metadata.json] 42 | run %w[git commit --message] << 'Initial commit' 43 | run %w[git push] 44 | end 45 | end 46 | end 47 | 48 | def read_only=(value) 49 | mode = value ? '0444' : '0644' 50 | FileUtils.chdir(bare_repo_dir) do 51 | run %W[git config core.sharedRepository #{mode}] 52 | end 53 | end 54 | 55 | def default_branch 56 | FileUtils.chdir(bare_repo_dir) do 57 | stdout = run %w[git symbolic-ref --short HEAD] 58 | return stdout.chomp 59 | end 60 | end 61 | 62 | def default_branch=(value) 63 | FileUtils.chdir(bare_repo_dir) do 64 | run %W[git branch -M #{default_branch} #{value}] 65 | run %W[git symbolic-ref HEAD refs/heads/#{value}] 66 | end 67 | end 68 | 69 | def read_file(filename, branch = nil) 70 | branch ||= default_branch 71 | FileUtils.chdir(bare_repo_dir) do 72 | return run %W[git show #{branch}:#{filename}] 73 | rescue CommandExecutionError 74 | return nil 75 | end 76 | end 77 | 78 | def add_file(filename, content, branch = nil) 79 | branch ||= default_branch 80 | FileUtils.chdir(tmp_repo_dir) do 81 | run %W[git checkout #{branch}] 82 | run %w[git pull --force --prune] 83 | File.write filename, content 84 | run %W[git add #{filename}] 85 | run %w[git commit --message] << "Add file: '#{filename}'" 86 | run %w[git push] 87 | end 88 | end 89 | 90 | def commit_count_between(commit1, commit2) 91 | FileUtils.chdir(bare_repo_dir) do 92 | stdout = run %W[git rev-list --count #{commit1}..#{commit2}] 93 | return Integer(stdout) 94 | end 95 | end 96 | 97 | def commit_count_by(author, commit = nil) 98 | FileUtils.chdir(bare_repo_dir) do 99 | commit ||= '--all' 100 | stdout = run %W[git rev-list --author #{author} --count #{commit} --] 101 | return Integer(stdout) 102 | end 103 | end 104 | 105 | def tags 106 | FileUtils.chdir(bare_repo_dir) do 107 | return run %w[git tag --list] 108 | end 109 | end 110 | 111 | def delete_branch(branch) 112 | FileUtils.chdir(bare_repo_dir) do 113 | run %W[git branch -D #{branch}] 114 | end 115 | end 116 | 117 | def create_branch(branch, from = nil) 118 | from ||= default_branch 119 | FileUtils.chdir(tmp_repo_dir) do 120 | run %W[git branch -c #{from} #{branch}] 121 | run %W[git push --set-upstream origin #{branch}] 122 | end 123 | end 124 | 125 | def remote_url 126 | "file://#{bare_repo_dir}" 127 | end 128 | 129 | def self.git_base 130 | "file://#{Faker.working_directory}/bare/" 131 | end 132 | 133 | private 134 | 135 | def tmp_repo_dir 136 | File.join Faker.working_directory, 'tmp', namespace, name 137 | end 138 | 139 | def bare_repo_dir 140 | File.join Faker.working_directory, 'bare', namespace, "#{name}.git" 141 | end 142 | 143 | GIT_ENV = { 144 | 'GIT_AUTHOR_NAME' => 'Faker', 145 | 'GIT_AUTHOR_EMAIL' => 'faker@example.com', 146 | 'GIT_COMMITTER_NAME' => 'Faker', 147 | 'GIT_COMMITTER_EMAIL' => 'faker@example.com', 148 | }.freeze 149 | 150 | def run(command) 151 | stdout, stderr, status = Open3.capture3(GIT_ENV, *command) 152 | return stdout if status.success? 153 | 154 | warn "Command '#{command}' failed: #{status}" 155 | warn ' STDOUT:' 156 | warn stdout 157 | warn ' STDERR:' 158 | warn stderr 159 | raise CommandExecutionError, "Command '#{command}' failed: #{status}" 160 | end 161 | end 162 | end 163 | end 164 | -------------------------------------------------------------------------------- /features/update/pull_request.feature: -------------------------------------------------------------------------------- 1 | Feature: Create a pull-request/merge-request after update 2 | 3 | Scenario: Run update in no-op mode and ask for GitHub PR 4 | Given a basic setup with a puppet module "puppet-test" from "fakenamespace" 5 | And a file named "managed_modules.yml" with: 6 | """ 7 | --- 8 | puppet-test: 9 | github: {} 10 | """ 11 | And I set the environment variables to: 12 | | variable | value | 13 | | GITHUB_TOKEN | foobar | 14 | | GITHUB_BASE_URL | https://github.example.com | 15 | And a file named "config_defaults.yml" with: 16 | """ 17 | --- 18 | test: 19 | name: aruba 20 | """ 21 | And a file named "moduleroot/test.erb" with: 22 | """ 23 | <%= @configs['name'] %> 24 | """ 25 | When I successfully run `msync update --noop --branch managed_update --pr` 26 | Then the output should contain "Would submit PR " 27 | And the puppet module "puppet-test" from "fakenamespace" should have no commits made by "Aruba" 28 | 29 | Scenario: Run update in no-op mode and ask for GitLab MR 30 | Given a basic setup with a puppet module "puppet-test" from "fakenamespace" 31 | And a file named "managed_modules.yml" with: 32 | """ 33 | --- 34 | puppet-test: 35 | gitlab: 36 | base_url: 'https://gitlab.example.com' 37 | """ 38 | And I set the environment variables to: 39 | | variable | value | 40 | | GITLAB_TOKEN | foobar | 41 | And a file named "config_defaults.yml" with: 42 | """ 43 | --- 44 | test: 45 | name: aruba 46 | """ 47 | And a file named "moduleroot/test.erb" with: 48 | """ 49 | <%= @configs['name'] %> 50 | """ 51 | When I successfully run `msync update --noop --branch managed_update --pr` 52 | Then the output should contain "Would submit MR " 53 | And the puppet module "puppet-test" from "fakenamespace" should have no commits made by "Aruba" 54 | 55 | Scenario: Run update without changes in no-op mode and ask for GitLab MR 56 | Given a basic setup with a puppet module "puppet-test" from "fakenamespace" 57 | And a directory named "moduleroot" 58 | And a file named "managed_modules.yml" with: 59 | """ 60 | --- 61 | puppet-test: 62 | gitlab: 63 | base_url: 'https://gitlab.example.com' 64 | """ 65 | And I set the environment variables to: 66 | | variable | value | 67 | | GITLAB_TOKEN | foobar | 68 | When I successfully run `msync update --noop --branch managed_update --pr` 69 | Then the output should not contain "Would submit MR " 70 | And the puppet module "puppet-test" from "fakenamespace" should have no commits made by "Aruba" 71 | 72 | Scenario: Ask for PR without credentials 73 | Given a basic setup with a puppet module "puppet-test" from "fakenamespace" 74 | And a file named "managed_modules.yml" with: 75 | """ 76 | --- 77 | puppet-test: 78 | gitlab: 79 | base_url: https://gitlab.example.com 80 | """ 81 | And a file named "config_defaults.yml" with: 82 | """ 83 | --- 84 | test: 85 | name: aruba 86 | """ 87 | And a file named "moduleroot/test.erb" with: 88 | """ 89 | <%= @configs['name'] %> 90 | """ 91 | When I run `msync update --noop --pr` 92 | Then the stderr should contain "A token is required to use services from gitlab" 93 | And the exit status should be 1 94 | And the puppet module "puppet-test" from "fakenamespace" should have no commits made by "Aruba" 95 | 96 | Scenario: Ask for PR/MR with modules from GitHub and from GitLab 97 | Given a basic setup with a puppet module "puppet-github" from "fakenamespace" 98 | And a basic setup with a puppet module "puppet-gitlab" from "fakenamespace" 99 | And a file named "managed_modules.yml" with: 100 | """ 101 | --- 102 | puppet-github: 103 | github: 104 | base_url: https://github.example.com 105 | token: 'secret' 106 | puppet-gitlab: 107 | gitlab: 108 | base_url: https://gitlab.example.com 109 | token: 'secret' 110 | """ 111 | And a file named "config_defaults.yml" with: 112 | """ 113 | --- 114 | test: 115 | name: aruba 116 | """ 117 | And a file named "moduleroot/test.erb" with: 118 | """ 119 | <%= @configs['name'] %> 120 | """ 121 | When I successfully run `msync update --noop --branch managed_update --pr` 122 | Then the output should contain "Would submit PR " 123 | And the output should contain "Would submit MR " 124 | And the puppet module "puppet-github" from "fakenamespace" should have no commits made by "Aruba" 125 | And the puppet module "puppet-gitlab" from "fakenamespace" should have no commits made by "Aruba" 126 | 127 | Scenario: Ask for PR with same source and target branch 128 | Given a basic setup with a puppet module "puppet-test" from "fakenamespace" 129 | And a file named "managed_modules.yml" with: 130 | """ 131 | --- 132 | puppet-test: 133 | gitlab: 134 | token: 'secret' 135 | base_url: 'https://gitlab.example.com' 136 | """ 137 | And a file named "config_defaults.yml" with: 138 | """ 139 | --- 140 | test: 141 | name: aruba 142 | """ 143 | And a file named "moduleroot/test.erb" with: 144 | """ 145 | <%= @configs['name'] %> 146 | """ 147 | When I run `msync update --noop --branch managed_update --pr --pr-target-branch managed_update` 148 | Then the stderr should contain "Unable to open a pull request with the same source and target branch: 'managed_update'" 149 | And the exit status should be 1 150 | And the puppet module "puppet-test" from "fakenamespace" should have no commits made by "Aruba" 151 | 152 | Scenario: Ask for PR with the default branch as source and target 153 | Given a basic setup with a puppet module "puppet-test" from "fakenamespace" 154 | And the puppet module "puppet-test" from "fakenamespace" has the default branch named "custom_default_branch" 155 | And a file named "managed_modules.yml" with: 156 | """ 157 | --- 158 | puppet-test: 159 | github: 160 | token: 'secret' 161 | base_url: 'https://gitlab.example.com' 162 | """ 163 | And a file named "config_defaults.yml" with: 164 | """ 165 | --- 166 | test: 167 | name: aruba 168 | """ 169 | And a file named "moduleroot/test.erb" with: 170 | """ 171 | <%= @configs['name'] %> 172 | """ 173 | And a directory named "moduleroot" 174 | When I run `msync update --noop --pr` 175 | Then the stderr should contain "Unable to open a pull request with the same source and target branch: 'custom_default_branch'" 176 | And the exit status should be 1 177 | And the puppet module "puppet-test" from "fakenamespace" should have no commits made by "Aruba" 178 | -------------------------------------------------------------------------------- /lib/modulesync/repository.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'git' 4 | 5 | module ModuleSync 6 | # Wrapper for Git in ModuleSync context 7 | class Repository 8 | def initialize(directory:, remote:) 9 | @directory = directory 10 | @remote = remote 11 | end 12 | 13 | def git 14 | @git ||= Git.open @directory 15 | end 16 | 17 | # This is an alias to minimize code alteration 18 | def repo 19 | git 20 | end 21 | 22 | def remote_branch_exists?(branch) 23 | repo.branches.remote.collect(&:name).include?(branch) 24 | end 25 | 26 | def local_branch_exists?(branch) 27 | repo.branches.local.collect(&:name).include?(branch) 28 | end 29 | 30 | def remote_branch_differ?(local_branch, remote_branch) 31 | !remote_branch_exists?(remote_branch) || 32 | repo.diff("#{local_branch}..origin/#{remote_branch}").any? 33 | end 34 | 35 | def default_branch 36 | # `Git.default_branch` requires ruby-git >= 1.17.0 37 | return Git.default_branch(repo.dir) if Git.respond_to? :default_branch 38 | 39 | symbolic_ref = repo.branches.find { |b| b.full.include?('remotes/origin/HEAD') } 40 | return unless symbolic_ref 41 | 42 | %r{remotes/origin/HEAD\s+->\s+origin/(?.+?)$}.match(symbolic_ref.full)[:branch] 43 | end 44 | 45 | def switch(branch:) 46 | unless branch 47 | branch = default_branch 48 | puts "Using repository's default branch: #{branch}" 49 | end 50 | return if repo.current_branch == branch 51 | 52 | if local_branch_exists?(branch) 53 | puts "Switching to branch #{branch}" 54 | repo.checkout(branch) 55 | elsif remote_branch_exists?(branch) 56 | puts "Creating local branch #{branch} from origin/#{branch}" 57 | repo.checkout("origin/#{branch}") 58 | repo.branch(branch).checkout 59 | else 60 | base_branch = default_branch 61 | unless base_branch 62 | puts "Couldn't detect default branch. Falling back to assuming 'master'" 63 | base_branch = 'master' 64 | end 65 | puts "Creating new branch #{branch} from #{base_branch}" 66 | repo.checkout("origin/#{base_branch}") 67 | repo.branch(branch).checkout 68 | end 69 | end 70 | 71 | def cloned? 72 | Dir.exist? File.join(@directory, '.git') 73 | end 74 | 75 | def clone 76 | puts "Cloning from '#{@remote}'" 77 | @git = Git.clone(@remote, @directory) 78 | end 79 | 80 | def prepare_workspace(branch:, operate_offline:) 81 | if cloned? 82 | puts "Overriding any local changes to repository in '#{@directory}'" 83 | git.fetch 'origin', prune: true unless operate_offline 84 | git.reset_hard 85 | switch(branch: branch) 86 | git.pull('origin', branch) if !operate_offline && remote_branch_exists?(branch) 87 | else 88 | raise ModuleSync::Error, 'Unable to clone in offline mode.' if operate_offline 89 | 90 | clone 91 | switch(branch: branch) 92 | end 93 | end 94 | 95 | def default_reset_branch(branch) 96 | remote_branch_exists?(branch) ? branch : default_branch 97 | end 98 | 99 | def reset_workspace(branch:, operate_offline:, source_branch: nil) 100 | raise if branch.nil? 101 | 102 | if cloned? 103 | source_branch ||= "origin/#{default_reset_branch branch}" 104 | puts "Hard-resetting any local changes to repository in '#{@directory}' from branch '#{source_branch}'" 105 | switch(branch: branch) 106 | git.fetch 'origin', prune: true unless operate_offline 107 | 108 | git.reset_hard source_branch 109 | git.clean(d: true, force: true) 110 | else 111 | raise ModuleSync::Error, 'Unable to clone in offline mode.' if operate_offline 112 | 113 | clone 114 | switch(branch: branch) 115 | end 116 | end 117 | 118 | def tag(version, tag_pattern) 119 | tag = tag_pattern % version 120 | puts "Tagging with #{tag}" 121 | repo.add_tag(tag) 122 | repo.push('origin', tag) 123 | end 124 | 125 | def checkout_branch(branch) 126 | selected_branch = branch || repo.current_branch || 'master' 127 | repo.branch(selected_branch).checkout 128 | selected_branch 129 | end 130 | 131 | # Git add/rm, git commit, git push 132 | def submit_changes(files, options) 133 | message = options[:message] 134 | branch = checkout_branch(options[:branch]) 135 | files.each do |file| 136 | if repo.status.deleted.include?(file) 137 | repo.remove(file) 138 | elsif File.exist? File.join(@directory, file) 139 | repo.add(file) 140 | end 141 | end 142 | begin 143 | opts_commit = {} 144 | opts_push = {} 145 | opts_commit = { amend: true } if options[:amend] 146 | opts_push = { force: true } if options[:force] 147 | if options[:pre_commit_script] 148 | script = "#{File.dirname(__FILE__, 3)}/contrib/#{options[:pre_commit_script]}" 149 | system(script, @directory) 150 | end 151 | if repo.status.changed.empty? && repo.status.added.empty? && repo.status.deleted.empty? 152 | puts "There were no changes in '#{@directory}'. Not committing." 153 | return false 154 | else 155 | repo.commit(message, opts_commit) 156 | end 157 | if options[:remote_branch] 158 | if remote_branch_differ?(branch, options[:remote_branch]) 159 | repo.push('origin', "#{branch}:#{options[:remote_branch]}", opts_push) 160 | puts "Changes have been pushed to: '#{branch}:#{options[:remote_branch]}'" 161 | end 162 | else 163 | repo.push('origin', branch, opts_push) 164 | puts "Changes have been pushed to: '#{branch}'" 165 | end 166 | rescue Git::Error => e 167 | raise e.message 168 | end 169 | 170 | true 171 | end 172 | 173 | def push(branch:, remote_branch:, remote_name: 'origin') 174 | raise ModuleSync::Error, 'Repository must be locally available before trying to push' unless cloned? 175 | 176 | remote_url = git.remote(remote_name).url 177 | remote_branch ||= branch 178 | puts "Push branch '#{branch}' to '#{remote_url}' (#{remote_name}/#{remote_branch})" 179 | 180 | git.push(remote_name, "#{branch}:#{remote_branch}", force: true) 181 | end 182 | 183 | # Needed because of a bug in the git gem that lists ignored files as 184 | # untracked under some circumstances 185 | # https://github.com/schacon/ruby-git/issues/130 186 | def untracked_unignored_files 187 | ignore_path = File.join @directory, '.gitignore' 188 | ignored = File.exist?(ignore_path) ? File.read(ignore_path).split : [] 189 | repo.status.untracked.keep_if { |f, _| ignored.none? { |i| File.fnmatch(i, f) } } 190 | end 191 | 192 | def show_changes(options) 193 | checkout_branch(options[:branch]) 194 | 195 | $stdout.puts 'Files changed:' 196 | repo.diff('HEAD').each do |diff| 197 | $stdout.puts diff.patch 198 | end 199 | 200 | $stdout.puts 'Files added:' 201 | untracked_unignored_files.each_key do |file| 202 | $stdout.puts file 203 | end 204 | 205 | $stdout.puts "\n\n" 206 | $stdout.puts '--------------------------------' 207 | 208 | git.diff('HEAD').any? || untracked_unignored_files.any? 209 | end 210 | 211 | def puts(*) 212 | $stdout.puts(*) if ModuleSync.options[:verbose] 213 | end 214 | end 215 | end 216 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | ## [1.3.0](https://github.com/voxpupuli/modulesync/tree/1.3.0) (2020-07-03) 2 | 3 | * Expose --managed_modules_conf [#184](https://github.com/voxpupuli/modulesync/pull/184) 4 | * Allow absolute path for config files [#183](https://github.com/voxpupuli/modulesync/pull/183) 5 | * Add pr_target_branch option [#182](https://github.com/voxpupuli/modulesync/pull/182) 6 | * Allow to specify namespace in module_options [#181](https://github.com/voxpupuli/modulesync/pull/181) 7 | * Allow to override PR parameters per module [#178](https://github.com/voxpupuli/modulesync/pull/178) 8 | * Include the gitlab library (if we interact with gitlab), not github [#179](https://github.com/voxpupuli/modulesync/pull/179) 9 | 10 | ## 2020-07-03 - 1.2.0 11 | 12 | * Add support for GitLab merge requests (MRs) [#175](https://github.com/voxpupuli/modulesync/pull/175) 13 | 14 | ## 2020-05-01 - 1.1.0 15 | 16 | This release provides metadata in the ERB template scope which makes it easy to read files from inside the module. A possible application is reading metadata.json and generating CI configs based on that. 17 | 18 | * Add metadata to ERB template scope - [#168](https://github.com/voxpupuli/modulesync/pull/168) 19 | * Skip issuing a PR if one already exists for -b option - [#171](https://github.com/voxpupuli/modulesync/pull/171) 20 | * Correct the type on the pr-labels option to prevent a deprecation warning - [#173](https://github.com/voxpupuli/modulesync/pull/173) 21 | 22 | ## 2019-09-19 - 1.0.0 23 | 24 | This is the first stable release! 🎉 25 | 26 | * Use namespace in directory structure when cloning repositories - [#152](https://github.com/voxpupuli/modulesync/pull/152) 27 | * Fix minor typo in help output - [#165](https://github.com/voxpupuli/modulesync/pull/165) 28 | * Small improvements and fixes - [#166](https://github.com/voxpupuli/modulesync/pull/166) 29 | * Fix overwriting of :global values - [#169](https://github.com/voxpupuli/modulesync/pull/169) 30 | 31 | ## 2018-12-27 - 0.10.0 32 | 33 | This is another awesome release! 34 | 35 | * Add support to submit PRs to GitHub when changes are pushed - [#147](https://github.com/voxpupuli/modulesync/pull/147) 36 | * Fix "flat files" still mentioned in README - [#151](https://github.com/voxpupuli/modulesync/pull/151) 37 | 38 | ## 2018-02-15 - 0.9.0 39 | 40 | ## Summary 41 | 42 | This is an awesome release - Now honors the repo default branch[#142](https://github.com/voxpupuli/modulesync/pull/142) 43 | 44 | ### Bugfixes 45 | 46 | * Monkey patch ls_files until ruby-git/ruby-git#320 is resolved 47 | * Reraise exception rather than exit so we can rescue a derived StandardError when using skip_broken option 48 | 49 | ### Enhancements 50 | 51 | * Add new option to produce a failure exit code on warnings 52 | * Remove hard coding of managed_modules.yml which means that options passed to ModuleSync.update can override the filename 53 | 54 | ## 2017-11-03 - 0.8.2 55 | 56 | ### Summary 57 | 58 | This release fixes: 59 | * Bug that caused .gitignore file handle to be left open - [#131](https://github.com/voxpupuli/modulesync/pull/131). 60 | * Fixed switch_branch to use current_branch instead of master - [#130](https://github.com/voxpupuli/modulesync/pull/130). 61 | * Fixed bug where failed runs wouldn't return correct exit code - [#125](https://github.com/voxpupuli/modulesync/pull/125). 62 | * Fix typo in README link to Voxpupuli modulesync_config [#123](https://github.com/voxpupuli/modulesync/pull/123). 63 | 64 | ## 2017-05-08 - 0.8.1 65 | 66 | ### Summary 67 | 68 | This release fixes a nasty bug with CLI vs configuration file option handling: Before [#117](https://github.com/voxpupuli/modulesync/pull/117) it was not possible to override options set in `modulesync.yml` on the command line, which could cause confusion in many cases. Now the configuration file is only used to populate the default values of the options specified in the README, and setting them on the command line will properly use those new values. 69 | 70 | ## 2017-05-05 - 0.8.0 71 | 72 | ### Summary 73 | 74 | This release now prefers `.erb` suffixes on template files. To convert your moduleroot directory, run this command in your configs repo: 75 | 76 | find moduleroot/ -type f -exec git mv {} {}.erb \; 77 | 78 | Note that any `.erb`-suffixed configuration keys in `config_defaults.yml`, and `.sync.yml` need to be removed by hand. (This was unreleased functionality, will not affect most users.) 79 | 80 | #### Refactoring 81 | 82 | - Prefer `.erb` suffixes on template files, issue deprecation warning for templates without the extension 83 | - Require Ruby 2.0 or higher 84 | 85 | #### Bugfixes 86 | 87 | - Fix dependency on `git` gem for diff functionality 88 | - Fix error from `git` gem when diff contained line ending changes 89 | 90 | ## 2017-02-13 - 0.7.2 91 | 92 | Fixes an issue releasing 0.7.1, no functional changes. 93 | 94 | ## 2017-02-13 - 0.7.1 95 | 96 | Fixes an issue releasing 0.7.0, no functional changes. 97 | 98 | ## 2017-02-13 - 0.7.0 99 | 100 | ### Summary 101 | 102 | This is the first release from Vox Pupuli, which has taken over maintenance of 103 | modulesync. 104 | 105 | #### Features 106 | - New `msync update` arguments: 107 | - `--git-base` to override `git_base`, e.g. for read-only git clones 108 | - `-s` to skip the current module and continue on error 109 | - `-x` for a negative filter (blacklist) of modules not to update 110 | - Add `-a` argument to `msync hook` to pass additional arguments 111 | - Add `:git_base` and `:namespace` data to `@configs` hash 112 | - Allow `managed_modules.yml` to list modules with a different namespace 113 | - Entire directories can be listed with `unmanaged: true` 114 | 115 | #### Refactoring 116 | - Replace CLI optionparser with thor 117 | 118 | #### Bugfixes 119 | - Fix git 1.8.0 compatibility, detecting when no files are changed 120 | - Fix `delete: true` feature, now deletes files correctly 121 | - Fix handling of `:global` config entries, not interpreted as a path 122 | - Fix push without force to remote branch when no files have changed (#102) 123 | - Output template name when ERB rendering fails 124 | - Remove extraneous whitespace in `--noop` output 125 | 126 | ## 2015-08-13 - 0.6.1 127 | 128 | ### Summary 129 | 130 | This is a bugfix release to fix an issue caused by the --project-root flag. 131 | 132 | #### Bugfixes 133 | 134 | - Fix bug in git pull function (#55) 135 | 136 | ##2015-08-11 - 0.6.0 137 | 138 | ### Summary 139 | 140 | This release adds two new flags to help modulesync better integrate with CI tools. 141 | 142 | #### Features 143 | 144 | - Add --project-root flag 145 | - Create --offline flag to disable git functionality 146 | 147 | #### Bugfixes 148 | 149 | - Fix :remote option for repo 150 | 151 | #### Maintenance 152 | 153 | - Added tests 154 | 155 | ## 2015-06-30 - 0.5.0 156 | 157 | ### Summary 158 | 159 | This release adds the ability to sync a non-bare local git repo. 160 | 161 | #### Features 162 | 163 | - Allow one to sync non-bare local git repository 164 | 165 | ## 2015-06-24 - 0.4.0 166 | 167 | ### Summary 168 | 169 | This release adds a --remote-branch flag and adds a global key for template 170 | config. 171 | 172 | #### Features 173 | 174 | - Expose --remote-branch 175 | - Add a global config key 176 | 177 | #### Bugfixes 178 | 179 | - Fix markdown syntax in README 180 | 181 | ## 2015-03-12 - 0.3.0 182 | 183 | ### Summary 184 | 185 | This release contains a breaking change to some parameters exposed in 186 | modulesync.yml. In particular, it abandons the user of git_user and 187 | git_provider in favor of the parameter git_base to specify the base part of a 188 | git URL to pull from. It also adds support for gerrit by adding a remote_branch 189 | parameter for modulesync.yml that can differ from the local branch, plus a 190 | number of new flags for updating modules. 191 | 192 | #### Backwards-incompatible changes 193 | 194 | - Remove git_user and git_provider_address as parameters in favor of using 195 | git_base as a whole 196 | 197 | #### Features 198 | 199 | - Expose the puppet module name in the ERB templates 200 | - Add support for gerrit by: 201 | - Adding a --amend flag 202 | - Adding a remote_branch parameter for modulesync.yml config file that can 203 | differ from the local branch 204 | - Adding a script to handle the pre-commit hook for adding a commit id 205 | - Using git_base to specify an arbitrary git URL instead of an SCP-style one 206 | - Add a --force flag (usually needed with the --amend flag if not using gerrit) 207 | - Add --bump, --tag, --tag-pattern, and --changelog flags 208 | 209 | #### Bugfixes 210 | 211 | - Stop requiring .gitignore to exist 212 | - Fix non-master branch functionality 213 | - Add workarounds for older git versions 214 | 215 | ## 2014-11-16 - 0.2.0 216 | 217 | ### Summary 218 | 219 | This release adds the --filter flag to filter what modules to sync. 220 | Also fixes the README to document the very important -m flag. 221 | 222 | ## 2014-9-29 - 0.1.0 223 | 224 | ### Summary 225 | 226 | This release adds support for other SSH-based git servers, which means 227 | gitlab is now supported. 228 | -------------------------------------------------------------------------------- /spec/unit/module_sync/git_service_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'modulesync' 4 | require 'modulesync/git_service' 5 | 6 | describe ModuleSync::GitService do 7 | before do 8 | options = ModuleSync.config_defaults.merge({ 9 | git_base: 'file:///tmp/dummy', 10 | }) 11 | ModuleSync.instance_variable_set :@options, options 12 | end 13 | 14 | context 'when guessing the git service configuration' do 15 | before do 16 | allow(ENV).to receive(:[]) 17 | .and_return(nil) 18 | end 19 | 20 | let(:sourcecode) do 21 | ModuleSync::SourceCode.new 'puppet-test', sourcecode_options 22 | end 23 | 24 | context 'when using a complete git service configuration entry' do 25 | let(:sourcecode_options) do 26 | { 27 | gitlab: { 28 | base_url: 'https://vcs.example.com/api/v4', 29 | token: 'secret', 30 | }, 31 | } 32 | end 33 | 34 | it 'build git service arguments from configuration entry' do 35 | expect(described_class.configuration_for(sourcecode: sourcecode)).to eq({ 36 | type: :gitlab, 37 | endpoint: 'https://vcs.example.com/api/v4', 38 | token: 'secret', 39 | }) 40 | end 41 | end 42 | 43 | context 'when using a simple git service key entry' do 44 | let(:sourcecode_options) do 45 | { 46 | gitlab: {}, 47 | remote: 'git@git.example.com:namespace/puppet-test', 48 | } 49 | end 50 | 51 | context 'with GITLAB_BASE_URL and GITLAB_TOKEN environment variables sets' do 52 | it 'build git service arguments from environment variables' do 53 | allow(ENV).to receive(:[]) 54 | .with('GITLAB_BASE_URL') 55 | .and_return('https://vcs.example.com/api/v4') 56 | allow(ENV).to receive(:[]) 57 | .with('GITLAB_TOKEN') 58 | .and_return('secret') 59 | 60 | expect(described_class.configuration_for(sourcecode: sourcecode)).to eq({ 61 | type: :gitlab, 62 | endpoint: 'https://vcs.example.com/api/v4', 63 | token: 'secret', 64 | }) 65 | end 66 | end 67 | 68 | context 'with only GITLAB_TOKEN environment variable sets' do 69 | it 'guesses the endpoint based on repository remote' do 70 | allow(ENV).to receive(:[]) 71 | .with('GITLAB_TOKEN') 72 | .and_return('secret') 73 | 74 | expect(described_class.configuration_for(sourcecode: sourcecode)).to eq({ 75 | type: :gitlab, 76 | endpoint: 'https://git.example.com/api/v4', 77 | token: 'secret', 78 | }) 79 | end 80 | end 81 | 82 | context 'without any environment variable sets' do 83 | it 'returns a nil token' do 84 | expect(described_class.configuration_for(sourcecode: sourcecode)).to eq({ 85 | type: :gitlab, 86 | endpoint: 'https://git.example.com/api/v4', 87 | token: nil, 88 | }) 89 | end 90 | end 91 | end 92 | 93 | context 'without git service configuration entry' do 94 | context 'with a guessable endpoint based on repository remote' do 95 | let(:sourcecode_options) do 96 | { 97 | remote: 'git@gitlab.example.com:namespace/puppet-test', 98 | } 99 | end 100 | 101 | context 'with a GITLAB_TOKEN environment variable sets' do 102 | it 'guesses git service configuration' do 103 | allow(ENV).to receive(:[]) 104 | .with('GITLAB_TOKEN') 105 | .and_return('secret') 106 | 107 | expect(described_class.configuration_for(sourcecode: sourcecode)).to eq({ 108 | type: :gitlab, 109 | endpoint: 'https://gitlab.example.com/api/v4', 110 | token: 'secret', 111 | }) 112 | end 113 | end 114 | 115 | context 'without a GITLAB_TOKEN environment variable sets' do 116 | it 'returns a nil token' do 117 | expect(described_class.configuration_for(sourcecode: sourcecode)).to eq({ 118 | type: :gitlab, 119 | endpoint: 'https://gitlab.example.com/api/v4', 120 | token: nil, 121 | }) 122 | end 123 | end 124 | end 125 | 126 | context 'with a unguessable endpoint' do 127 | let(:sourcecode_options) do 128 | { 129 | remote: 'git@vcs.example.com:namespace/puppet-test', 130 | } 131 | end 132 | 133 | context 'with GITHUB_TOKEN environments variable sets' do 134 | it 'guesses git service configuration' do 135 | allow(ENV).to receive(:[]) 136 | .with('GITHUB_TOKEN') 137 | .and_return('secret') 138 | 139 | expect(described_class.configuration_for(sourcecode: sourcecode)).to eq({ 140 | type: :github, 141 | endpoint: 'https://vcs.example.com', 142 | token: 'secret', 143 | }) 144 | end 145 | end 146 | 147 | context 'with GITLAB_TOKEN and GITHUB_TOKEN environments variables sets' do 148 | it 'raises an error' do 149 | allow(ENV).to receive(:[]) 150 | .with('GITHUB_TOKEN') 151 | .and_return('secret') 152 | 153 | allow(ENV).to receive(:[]) 154 | .with('GITLAB_TOKEN') 155 | .and_return('secret') 156 | 157 | expect { described_class.configuration_for(sourcecode: sourcecode) } 158 | .to raise_error ModuleSync::Error 159 | end 160 | end 161 | end 162 | end 163 | end 164 | 165 | RSpec.shared_examples 'hostname_extractor' do |url, hostname| 166 | context "with '#{url}' URL" do 167 | subject { ModuleSync::GitService::Base.extract_hostname(url) } 168 | 169 | it "extracts #{hostname.nil? ? 'nil' : "'#{hostname}'"} as hostname" do 170 | expect(subject).to eq(hostname) 171 | end 172 | end 173 | end 174 | 175 | describe '#extract_hostname' do 176 | [ 177 | %w[ssh://user@host.xz:4444/path/to/repo.git/ host.xz], 178 | %w[ssh://user@host.xz:/path/to/repo.git/ host.xz], 179 | %w[ssh://host.xz/path/to/repo.git/ host.xz], 180 | 181 | %w[git://host.xz/path/to/repo.git/ host.xz], 182 | %w[git://host.xz/path/to/repo/ host.xz], 183 | %w[git://host.xz/path/to/repo host.xz], 184 | 185 | %w[user@host.xz:path/to/repo.git/ host.xz], 186 | %w[user@host.xz:path/to/repo.git host.xz], 187 | %w[user@host.xz:path/to/repo host.xz], 188 | %w[host.xz:path/to/repo.git/ host.xz], 189 | 190 | %w[https://host.xz:8443/path/to/repo.git/ host.xz], 191 | %w[https://host.xz/path/to/repo.git/ host.xz], 192 | 193 | %w[ftp://host.xz/path/to/repo/ host.xz], 194 | 195 | ['/path/to/repo.git/', nil], 196 | 197 | ['file:///path/to/repo.git/', nil], 198 | 199 | ['something-invalid', nil], 200 | ].each do |url, hostname| 201 | it_behaves_like 'hostname_extractor', url, hostname 202 | end 203 | end 204 | end 205 | -------------------------------------------------------------------------------- /lib/modulesync.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'English' 4 | require 'fileutils' 5 | require 'pathname' 6 | 7 | require 'modulesync/cli' 8 | require 'modulesync/constants' 9 | require 'modulesync/hook' 10 | require 'modulesync/puppet_module' 11 | require 'modulesync/renderer' 12 | require 'modulesync/settings' 13 | require 'modulesync/util' 14 | 15 | require 'monkey_patches' 16 | 17 | module ModuleSync 18 | class Error < StandardError; end 19 | 20 | include Constants 21 | 22 | def self.config_defaults 23 | { 24 | project_root: 'modules/', 25 | managed_modules_conf: 'managed_modules.yml', 26 | configs: '.', 27 | tag_pattern: '%s', 28 | } 29 | end 30 | 31 | def self.options 32 | @options 33 | end 34 | 35 | def self.local_file(config_path, file) 36 | path = File.join(config_path, MODULE_FILES_DIR, file) 37 | if !File.exist?("#{path}.erb") && File.exist?(path) 38 | warn "Warning: using '#{path}' as template without '.erb' suffix" 39 | path 40 | else 41 | "#{path}.erb" 42 | end 43 | end 44 | 45 | # List all template files. 46 | # 47 | # Only select *.erb files, and strip the extension. This way all the code will only have to handle bare paths, 48 | # except when reading the actual ERB text 49 | def self.find_template_files(local_template_dir) 50 | if File.exist?(local_template_dir) 51 | Find.find(local_template_dir).find_all { |p| p =~ /.erb$/ && !File.directory?(p) } 52 | .collect { |p| p.chomp('.erb') } 53 | .to_a 54 | else 55 | warn "#{local_template_dir} does not exist. " \ 56 | 'Check that you are working in your module configs directory or ' \ 57 | 'that you have passed in the correct directory with -c.' 58 | exit 1 59 | end 60 | end 61 | 62 | def self.relative_names(file_list, path) 63 | file_list.map { |file| file.sub(/#{path}/, '') } 64 | end 65 | 66 | def self.managed_modules 67 | config_file = config_path(options[:managed_modules_conf], options) 68 | filter = options[:filter] 69 | negative_filter = options[:negative_filter] 70 | 71 | managed_modules = Util.parse_config(config_file) 72 | if managed_modules.empty? 73 | warn "No modules found in #{config_file}. " \ 74 | 'Check that you specified the right :configs directory and :managed_modules_conf file.' 75 | exit 1 76 | end 77 | managed_modules.select! { |m| m =~ Regexp.new(filter) } unless filter.nil? 78 | managed_modules.reject! { |m| m =~ Regexp.new(negative_filter) } unless negative_filter.nil? 79 | managed_modules.map { |given_name, options| PuppetModule.new(given_name, options) } 80 | end 81 | 82 | def self.hook(options) 83 | hook = Hook.new(HOOK_FILE, options) 84 | 85 | case options[:hook] 86 | when 'activate' 87 | hook.activate 88 | when 'deactivate' 89 | hook.deactivate 90 | end 91 | end 92 | 93 | def self.manage_file(puppet_module, filename, settings, options) 94 | configs = settings.build_file_configs(filename) 95 | target_file = puppet_module.path(filename) 96 | if configs['delete'] 97 | Renderer.remove(target_file) 98 | else 99 | template_file = local_file(options[:configs], filename) 100 | begin 101 | erb = Renderer.build(template_file) 102 | # Meta data passed to the template as @metadata[:name] 103 | metadata = { 104 | module_name: settings.additional_settings[:puppet_module], 105 | namespace: settings.additional_settings[:namespace], 106 | workdir: puppet_module.working_directory, 107 | target_file: target_file, 108 | } 109 | template = Renderer.render(erb, configs, metadata) 110 | mode = File.stat(template_file).mode 111 | Renderer.sync(template, target_file, mode) 112 | rescue StandardError 113 | warn "#{puppet_module.given_name}: Error while rendering file: '#{filename}'" 114 | raise 115 | end 116 | end 117 | end 118 | 119 | def self.manage_module(puppet_module, module_files, defaults) 120 | puts "Syncing '#{puppet_module.given_name}'" 121 | # NOTE: #prepare_workspace now supports to execute only offline operations 122 | # but we totally skip the workspace preparation to keep the current behavior 123 | unless options[:offline] 124 | puppet_module.repository.prepare_workspace(branch: options[:branch], 125 | operate_offline: false) 126 | end 127 | 128 | module_configs = Util.parse_config puppet_module.path(MODULE_CONF_FILE) 129 | settings = Settings.new(defaults[GLOBAL_DEFAULTS_KEY] || {}, 130 | defaults, 131 | module_configs[GLOBAL_DEFAULTS_KEY] || {}, 132 | module_configs, 133 | puppet_module: puppet_module.repository_name, 134 | git_base: options[:git_base], 135 | namespace: puppet_module.repository_namespace) 136 | 137 | settings.unmanaged_files(module_files).each do |filename| 138 | $stdout.puts "Not managing '#{filename}' in '#{puppet_module.given_name}'" 139 | end 140 | 141 | files_to_manage = settings.managed_files(module_files) 142 | files_to_manage.each { |filename| manage_file(puppet_module, filename, settings, options) } 143 | 144 | if options[:noop] 145 | puts "Using no-op. Files in '#{puppet_module.given_name}' may be changed but will not be committed." 146 | changed = puppet_module.repository.show_changes(options) 147 | changed && options[:pr] && puppet_module.open_pull_request 148 | elsif !options[:offline] 149 | pushed = puppet_module.repository.submit_changes(files_to_manage, options) 150 | # Only bump/tag if pushing didn't fail (i.e. there were changes) 151 | if pushed && options[:bump] 152 | new = puppet_module.bump(options[:message], options[:changelog]) 153 | puppet_module.repository.tag(new, options[:tag_pattern]) if options[:tag] 154 | end 155 | pushed && options[:pr] && puppet_module.open_pull_request 156 | end 157 | end 158 | 159 | def self.config_path(file, options) 160 | return file if Pathname.new(file).absolute? 161 | 162 | File.join(options[:configs], file) 163 | end 164 | 165 | def config_path(file, options) 166 | self.class.config_path(file, options) 167 | end 168 | 169 | def self.update(cli_options) 170 | @options = config_defaults.merge(cli_options) 171 | defaults = Util.parse_config(config_path(CONF_FILE, options)) 172 | 173 | local_template_dir = config_path(MODULE_FILES_DIR, options) 174 | local_files = find_template_files(local_template_dir) 175 | module_files = relative_names(local_files, local_template_dir) 176 | 177 | errors = false 178 | # managed_modules is either an array or a hash 179 | managed_modules.each do |puppet_module| 180 | manage_module(puppet_module, module_files, defaults) 181 | rescue ModuleSync::Error, Git::Error => e 182 | message = e.message || 'Error during `update`' 183 | warn "#{puppet_module.given_name}: #{message}" 184 | exit 1 unless options[:skip_broken] 185 | errors = true 186 | $stdout.puts "Skipping '#{puppet_module.given_name}' as update process failed" 187 | rescue StandardError 188 | raise unless options[:skip_broken] 189 | 190 | errors = true 191 | $stdout.puts "Skipping '#{puppet_module.given_name}' as update process failed" 192 | end 193 | exit 1 if errors && options[:fail_on_warnings] 194 | end 195 | 196 | def self.clone(cli_options) 197 | @options = config_defaults.merge(cli_options) 198 | 199 | managed_modules.each do |puppet_module| 200 | puppet_module.repository.clone unless puppet_module.repository.cloned? 201 | end 202 | end 203 | 204 | def self.execute(cli_options) 205 | @options = config_defaults.merge(cli_options) 206 | 207 | errors = {} 208 | managed_modules.each do |puppet_module| 209 | $stdout.puts "#{puppet_module.given_name}:" 210 | 211 | puppet_module.repository.clone unless puppet_module.repository.cloned? 212 | if @options[:default_branch] 213 | puppet_module.repository.switch branch: false 214 | else 215 | puppet_module.repository.switch branch: @options[:branch] 216 | end 217 | 218 | command_args = cli_options[:command_args] 219 | env_whitelist = (@options[:env] || '').split(',') 220 | local_script = File.expand_path command_args[0] 221 | command_args[0] = local_script if File.exist?(local_script) 222 | 223 | # Remove bundler-related env vars to allow the subprocess to run `bundle` unless explicitly whitelisted. 224 | command_env = ENV.reject { |k, _v| k.match?(/(^BUNDLE|^SOURCE_DATE_EPOCH$|^GEM_|RUBY)/) && !env_whitelist.include?(k) } 225 | 226 | result = system command_env, *command_args, unsetenv_others: true, chdir: puppet_module.working_directory 227 | unless result 228 | message = "Command execution failed ('#{@options[:command_args].join ' '}': #{$CHILD_STATUS})" 229 | raise Thor::Error, message if @options[:fail_fast] 230 | 231 | errors[puppet_module.given_name] = message 232 | warn message 233 | end 234 | 235 | $stdout.puts '' 236 | end 237 | 238 | unless errors.empty? 239 | raise Thor::Error, <<~MSG 240 | Error(s) during `execute` command: 241 | #{errors.map { |name, message| " * #{name}: #{message}" }.join "\n"} 242 | MSG 243 | end 244 | 245 | exit 1 unless errors.empty? 246 | end 247 | 248 | def self.reset(cli_options) 249 | @options = config_defaults.merge(cli_options) 250 | if @options[:branch].nil? 251 | raise Thor::Error, 252 | "Error: 'branch' option is missing, please set it in configuration or in command line." 253 | end 254 | 255 | managed_modules.each do |puppet_module| 256 | puppet_module.repository.reset_workspace( 257 | branch: @options[:branch], 258 | source_branch: @options[:source_branch], 259 | operate_offline: @options[:offline], 260 | ) 261 | end 262 | end 263 | 264 | def self.push(cli_options) 265 | @options = config_defaults.merge(cli_options) 266 | 267 | if @options[:branch].nil? 268 | raise Thor::Error, 269 | "Error: 'branch' option is missing, please set it in configuration or in command line." 270 | end 271 | 272 | managed_modules.each do |puppet_module| 273 | puppet_module.repository.push branch: @options[:branch], remote_branch: @options[:remote_branch] 274 | rescue ModuleSync::Error => e 275 | raise Thor::Error, "#{puppet_module.given_name}: #{e.message}" 276 | end 277 | end 278 | end 279 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2014 Puppet Labs Inc 2 | 3 | Apache License 4 | Version 2.0, January 2004 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, 12 | and distribution as defined by Sections 1 through 9 of this document. 13 | 14 | "Licensor" shall mean the copyright owner or entity authorized by 15 | the copyright owner that is granting the License. 16 | 17 | "Legal Entity" shall mean the union of the acting entity and all 18 | other entities that control, are controlled by, or are under common 19 | control with that entity. For the purposes of this definition, 20 | "control" means (i) the power, direct or indirect, to cause the 21 | direction or management of such entity, whether by contract or 22 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 23 | outstanding shares, or (iii) beneficial ownership of such entity. 24 | 25 | "You" (or "Your") shall mean an individual or Legal Entity 26 | exercising permissions granted by this License. 27 | 28 | "Source" form shall mean the preferred form for making modifications, 29 | including but not limited to software source code, documentation 30 | source, and configuration files. 31 | 32 | "Object" form shall mean any form resulting from mechanical 33 | transformation or translation of a Source form, including but 34 | not limited to compiled object code, generated documentation, 35 | and conversions to other media types. 36 | 37 | "Work" shall mean the work of authorship, whether in Source or 38 | Object form, made available under the License, as indicated by a 39 | copyright notice that is included in or attached to the work 40 | (an example is provided in the Appendix below). 41 | 42 | "Derivative Works" shall mean any work, whether in Source or Object 43 | form, that is based on (or derived from) the Work and for which the 44 | editorial revisions, annotations, elaborations, or other modifications 45 | represent, as a whole, an original work of authorship. For the purposes 46 | of this License, Derivative Works shall not include works that remain 47 | separable from, or merely link (or bind by name) to the interfaces of, 48 | the Work and Derivative Works thereof. 49 | 50 | "Contribution" shall mean any work of authorship, including 51 | the original version of the Work and any modifications or additions 52 | to that Work or Derivative Works thereof, that is intentionally 53 | submitted to Licensor for inclusion in the Work by the copyright owner 54 | or by an individual or Legal Entity authorized to submit on behalf of 55 | the copyright owner. For the purposes of this definition, "submitted" 56 | means any form of electronic, verbal, or written communication sent 57 | to the Licensor or its representatives, including but not limited to 58 | communication on electronic mailing lists, source code control systems, 59 | and issue tracking systems that are managed by, or on behalf of, the 60 | Licensor for the purpose of discussing and improving the Work, but 61 | excluding communication that is conspicuously marked or otherwise 62 | designated in writing by the copyright owner as "Not a Contribution." 63 | 64 | "Contributor" shall mean Licensor and any individual or Legal Entity 65 | on behalf of whom a Contribution has been received by Licensor and 66 | subsequently incorporated within the Work. 67 | 68 | 2. Grant of Copyright License. Subject to the terms and conditions of 69 | this License, each Contributor hereby grants to You a perpetual, 70 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 71 | copyright license to reproduce, prepare Derivative Works of, 72 | publicly display, publicly perform, sublicense, and distribute the 73 | Work and such Derivative Works in Source or Object form. 74 | 75 | 3. Grant of Patent License. Subject to the terms and conditions of 76 | this License, each Contributor hereby grants to You a perpetual, 77 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 78 | (except as stated in this section) patent license to make, have made, 79 | use, offer to sell, sell, import, and otherwise transfer the Work, 80 | where such license applies only to those patent claims licensable 81 | by such Contributor that are necessarily infringed by their 82 | Contribution(s) alone or by combination of their Contribution(s) 83 | with the Work to which such Contribution(s) was submitted. If You 84 | institute patent litigation against any entity (including a 85 | cross-claim or counterclaim in a lawsuit) alleging that the Work 86 | or a Contribution incorporated within the Work constitutes direct 87 | or contributory patent infringement, then any patent licenses 88 | granted to You under this License for that Work shall terminate 89 | as of the date such litigation is filed. 90 | 91 | 4. Redistribution. You may reproduce and distribute copies of the 92 | Work or Derivative Works thereof in any medium, with or without 93 | modifications, and in Source or Object form, provided that You 94 | meet the following conditions: 95 | 96 | (a) You must give any other recipients of the Work or 97 | Derivative Works a copy of this License; and 98 | 99 | (b) You must cause any modified files to carry prominent notices 100 | stating that You changed the files; and 101 | 102 | (c) You must retain, in the Source form of any Derivative Works 103 | that You distribute, all copyright, patent, trademark, and 104 | attribution notices from the Source form of the Work, 105 | excluding those notices that do not pertain to any part of 106 | the Derivative Works; and 107 | 108 | (d) If the Work includes a "NOTICE" text file as part of its 109 | distribution, then any Derivative Works that You distribute must 110 | include a readable copy of the attribution notices contained 111 | within such NOTICE file, excluding those notices that do not 112 | pertain to any part of the Derivative Works, in at least one 113 | of the following places: within a NOTICE text file distributed 114 | as part of the Derivative Works; within the Source form or 115 | documentation, if provided along with the Derivative Works; or, 116 | within a display generated by the Derivative Works, if and 117 | wherever such third-party notices normally appear. The contents 118 | of the NOTICE file are for informational purposes only and 119 | do not modify the License. You may add Your own attribution 120 | notices within Derivative Works that You distribute, alongside 121 | or as an addendum to the NOTICE text from the Work, provided 122 | that such additional attribution notices cannot be construed 123 | as modifying the License. 124 | 125 | You may add Your own copyright statement to Your modifications and 126 | may provide additional or different license terms and conditions 127 | for use, reproduction, or distribution of Your modifications, or 128 | for any such Derivative Works as a whole, provided Your use, 129 | reproduction, and distribution of the Work otherwise complies with 130 | the conditions stated in this License. 131 | 132 | 5. Submission of Contributions. Unless You explicitly state otherwise, 133 | any Contribution intentionally submitted for inclusion in the Work 134 | by You to the Licensor shall be under the terms and conditions of 135 | this License, without any additional terms or conditions. 136 | Notwithstanding the above, nothing herein shall supersede or modify 137 | the terms of any separate license agreement you may have executed 138 | with Licensor regarding such Contributions. 139 | 140 | 6. Trademarks. This License does not grant permission to use the trade 141 | names, trademarks, service marks, or product names of the Licensor, 142 | except as required for reasonable and customary use in describing the 143 | origin of the Work and reproducing the content of the NOTICE file. 144 | 145 | 7. Disclaimer of Warranty. Unless required by applicable law or 146 | agreed to in writing, Licensor provides the Work (and each 147 | Contributor provides its Contributions) on an "AS IS" BASIS, 148 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 149 | implied, including, without limitation, any warranties or conditions 150 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 151 | PARTICULAR PURPOSE. You are solely responsible for determining the 152 | appropriateness of using or redistributing the Work and assume any 153 | risks associated with Your exercise of permissions under this License. 154 | 155 | 8. Limitation of Liability. In no event and under no legal theory, 156 | whether in tort (including negligence), contract, or otherwise, 157 | unless required by applicable law (such as deliberate and grossly 158 | negligent acts) or agreed to in writing, shall any Contributor be 159 | liable to You for damages, including any direct, indirect, special, 160 | incidental, or consequential damages of any character arising as a 161 | result of this License or out of the use or inability to use the 162 | Work (including but not limited to damages for loss of goodwill, 163 | work stoppage, computer failure or malfunction, or any and all 164 | other commercial damages or losses), even if such Contributor 165 | has been advised of the possibility of such damages. 166 | 167 | 9. Accepting Warranty or Additional Liability. While redistributing 168 | the Work or Derivative Works thereof, You may choose to offer, 169 | and charge a fee for, acceptance of support, warranty, indemnity, 170 | or other liability obligations and/or rights consistent with this 171 | License. However, in accepting such obligations, You may act only 172 | on Your own behalf and on Your sole responsibility, not on behalf 173 | of any other Contributor, and only if You agree to indemnify, 174 | defend, and hold each Contributor harmless for any liability 175 | incurred by, or claims asserted against, such Contributor by reason 176 | of your accepting any such warranty or additional liability. 177 | 178 | END OF TERMS AND CONDITIONS 179 | -------------------------------------------------------------------------------- /lib/modulesync/cli.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'thor' 4 | 5 | require 'modulesync' 6 | require 'modulesync/cli/thor' 7 | require 'modulesync/constants' 8 | require 'modulesync/util' 9 | 10 | module ModuleSync 11 | module CLI 12 | def self.prepare_options(cli_options, **more_options) 13 | options = CLI.defaults 14 | options.merge! Util.symbolize_keys(cli_options) 15 | options.merge! more_options 16 | 17 | Util.symbolize_keys options 18 | end 19 | 20 | def self.defaults 21 | @defaults ||= Util.symbolize_keys(Util.parse_config(Constants::MODULESYNC_CONF_FILE)) 22 | end 23 | 24 | class Hook < Thor 25 | option :hook_args, 26 | aliases: '-a', 27 | desc: 'Arguments to pass to msync in the git hook' 28 | option :branch, 29 | aliases: '-b', 30 | desc: 'Branch name to pass to msync in the git hook', 31 | default: CLI.defaults[:branch] 32 | desc 'activate', 'Activate the git hook.' 33 | def activate 34 | ModuleSync.hook CLI.prepare_options(options, hook: 'activate') 35 | end 36 | 37 | desc 'deactivate', 'Deactivate the git hook.' 38 | def deactivate 39 | ModuleSync.hook CLI.prepare_options(options, hook: 'deactivate') 40 | end 41 | end 42 | 43 | class Base < Thor 44 | class_option :project_root, 45 | aliases: '-c', 46 | desc: 'Path used by git to clone modules into.', 47 | default: CLI.defaults[:project_root] 48 | class_option :git_base, 49 | desc: 'Specify the base part of a git URL to pull from', 50 | default: CLI.defaults[:git_base] || 'git@github.com:' 51 | class_option :namespace, 52 | aliases: '-n', 53 | desc: 'Remote github namespace (user or organization) to clone from and push to.', 54 | default: CLI.defaults[:namespace] || 'puppetlabs' 55 | class_option :filter, 56 | aliases: '-f', 57 | desc: 'A regular expression to select repositories to update.' 58 | class_option :negative_filter, 59 | aliases: '-x', 60 | desc: 'A regular expression to skip repositories.' 61 | class_option :verbose, 62 | aliases: '-v', 63 | desc: 'Verbose mode', 64 | type: :boolean, 65 | default: false 66 | 67 | desc 'update', 'Update the modules in managed_modules.yml' 68 | option :message, 69 | aliases: '-m', 70 | desc: 'Commit message to apply to updated modules. Required unless running in noop mode.', 71 | default: CLI.defaults[:message] 72 | option :configs, 73 | aliases: '-c', 74 | desc: 'The local directory or remote repository to define the list of managed modules, ' \ 75 | 'the file templates, and the default values for template variables.' 76 | option :managed_modules_conf, 77 | desc: 'The file name to define the list of managed modules' 78 | option :remote_branch, 79 | aliases: '-r', 80 | desc: 'Remote branch name to push the changes to. Defaults to the branch name.', 81 | default: CLI.defaults[:remote_branch] 82 | option :skip_broken, 83 | type: :boolean, 84 | aliases: '-s', 85 | desc: 'Process remaining modules if an error is found', 86 | default: false 87 | option :amend, 88 | type: :boolean, 89 | desc: 'Amend previous commit', 90 | default: false 91 | option :force, 92 | type: :boolean, 93 | desc: 'Force push amended commit', 94 | default: false 95 | option :noop, 96 | type: :boolean, 97 | desc: 'No-op mode', 98 | default: false 99 | option :pr, 100 | type: :boolean, 101 | desc: 'Submit pull/merge request', 102 | default: false 103 | option :pr_title, 104 | desc: 'Title of pull/merge request', 105 | default: CLI.defaults[:pr_title] || 'Update to module template files' 106 | option :pr_labels, 107 | type: :array, 108 | desc: 'Labels to add to the pull/merge request', 109 | default: CLI.defaults[:pr_labels] || [] 110 | option :pr_target_branch, 111 | desc: 'Target branch for the pull/merge request', 112 | default: CLI.defaults[:pr_target_branch] 113 | option :offline, 114 | type: :boolean, 115 | desc: 'Do not run any Git commands. Allows the user to manage Git outside of ModuleSync.', 116 | default: false 117 | option :bump, 118 | type: :boolean, 119 | desc: 'Bump module version to the next minor', 120 | default: false 121 | option :changelog, 122 | type: :boolean, 123 | desc: 'Update CHANGELOG.md if version was bumped', 124 | default: false 125 | option :tag, 126 | type: :boolean, 127 | desc: 'Git tag with the current module version', 128 | default: false 129 | option :tag_pattern, 130 | desc: 'The pattern to use when tagging releases.' 131 | option :pre_commit_script, 132 | desc: 'A script to be run before committing', 133 | default: CLI.defaults[:pre_commit_script] 134 | option :fail_on_warnings, 135 | type: :boolean, 136 | aliases: '-F', 137 | desc: 'Produce a failure exit code when there are warnings ' \ 138 | '(only has effect when --skip_broken is enabled)', 139 | default: false 140 | option :branch, 141 | aliases: '-b', 142 | desc: 'Branch name to make the changes in. ' \ 143 | 'Defaults to the default branch of the upstream repository, but falls back to "master".', 144 | default: CLI.defaults[:branch] 145 | def update 146 | config = CLI.prepare_options(options) 147 | raise Thor::Error, 'No value provided for required option "--message"' unless config[:noop] \ 148 | || config[:message] \ 149 | || config[:offline] 150 | 151 | ModuleSync.update config 152 | end 153 | 154 | desc 'execute [OPTIONS] -- COMMAND..', 'Execute the command in each managed modules' 155 | long_desc <<~DESC 156 | Execute the command in each managed modules. 157 | 158 | COMMAND can be an absolute or a relative path. 159 | 160 | To ease running local commands, a relative path is expanded with the current user directory but only if the target file exists. 161 | 162 | Example: `msync exec custom-scripts/true` will run "$PWD/custom-scripts/true" in each repository. 163 | 164 | As side effect, you can shadow system binary if a local file is present: 165 | \x5 `msync exec true` will run "$PWD/true", not `/bin/true` if "$PWD/true" exists. 166 | DESC 167 | 168 | option :configs, 169 | aliases: '-c', 170 | desc: 'The local directory or remote repository to define the list of managed modules, ' \ 171 | 'the file templates, and the default values for template variables.' 172 | option :managed_modules_conf, 173 | desc: 'The file name to define the list of managed modules' 174 | option :branch, 175 | aliases: '-b', 176 | desc: 'Branch name to make the changes in.', 177 | default: CLI.defaults[:branch] 178 | option :default_branch, 179 | aliases: '-B', 180 | type: :boolean, 181 | desc: 'Work on the default branch (take precedence over --branch).', 182 | default: false 183 | option :fail_fast, 184 | type: :boolean, 185 | desc: 'Abort the run after a command execution failure', 186 | default: CLI.defaults[:fail_fast].nil? || CLI.defaults[:fail_fast] 187 | option :env, 188 | aliases: '-e', 189 | desc: 'Comma-separated list of environment variables to preserve.' 190 | def execute(*command_args) 191 | raise Thor::Error, 'COMMAND is a required argument' if command_args.empty? 192 | 193 | ModuleSync.execute CLI.prepare_options(options, command_args: command_args) 194 | end 195 | 196 | desc 'reset', 'Reset local repositories to a well-known state' 197 | long_desc <<~DESC 198 | Reset local repository to a well-known state: 199 | \x5 * Switch local repositories to specified branch 200 | \x5 * Fetch and prune repositories unless running with `--offline` option 201 | \x5 * Hard-reset any changes to specified source branch, technically any git refs, e.g. `main`, `origin/wip` 202 | \x5 * Clean all extra local files 203 | 204 | Note: If a repository is not already cloned, it will operate the following to reach to well-known state: 205 | \x5 * Clone the repository 206 | \x5 * Switch to specified branch 207 | DESC 208 | option :configs, 209 | aliases: '-c', 210 | desc: 'The local directory or remote repository to define the list of managed modules, ' \ 211 | 'the file templates, and the default values for template variables.' 212 | option :managed_modules_conf, 213 | desc: 'The file name to define the list of managed modules' 214 | option :branch, 215 | aliases: '-b', 216 | desc: 'Branch name to make the changes in.', 217 | default: CLI.defaults[:branch] 218 | option :offline, 219 | type: :boolean, 220 | desc: 'Only proceed local operations', 221 | default: false 222 | option :source_branch, 223 | desc: 'Branch to reset from (e.g. origin/wip)' 224 | def reset 225 | ModuleSync.reset CLI.prepare_options(options) 226 | end 227 | 228 | desc 'push', 'Push all available commits from branch to remote' 229 | option :configs, 230 | aliases: '-c', 231 | desc: 'The local directory or remote repository to define the list of managed modules, ' \ 232 | 'the file templates, and the default values for template variables.' 233 | option :managed_modules_conf, 234 | desc: 'The file name to define the list of managed modules' 235 | option :branch, 236 | aliases: '-b', 237 | desc: 'Branch name to push', 238 | default: CLI.defaults[:branch] 239 | option :remote_branch, 240 | desc: 'Remote branch to push to (e.g. maintenance)' 241 | def push 242 | ModuleSync.push CLI.prepare_options(options) 243 | end 244 | 245 | desc 'clone', 'Clone repositories that need to' 246 | def clone 247 | ModuleSync.clone CLI.prepare_options(options) 248 | end 249 | 250 | desc 'hook', 'Activate or deactivate a git hook.' 251 | subcommand 'hook', ModuleSync::CLI::Hook 252 | end 253 | end 254 | end 255 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ModuleSync 2 | =========== 3 | 4 | [![License](https://img.shields.io/github/license/voxpupuli/modulesync.svg)](https://github.com/voxpupuli/modulesync/blob/master/LICENSE) 5 | [![Test](https://github.com/voxpupuli/modulesync/actions/workflows/ci.yml/badge.svg)](https://github.com/voxpupuli/modulesync/actions/workflows/ci.yml) 6 | [![codecov](https://codecov.io/gh/voxpupuli/modulesync/branch/master/graph/badge.svg?token=Mypkl78hvK)](https://codecov.io/gh/voxpupuli/modulesync) 7 | [![Release](https://github.com/voxpupuli/modulesync/actions/workflows/release.yml/badge.svg)](https://github.com/voxpupuli/modulesync/actions/workflows/release.yml) 8 | [![RubyGem Version](https://img.shields.io/gem/v/modulesync.svg)](https://rubygems.org/gems/modulesync) 9 | [![RubyGem Downloads](https://img.shields.io/gem/dt/modulesync.svg)](https://rubygems.org/gems/modulesync) 10 | [![Donated by Puppet Inc](https://img.shields.io/badge/donated%20by-Puppet%20Inc-fb7047.svg)](#transfer-notice) 11 | 12 | Table of Contents 13 | ----------------- 14 | 15 | 1. [Usage TLDR](#usage-tldr) 16 | 2. [Overview](#overview) 17 | 3. [How it works](#how-it-works) 18 | 4. [Installing](#installing) 19 | 5. [Workflow](#workflow) 20 | 6. [The Templates](#the-templates) 21 | 22 | Usage TLDR 23 | ---------- 24 | 25 | ``` 26 | gem install modulesync 27 | msync --help 28 | ``` 29 | 30 | Overview 31 | -------- 32 | 33 | ModuleSync was written as a simple script with ERB templates to help the 34 | Puppet Labs module engineers manage the zoo of Puppet modules on GitHub, and 35 | has now been restructured and generalized to be used within other 36 | organizations. Puppet modules within an organization tend to have a number of 37 | meta-files that are identical or very similar between modules, such as the 38 | `Gemfile`, `.travis.yml`, `.gitignore`, or `spec_helper.rb`. If a file needs to 39 | change in one module, it likely needs to change in the same way in every other 40 | module that the organization manages. 41 | 42 | One approach to this problem is to use sed in a bash for loop on the modules to 43 | make a single change across every module. This approach falls short if there is 44 | a single file that is purposefully different than the others, but still needs 45 | to be managed. Moreover, this approach does not help if two files are 46 | structured differently but need to be changed with the same meaning; for 47 | instance, if the .travis.yml of one module uses a list of environments to 48 | include, and another uses a matrix of includes with a list of excludes, adding 49 | a test environment to both modules will require entirely different approaches. 50 | 51 | ModuleSync provides the advantage of defining a standard template for each 52 | file to follow, so it becomes clear what a file is supposed to look like. Two 53 | files with the same semantics should also have the same syntax. A difference 54 | between two files should have clear reasons, and old cruft should not be left 55 | in files of one module as the files of another module march forward. 56 | 57 | Another advantage of ModuleSync is the ability to run in no-op mode, which 58 | makes local changes and shows the diffs, but does not make permanent changes in 59 | the remote repository. 60 | 61 | How It Works 62 | ------------ 63 | 64 | ModuleSync is a gem that uses the GitHub workflow to clone, update, and push module 65 | repositories. It expects to be activated from a directory containing 66 | configuration for modulesync and the modules, or you can pass it the location 67 | of this configuration directory. [The configuration for the Vox Pupuli 68 | modules](https://github.com/voxpupuli/modulesync_config), can be used as an 69 | example for your own configuration. The configuration directory contains a 70 | directory called moduleroot which mirrors the structure of a module. The files 71 | in the moduleroot are ERB templates, and MUST be named after the target file, 72 | with `.erb.` appended. The templates are 73 | rendered using values from a file called `config_defaults.yml` in the root (not 74 | moduleroot) of the configuration directory. The default values can be 75 | overridden or extended by adding a file called `.sync.yml` to the module itself. 76 | This allows us to, for example, have a set of "required" gems that are added 77 | to all Gemfiles, and a set of "optional" gems that a single module might add. 78 | 79 | Within the templates, values can be accessed in the `@configs` hash, which is 80 | merged from the values under the keys `:global` and the target file name (no 81 | `.erb` suffix). 82 | 83 | The list of modules to manage is in `managed_modules.yml` in the configuration 84 | directory. This lists just the names of the modules to be managed. 85 | 86 | ModuleSync can be called from the command line with parameters to change the 87 | branch you're working on or the remote to clone from and push to. You can also 88 | define these parameters in a file named `modulesync.yml` in the configuration 89 | directory. 90 | 91 | Installing 92 | ---------- 93 | 94 | ``` 95 | gem install modulesync 96 | ``` 97 | 98 | For developers: 99 | 100 | ``` 101 | gem build modulesync.gemspec 102 | gem install modulesync-*.gem 103 | ``` 104 | 105 | Workflow 106 | -------- 107 | 108 | ### Default mode 109 | 110 | With no additional arguments, ModuleSync clones modules from the puppetlabs 111 | github organization and pushes to the master branch. 112 | 113 | #### Make changes 114 | 115 | Make changes to a file in the moduleroot. For sanity's sake you should commit 116 | and push these changes, but in this mode the update will be rendered from the 117 | state of the files locally. 118 | 119 | #### Dry-run 120 | 121 | Do a dry-run to see what files will be changed, added and removed. This clones 122 | the modules to `modules/-` in the current working 123 | directory, or if the modules are already cloned, does an effective `git fetch 124 | origin; git checkout master; git reset --hard origin/master` on the modules. 125 | Don't run modulesync if the current working directory contains a modules/ 126 | directory with changes you want to keep. The dry-run makes local changes there, 127 | but does not commit or push changes. It is still destructive in that it 128 | overwrites local changes. 129 | 130 | ``` 131 | msync update --noop 132 | ``` 133 | 134 | #### Offline support 135 | 136 | The --offline flag was added to allow a user to disable git support within 137 | msync. One reason for this is because the user wants to control git commands 138 | external to msync. Note, when using this command, msync assumes you have 139 | create the folder structure and git repositories correctly. If not, msync will 140 | fail to update correctly. 141 | 142 | ``` 143 | msync update --offline 144 | ``` 145 | 146 | #### Damage mode 147 | 148 | Make changes for real and push them back to master. This operates on the 149 | pre-cloned modules from the dry-run or clones them fresh if the modules aren't 150 | found. 151 | 152 | ``` 153 | msync update -m "Commit message" 154 | ``` 155 | 156 | Amend the commit if changes are needed. 157 | 158 | ``` 159 | msync update --amend 160 | ``` 161 | 162 | For most workflows you will need to force-push an amended commit. Not required 163 | for gerrit. 164 | 165 | ``` 166 | msync update --amend --force 167 | ``` 168 | 169 | #### Automating Updates 170 | 171 | You can install a pre-push git hook to automatically clone, update, and push 172 | modules upon pushing changes to the configuration directory. This does not 173 | include a noop mode. 174 | 175 | ``` 176 | msync hook activate 177 | ``` 178 | 179 | If you have activated the hook but want to make changes to the configuration 180 | directory (such as changes to `managed_modules.yml` or `modulesync.yml`) without 181 | touching the modules, you can deactivate the hook. 182 | 183 | ``` 184 | msync hook deactivate 185 | ``` 186 | 187 | #### Submitting PRs/MRs to GitHub or GitLab 188 | 189 | You can have modulesync submit Pull Requests on GitHub or Merge Requests on 190 | GitLab automatically with the `--pr` CLI option. 191 | 192 | ``` 193 | msync update --pr 194 | ``` 195 | 196 | In order for GitHub PRs or GitLab MRs to work, you must either provide 197 | the `GITHUB_TOKEN` or `GITLAB_TOKEN` environment variables, 198 | or set them per repository in `managed_modules.yml`, using the `github` or 199 | `gitlab` keys respectively. 200 | 201 | For GitHub Enterprise and self-hosted GitLab instances you also need to set the 202 | `GITHUB_BASE_URL` or `GITLAB_BASE_URL` environment variables, or specify the 203 | `base_url` parameter in `modulesync.yml`: 204 | 205 | ```yaml 206 | --- 207 | repo1: 208 | github: 209 | token: 'EXAMPLE_TOKEN' 210 | base_url: 'https://api.github.com/' 211 | 212 | repo2: 213 | gitlab: 214 | token: 'EXAMPLE_TOKEN' 215 | base_url: 'https://git.example.com/api/v4' 216 | ``` 217 | 218 | Then: 219 | 220 | * Set the PR/MR title with `--pr-title` or in `modulesync.yml` with the 221 | `pr_title` attribute. 222 | * Assign labels to the PR/MR with `--pr-labels` or in `modulesync.yml` with 223 | the `pr_labels` attribute. **NOTE:** `pr_labels` should be a list. When 224 | using the `--pr-labels` CLI option, you should use a comma separated list. 225 | * Set the target branch with `--pr-target-branch` or in `modulesync.yml` with 226 | the `pr_target_branch` attribute. 227 | 228 | More details for GitHub: 229 | 230 | * Octokit [`api_endpoint`](https://github.com/octokit/octokit.rb#interacting-with-the-githubcom-apis-in-github-enterprise) 231 | 232 | ### Using Forks and Non-master branches 233 | 234 | The default functionality is to run ModuleSync on the puppetlabs modules, but 235 | you can use this on your own organization's modules. This functionality also 236 | applies if you want to work on a fork of the puppetlabs modules or work on a 237 | non-master branch of any organization's modules. ModuleSync does not support 238 | cloning from one remote and pushing to another, you are expected to fork 239 | manually. It does not support automating pull requests. 240 | 241 | #### Dry-run 242 | 243 | If you dry-run before doing the live update, you need to specify what namespace 244 | to clone from because the live update will not re-clone if the modules are 245 | already cloned. The namespace should be your fork, not the upstream module (if 246 | working on a fork). 247 | 248 | ``` 249 | msync update -n puppetlabs --noop 250 | ``` 251 | 252 | #### Damage mode 253 | 254 | You don't technically need to specify the namespace if the modules are already 255 | cloned from the dry-run, but it doesn't hurt. You do need to specify the 256 | namespace if the modules are not pre-cloned. You need to specify a branch to 257 | push to if you are not pushing to master. 258 | 259 | ``` 260 | msync update -n puppetlabs -b sync_branch -m "Commit message" 261 | ``` 262 | 263 | #### Configuring ModuleSync defaults 264 | 265 | If you're not using the puppetlabs modules or only ever pushing to a fork of 266 | them, then specifying the namespace and branch every time you use ModuleSync 267 | probably seems excessive. You can create a file called modulesync.yml in the 268 | configuration directory that provides these arguments automatically. This file 269 | has a form such as: 270 | 271 | ```yaml 272 | --- 273 | namespace: mygithubusername 274 | branch: modulesyncbranch 275 | ``` 276 | 277 | Then you can run ModuleSync without extra arguments: 278 | 279 | ``` 280 | msync update --noop 281 | msync update -m "Commit message" 282 | ``` 283 | 284 | Available parameters for modulesync.yml 285 | 286 | * `git_base` : The default URL to git clone from (Default: 'git@github.com:') 287 | * `namespace` : Namespace of the projects to manage (Default: 'puppetlabs'). 288 | This value can be overridden in the module name (e.g. 'namespace/mod') or by 289 | using the `namespace` key for the module in `managed_modules.yml`. 290 | * `branch` : Branch to push to (Default: 'master') 291 | * `remote_branch` : Remote branch to push to (Default: Same value as branch) 292 | * `message` : Commit message to apply to updated modules. 293 | * `pre_commit_script` : A script to be run before commiting (e.g. 'contrib/myfooscript.sh') 294 | * `pr_title` : The title to use when submitting PRs/MRs to GitHub or GitLab. 295 | * `pr_labels` : A list of labels to assign PRs/MRs created on GitHub or GitLab. 296 | 297 | ##### Example 298 | 299 | ###### GitHub 300 | 301 | ```yaml 302 | --- 303 | namespace: MySuperOrganization 304 | branch: modulesyncbranch 305 | pr_title: "Updates to module template files via modulesync" 306 | pr_labels: 307 | - TOOLING 308 | - MAINTENANCE 309 | - MODULESYNC 310 | ``` 311 | 312 | ###### GitLab 313 | 314 | ```yaml 315 | --- 316 | git_base: 'user@gitlab.example.com:' 317 | namespace: MySuperOrganization 318 | branch: modulesyncbranch 319 | ``` 320 | 321 | ###### Gerrit 322 | 323 | ```yaml 324 | --- 325 | namespace: stackforge 326 | git_base: ssh://jdoe@review.openstack.org:29418/ 327 | branch: msync_foo 328 | remote_branch: refs/publish/master/msync_foo 329 | pre_commit_script: openstack-commit-msg-hook.sh 330 | ``` 331 | 332 | #### Filtering Repositories 333 | 334 | If you only want to sync some of the repositories in your managed_modules.yml, use the `-f` flag to filter by a regex: 335 | 336 | ``` 337 | msync update -f augeas -m "Commit message" # acts only on the augeas module 338 | msync update -f puppet-a..o "Commit message" 339 | ``` 340 | 341 | If you want to skip syncing some of the repositories in your managed_modules.yml, use the `-x` flag to filter by a regex: 342 | 343 | ``` 344 | msync update -x augeas -m "Commit message" # acts on all modules except the augeas module 345 | msync update -x puppet-a..o "Commit message" 346 | ``` 347 | 348 | If no `-f` is specified, all repository are processed, if no `-x` is specified no repository is skipped. If a repository matches both `-f` and `-x` it is skipped. 349 | 350 | #### Pushing to a different remote branch 351 | 352 | If you want to push the modified branch to a different remote branch, you can use the -r flag: 353 | 354 | ``` 355 | msync update -r master_new -m "Commit message" 356 | ``` 357 | 358 | #### Automating updates 359 | 360 | If you install a git hook, you need to tell it what remote and branch to push 361 | to. This may not work properly if you already have the modules cloned from a 362 | different remote. The hook will also look in modulesync.yml for default 363 | arguments. 364 | 365 | ``` 366 | msync hook activate -n puppetlabs -b sync_branch 367 | ``` 368 | 369 | #### Updating metadata.json 370 | 371 | Modulesync can optionally bump the minor version in `metadata.json` for each 372 | modified modules if you add the `--bump` flag to the command line: 373 | 374 | ``` 375 | msync update -m "Commit message" --bump 376 | ``` 377 | 378 | #### Tagging repositories 379 | 380 | If you wish to tag the modified repositories with the newly bumped version, 381 | you can do so by using the `--tag` flag: 382 | 383 | ``` 384 | msync update -m "Commit message" --bump --tag 385 | ``` 386 | 387 | #### Setting the tag pattern 388 | 389 | You can also set the format of the tag to be used (`printf`-formatted) 390 | by setting the `tag_pattern` option: 391 | 392 | ``` 393 | msync update -m "Commit message" --bump --tag --tag_pattern 'v%s' 394 | ``` 395 | 396 | The default for the tag pattern is `%s`. 397 | 398 | #### Executing bundle command with a shared bundle path and gemfile 399 | 400 | By default `msync execute` will remove bundler related env vars (^BUNDLE|^SOURCE_DATE_EPOCH$|^GEM_|RUBY), you can use `--env` flag to keep specified env vars: 401 | 402 | ``` 403 | msync execute --env BUNDLE_PATH,BUNDLE_GEMFILE bundle exec rake lint 404 | ``` 405 | 406 | #### Updating the CHANGELOG 407 | 408 | When bumping the version in `metadata.json`, modulesync can let you 409 | updating `CHANGELOG.md` in each modified repository. 410 | 411 | This is one by using the `--changelog` flag: 412 | 413 | ``` 414 | msync update -m "Commit message" --bump --changelog 415 | ``` 416 | 417 | This flag will cause the `CHANGELOG.md` file to be updated with the 418 | current date, bumped (minor) version, and commit message. 419 | 420 | If `CHANGELOG.md` is absent in the repository, nothing will happen. 421 | 422 | 423 | #### Working with templates 424 | 425 | As mentioned, files in the moduleroot directory must be ERB templates (they must have an .erb extension, or they will be ignored). These files have direct access to @configs hash, which gets values from config_defaults.yml file and from the module being processed: 426 | 427 | ```erb 428 | <%= @configs[:git_base] %> 429 | <%= @configs[:namespace] %> 430 | <%= @configs[:puppet_module] %> 431 | ``` 432 | 433 | Alternatively some meta data is passed to the template. This will allow you to add custom Ruby extensions inside the 434 | template, reading other files from the module, to make the template system more adaptive. 435 | 436 | ```erb 437 | module: <%= @metadata[:module_name] %> 438 | target: <%= @metadata[:target_file] %> 439 | workdir: <%= @metadata[:workdir] %> 440 | ``` 441 | 442 | Will result in something like: 443 | 444 | ``` 445 | module: puppet-test 446 | target: modules/github-org/puppet-test/test 447 | workdir: modules/github-org/puppet-test 448 | ``` 449 | 450 | The Templates 451 | ------------- 452 | 453 | See [Vox Pupuli's modulesync\_config](https://github.com/voxpupuli/modulesync_config) and [Puppet's modulesync\_configs (Archived)](https://github.com/puppetlabs/modulesync_configs) repositories for different templates currently in use. 454 | 455 | ## Transfer Notice 456 | 457 | This plugin was originally authored by [Puppet Inc](http://puppet.com). 458 | The maintainer preferred that Vox Pupuli take ownership of the module for future improvement and maintenance. 459 | Existing pull requests and issues were transferred over, please fork and continue to contribute at https://github.com/voxpupuli/modulesync. 460 | 461 | Previously: https://github.com/puppetlabs/modulesync 462 | 463 | ## License 464 | 465 | This gem is licensed under the Apache-2 license. 466 | 467 | ## Release information 468 | 469 | To make a new release, please do: 470 | * update the version in the gemspec file 471 | * Install gems with `bundle install --with release --path .vendor` 472 | * generate the changelog with `bundle exec rake changelog` 473 | * Check if the new version matches the closed issues/PRs in the changelog 474 | * Create a PR with it 475 | * After it got merged, push a tag. GitHub actions will do the actual release to rubygems and GitHub Packages 476 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [4.2.0](https://github.com/voxpupuli/modulesync/tree/4.2.0) (2025-10-16) 6 | 7 | [Full Changelog](https://github.com/voxpupuli/modulesync/compare/4.1.0...4.2.0) 8 | 9 | **Implemented enhancements:** 10 | 11 | - git: Allow 4.x [\#327](https://github.com/voxpupuli/modulesync/pull/327) ([bastelfreak](https://github.com/bastelfreak)) 12 | - puppet-blacksmith: Allow 9.x [\#325](https://github.com/voxpupuli/modulesync/pull/325) ([bastelfreak](https://github.com/bastelfreak)) 13 | 14 | **Merged pull requests:** 15 | 16 | - Update gitlab requirement from \>= 4, \< 6 to \>= 4, \< 7 [\#330](https://github.com/voxpupuli/modulesync/pull/330) ([dependabot[bot]](https://github.com/apps/dependabot)) 17 | - Update octokit requirement from \>= 4, \< 10 to \>= 4, \< 11 [\#329](https://github.com/voxpupuli/modulesync/pull/329) ([dependabot[bot]](https://github.com/apps/dependabot)) 18 | - Update cucumber requirement from ~\> 9.2 to ~\> 10.1 [\#328](https://github.com/voxpupuli/modulesync/pull/328) ([dependabot[bot]](https://github.com/apps/dependabot)) 19 | - voxpupuli-rubocop: Update 4.2.0-\>5.0.0 [\#324](https://github.com/voxpupuli/modulesync/pull/324) ([bastelfreak](https://github.com/bastelfreak)) 20 | 21 | ## [4.1.0](https://github.com/voxpupuli/modulesync/tree/4.1.0) (2025-10-16) 22 | 23 | [Full Changelog](https://github.com/voxpupuli/modulesync/compare/4.0.1...4.1.0) 24 | 25 | **Implemented enhancements:** 26 | 27 | - Add execute option to preserve env vars [\#321](https://github.com/voxpupuli/modulesync/pull/321) ([Joris29](https://github.com/Joris29)) 28 | - Update git requirement from ~\> 3.0 [\#280](https://github.com/voxpupuli/modulesync/pull/280) ([dependabot[bot]](https://github.com/apps/dependabot)) 29 | 30 | ## [4.0.1](https://github.com/voxpupuli/modulesync/tree/4.0.1) (2025-09-02) 31 | 32 | [Full Changelog](https://github.com/voxpupuli/modulesync/compare/4.0.0...4.0.1) 33 | 34 | We had some issues with the 4.0.0 release. It was published to rubygems, but we've a `v4.0.0` and a `4.0.0` git tag. This confuses renovate in some setups. To fix this, we will release 4.0.1. 35 | 36 | ## [4.0.0](https://github.com/voxpupuli/modulesync/tree/4.0.0) (2025-08-28) 37 | 38 | [Full Changelog](https://github.com/voxpupuli/modulesync/compare/v4.0.0...4.0.0) 39 | 40 | ## [v4.0.0](https://github.com/voxpupuli/modulesync/tree/v4.0.0) (2025-08-28) 41 | 42 | [Full Changelog](https://github.com/voxpupuli/modulesync/compare/3.5.0...v4.0.0) 43 | 44 | **Breaking changes:** 45 | 46 | - Require Ruby 3.2+ [\#317](https://github.com/voxpupuli/modulesync/pull/317) ([bastelfreak](https://github.com/bastelfreak)) 47 | - Drop ruby older than 3.2.0 [\#312](https://github.com/voxpupuli/modulesync/pull/312) ([traylenator](https://github.com/traylenator)) 48 | 49 | **Implemented enhancements:** 50 | 51 | - Allow YAML aliases in configuration [\#311](https://github.com/voxpupuli/modulesync/pull/311) ([traylenator](https://github.com/traylenator)) 52 | 53 | ## [3.5.0](https://github.com/voxpupuli/modulesync/tree/3.5.0) (2025-07-23) 54 | 55 | [Full Changelog](https://github.com/voxpupuli/modulesync/compare/3.4.2...3.5.0) 56 | 57 | **Merged pull requests:** 58 | 59 | - thor: require 1.4 or newer [\#309](https://github.com/voxpupuli/modulesync/pull/309) ([kenyon](https://github.com/kenyon)) 60 | - README: change example config to voxpupuli [\#308](https://github.com/voxpupuli/modulesync/pull/308) ([trefzer](https://github.com/trefzer)) 61 | 62 | ## [3.4.2](https://github.com/voxpupuli/modulesync/tree/3.4.2) (2025-06-27) 63 | 64 | [Full Changelog](https://github.com/voxpupuli/modulesync/compare/3.4.1...3.4.2) 65 | 66 | **Merged pull requests:** 67 | 68 | - cleanup github release action; switch to rubygems trusted publishers [\#306](https://github.com/voxpupuli/modulesync/pull/306) ([bastelfreak](https://github.com/bastelfreak)) 69 | 70 | ## [3.4.1](https://github.com/voxpupuli/modulesync/tree/3.4.1) (2025-05-21) 71 | 72 | [Full Changelog](https://github.com/voxpupuli/modulesync/compare/3.4.0...3.4.1) 73 | 74 | **Fixed bugs:** 75 | 76 | - thor: Allow 1.2 to stay compatible with ancient facter [\#303](https://github.com/voxpupuli/modulesync/pull/303) ([bastelfreak](https://github.com/bastelfreak)) 77 | 78 | ## [3.4.0](https://github.com/voxpupuli/modulesync/tree/3.4.0) (2025-05-21) 79 | 80 | [Full Changelog](https://github.com/voxpupuli/modulesync/compare/3.3.0...3.4.0) 81 | 82 | **Merged pull requests:** 83 | 84 | - Disable coverage reports [\#300](https://github.com/voxpupuli/modulesync/pull/300) ([bastelfreak](https://github.com/bastelfreak)) 85 | - voxpupuli-rubocop: Update 3.0.0-\>3.1.0 [\#299](https://github.com/voxpupuli/modulesync/pull/299) ([bastelfreak](https://github.com/bastelfreak)) 86 | - voxpupuli-rubocop: Update 2.8.0-\>3.0.0 [\#297](https://github.com/voxpupuli/modulesync/pull/297) ([bastelfreak](https://github.com/bastelfreak)) 87 | - Allow thor 1.3.2 and newer [\#293](https://github.com/voxpupuli/modulesync/pull/293) ([dependabot[bot]](https://github.com/apps/dependabot)) 88 | 89 | ## [3.3.0](https://github.com/voxpupuli/modulesync/tree/3.3.0) (2025-02-26) 90 | 91 | [Full Changelog](https://github.com/voxpupuli/modulesync/compare/3.2.0...3.3.0) 92 | 93 | **Implemented enhancements:** 94 | 95 | - Add Ruby 3.4 to CI [\#295](https://github.com/voxpupuli/modulesync/pull/295) ([kenyon](https://github.com/kenyon)) 96 | - gemspec: allow puppet-blacksmith 8.x [\#294](https://github.com/voxpupuli/modulesync/pull/294) ([kenyon](https://github.com/kenyon)) 97 | - CI: Build gems with strict and verbose mode [\#292](https://github.com/voxpupuli/modulesync/pull/292) ([bastelfreak](https://github.com/bastelfreak)) 98 | - Add Ruby 3.3 to CI [\#291](https://github.com/voxpupuli/modulesync/pull/291) ([bastelfreak](https://github.com/bastelfreak)) 99 | - Add a flag to `msync execute` on the default branch [\#288](https://github.com/voxpupuli/modulesync/pull/288) ([smortex](https://github.com/smortex)) 100 | - Update octokit requirement from \>= 4, \< 9 to \>= 4, \< 10 [\#287](https://github.com/voxpupuli/modulesync/pull/287) ([dependabot[bot]](https://github.com/apps/dependabot)) 101 | - update to voxpupuli-rubocop 2.7.0; adjust path to unit files & rubocop: autofix & regen todo file [\#281](https://github.com/voxpupuli/modulesync/pull/281) ([bastelfreak](https://github.com/bastelfreak)) 102 | 103 | **Fixed bugs:** 104 | 105 | - pin thor to 1.3.0 [\#282](https://github.com/voxpupuli/modulesync/pull/282) ([bastelfreak](https://github.com/bastelfreak)) 106 | 107 | **Merged pull requests:** 108 | 109 | - Update voxpupuli-rubocop requirement from ~\> 2.7.0 to ~\> 2.8.0 [\#290](https://github.com/voxpupuli/modulesync/pull/290) ([dependabot[bot]](https://github.com/apps/dependabot)) 110 | - Update gitlab requirement from ~\> 4.0 to \>= 4, \< 6 [\#289](https://github.com/voxpupuli/modulesync/pull/289) ([dependabot[bot]](https://github.com/apps/dependabot)) 111 | - rubocop: Fix Style/FrozenStringLiteralComment [\#285](https://github.com/voxpupuli/modulesync/pull/285) ([bastelfreak](https://github.com/bastelfreak)) 112 | 113 | ## [3.2.0](https://github.com/voxpupuli/modulesync/tree/3.2.0) (2023-10-31) 114 | 115 | [Full Changelog](https://github.com/voxpupuli/modulesync/compare/3.1.0...3.2.0) 116 | 117 | **Implemented enhancements:** 118 | 119 | - Update octokit requirement from \>= 4, \< 8 to \>= 4, \< 9 [\#278](https://github.com/voxpupuli/modulesync/pull/278) ([dependabot[bot]](https://github.com/apps/dependabot)) 120 | 121 | **Merged pull requests:** 122 | 123 | - Clean up redundant statement [\#276](https://github.com/voxpupuli/modulesync/pull/276) ([ekohl](https://github.com/ekohl)) 124 | 125 | ## [3.1.0](https://github.com/voxpupuli/modulesync/tree/3.1.0) (2023-08-02) 126 | 127 | [Full Changelog](https://github.com/voxpupuli/modulesync/compare/3.0.0...3.1.0) 128 | 129 | Release 3.0.0 was broken. It was tagged as 3.0.0 but accidentally released as 2.7.0. The only breaking change was dropping support for EoL ruby versions. 130 | 131 | **Merged pull requests:** 132 | 133 | - rubocop: autofix [\#273](https://github.com/voxpupuli/modulesync/pull/273) ([bastelfreak](https://github.com/bastelfreak)) 134 | - Update octokit requirement from \>= 4, \< 7 to \>= 4, \< 8 [\#272](https://github.com/voxpupuli/modulesync/pull/272) ([dependabot[bot]](https://github.com/apps/dependabot)) 135 | - Update voxpupuli-rubocop requirement from ~\> 1.3 to ~\> 2.0 [\#271](https://github.com/voxpupuli/modulesync/pull/271) ([dependabot[bot]](https://github.com/apps/dependabot)) 136 | 137 | ## [3.0.0](https://github.com/voxpupuli/modulesync/tree/3.0.0) (2023-06-16) 138 | 139 | [Full Changelog](https://github.com/voxpupuli/modulesync/compare/2.6.0...3.0.0) 140 | 141 | **Breaking changes:** 142 | 143 | - Drop EoL Ruby 2.5/2.6 support [\#270](https://github.com/voxpupuli/modulesync/pull/270) ([bastelfreak](https://github.com/bastelfreak)) 144 | 145 | **Merged pull requests:** 146 | 147 | - Update puppet-blacksmith requirement from \>= 3.0, \< 7 to \>= 3.0, \< 8 [\#268](https://github.com/voxpupuli/modulesync/pull/268) ([dependabot[bot]](https://github.com/apps/dependabot)) 148 | - Update octokit requirement from ~\> 4.0 to \>= 4, \< 7 [\#263](https://github.com/voxpupuli/modulesync/pull/263) ([dependabot[bot]](https://github.com/apps/dependabot)) 149 | 150 | ## [2.6.0](https://github.com/voxpupuli/modulesync/tree/2.6.0) (2023-04-14) 151 | 152 | [Full Changelog](https://github.com/voxpupuli/modulesync/compare/2.5.0...2.6.0) 153 | 154 | **Implemented enhancements:** 155 | 156 | - Add Ruby 3.2 support [\#266](https://github.com/voxpupuli/modulesync/pull/266) ([bastelfreak](https://github.com/bastelfreak)) 157 | - Update to latest RuboCop 1.28.2 [\#265](https://github.com/voxpupuli/modulesync/pull/265) ([bastelfreak](https://github.com/bastelfreak)) 158 | 159 | **Fixed bugs:** 160 | 161 | - Fix compatibility with latest `ruby-git` [\#260](https://github.com/voxpupuli/modulesync/pull/260) ([alexjfisher](https://github.com/alexjfisher)) 162 | 163 | **Closed issues:** 164 | 165 | - msync update --noop is broken with git 1.17.x [\#259](https://github.com/voxpupuli/modulesync/issues/259) 166 | 167 | **Merged pull requests:** 168 | 169 | - Add CI best practices [\#264](https://github.com/voxpupuli/modulesync/pull/264) ([bastelfreak](https://github.com/bastelfreak)) 170 | - dependabot: check for github actions and gems [\#261](https://github.com/voxpupuli/modulesync/pull/261) ([bastelfreak](https://github.com/bastelfreak)) 171 | 172 | ## [2.5.0](https://github.com/voxpupuli/modulesync/tree/2.5.0) (2022-10-14) 173 | 174 | [Full Changelog](https://github.com/voxpupuli/modulesync/compare/2.4.0...2.5.0) 175 | 176 | **Implemented enhancements:** 177 | 178 | - Copy file permissions from template to target [\#257](https://github.com/voxpupuli/modulesync/pull/257) ([ekohl](https://github.com/ekohl)) 179 | 180 | ## [2.4.0](https://github.com/voxpupuli/modulesync/tree/2.4.0) (2022-09-27) 181 | 182 | [Full Changelog](https://github.com/voxpupuli/modulesync/compare/2.3.1...2.4.0) 183 | 184 | **Implemented enhancements:** 185 | 186 | - Expose namespace in metadata [\#254](https://github.com/voxpupuli/modulesync/pull/254) ([ekohl](https://github.com/ekohl)) 187 | 188 | **Merged pull requests:** 189 | 190 | - Fix Rubocop and add additional rubocop plugins [\#255](https://github.com/voxpupuli/modulesync/pull/255) ([ekohl](https://github.com/ekohl)) 191 | 192 | ## [2.3.1](https://github.com/voxpupuli/modulesync/tree/2.3.1) (2022-05-05) 193 | 194 | [Full Changelog](https://github.com/voxpupuli/modulesync/compare/2.3.0...2.3.1) 195 | 196 | **Fixed bugs:** 197 | 198 | - Handle Ruby 3.1 ERB trim\_mode deprecation [\#252](https://github.com/voxpupuli/modulesync/pull/252) ([ekohl](https://github.com/ekohl)) 199 | 200 | ## [2.3.0](https://github.com/voxpupuli/modulesync/tree/2.3.0) (2022-03-07) 201 | 202 | [Full Changelog](https://github.com/voxpupuli/modulesync/compare/2.2.0...2.3.0) 203 | 204 | **Implemented enhancements:** 205 | 206 | - CLI: Show relevant help when using --help option on a subcommand [\#248](https://github.com/voxpupuli/modulesync/pull/248) ([neomilium](https://github.com/neomilium)) 207 | - New CLI commands [\#244](https://github.com/voxpupuli/modulesync/pull/244) ([neomilium](https://github.com/neomilium)) 208 | 209 | **Fixed bugs:** 210 | 211 | - Existing MR makes msync fail \(which leaves changes in target branch\) [\#195](https://github.com/voxpupuli/modulesync/issues/195) 212 | - Target branch `.sync.yml` not taken into account on branch update \(--force\) [\#192](https://github.com/voxpupuli/modulesync/issues/192) 213 | - Fix error when git upstream branch is deleted [\#240](https://github.com/voxpupuli/modulesync/pull/240) ([neomilium](https://github.com/neomilium)) 214 | 215 | **Closed issues:** 216 | 217 | - Linter is missing in CI [\#237](https://github.com/voxpupuli/modulesync/issues/237) 218 | - Behavior tests are missing in CI [\#236](https://github.com/voxpupuli/modulesync/issues/236) 219 | 220 | **Merged pull requests:** 221 | 222 | - Properly ensure the parent directory exists [\#247](https://github.com/voxpupuli/modulesync/pull/247) ([ekohl](https://github.com/ekohl)) 223 | - Add Ruby 3.1 to CI matrix [\#245](https://github.com/voxpupuli/modulesync/pull/245) ([bastelfreak](https://github.com/bastelfreak)) 224 | - Fix rubocop offences and add linter to CI [\#243](https://github.com/voxpupuli/modulesync/pull/243) ([neomilium](https://github.com/neomilium)) 225 | - Support `.sync.yml` changes between two runs [\#242](https://github.com/voxpupuli/modulesync/pull/242) ([neomilium](https://github.com/neomilium)) 226 | - Fix gitlab merge request submission [\#241](https://github.com/voxpupuli/modulesync/pull/241) ([neomilium](https://github.com/neomilium)) 227 | - Add behavior tests to CI [\#239](https://github.com/voxpupuli/modulesync/pull/239) ([neomilium](https://github.com/neomilium)) 228 | - Rework PR/MR feature [\#219](https://github.com/voxpupuli/modulesync/pull/219) ([neomilium](https://github.com/neomilium)) 229 | - Refactor code for maintainabilty [\#206](https://github.com/voxpupuli/modulesync/pull/206) ([neomilium](https://github.com/neomilium)) 230 | 231 | ## [2.2.0](https://github.com/voxpupuli/modulesync/tree/2.2.0) (2021-07-24) 232 | 233 | [Full Changelog](https://github.com/voxpupuli/modulesync/compare/2.1.1...2.2.0) 234 | 235 | **Implemented enhancements:** 236 | 237 | - Implement codecov/update README.md [\#234](https://github.com/voxpupuli/modulesync/pull/234) ([bastelfreak](https://github.com/bastelfreak)) 238 | - Checkout default\_branch and not hardcoded `master` [\#233](https://github.com/voxpupuli/modulesync/pull/233) ([alexjfisher](https://github.com/alexjfisher)) 239 | 240 | **Fixed bugs:** 241 | 242 | - Fix condition for triggering the release workflow [\#232](https://github.com/voxpupuli/modulesync/pull/232) ([smortex](https://github.com/smortex)) 243 | 244 | **Merged pull requests:** 245 | 246 | - Move cucumber from Gemfile to gemspec [\#230](https://github.com/voxpupuli/modulesync/pull/230) ([bastelfreak](https://github.com/bastelfreak)) 247 | - switch to https link in gemspec [\#228](https://github.com/voxpupuli/modulesync/pull/228) ([bastelfreak](https://github.com/bastelfreak)) 248 | - dont install octokit via Gemfile [\#227](https://github.com/voxpupuli/modulesync/pull/227) ([bastelfreak](https://github.com/bastelfreak)) 249 | - Allow latest aruba dependency [\#226](https://github.com/voxpupuli/modulesync/pull/226) ([bastelfreak](https://github.com/bastelfreak)) 250 | 251 | ## [2.1.1](https://github.com/voxpupuli/modulesync/tree/2.1.1) (2021-06-15) 252 | 253 | [Full Changelog](https://github.com/voxpupuli/modulesync/compare/2.1.0...2.1.1) 254 | 255 | The 2.1.0 release didn't make it to github packages. 2.1.1 is a new release with identical code. 256 | 257 | ## [2.1.0](https://github.com/voxpupuli/modulesync/tree/2.1.0) (2021-06-15) 258 | 259 | [Full Changelog](https://github.com/voxpupuli/modulesync/compare/2.0.2...2.1.0) 260 | 261 | **Merged pull requests:** 262 | 263 | - publish to github packages + test on ruby 3 [\#222](https://github.com/voxpupuli/modulesync/pull/222) ([bastelfreak](https://github.com/bastelfreak)) 264 | - Rework exception handling [\#217](https://github.com/voxpupuli/modulesync/pull/217) ([neomilium](https://github.com/neomilium)) 265 | - Split generic and specific code [\#215](https://github.com/voxpupuli/modulesync/pull/215) ([neomilium](https://github.com/neomilium)) 266 | - Refactor repository related code [\#214](https://github.com/voxpupuli/modulesync/pull/214) ([neomilium](https://github.com/neomilium)) 267 | - Tests: Add tests for bump feature [\#213](https://github.com/voxpupuli/modulesync/pull/213) ([neomilium](https://github.com/neomilium)) 268 | - Refactor puppet modules properties [\#212](https://github.com/voxpupuli/modulesync/pull/212) ([neomilium](https://github.com/neomilium)) 269 | - Switch from Travis CI to GitHub Actions [\#205](https://github.com/voxpupuli/modulesync/pull/205) ([neomilium](https://github.com/neomilium)) 270 | 271 | ## [2.0.2](https://github.com/voxpupuli/modulesync/tree/2.0.2) (2021-04-03) 272 | 273 | [Full Changelog](https://github.com/voxpupuli/modulesync/compare/2.0.1...2.0.2) 274 | 275 | **Fixed bugs:** 276 | 277 | - Fix language-dependent Git output handling [\#200](https://github.com/voxpupuli/modulesync/pull/200) ([neomilium](https://github.com/neomilium)) 278 | 279 | **Closed issues:** 280 | 281 | - PR/MR feature should honor the repository default branch name as target branch [\#207](https://github.com/voxpupuli/modulesync/issues/207) 282 | - Add linting \(rubocop\) to Travis CI configuration [\#153](https://github.com/voxpupuli/modulesync/issues/153) 283 | - Language sensitive GIT handling [\#85](https://github.com/voxpupuli/modulesync/issues/85) 284 | 285 | **Merged pull requests:** 286 | 287 | - Fix spelling of PR CLI option \(kebab-case\) [\#209](https://github.com/voxpupuli/modulesync/pull/209) ([bittner](https://github.com/bittner)) 288 | - Correctly state which config file to update [\#208](https://github.com/voxpupuli/modulesync/pull/208) ([bittner](https://github.com/bittner)) 289 | - Fix exit status code on failures [\#204](https://github.com/voxpupuli/modulesync/pull/204) ([neomilium](https://github.com/neomilium)) 290 | - Remove monkey patches [\#203](https://github.com/voxpupuli/modulesync/pull/203) ([neomilium](https://github.com/neomilium)) 291 | - Improve tests capabilities by using local/fake remote repositories [\#202](https://github.com/voxpupuli/modulesync/pull/202) ([neomilium](https://github.com/neomilium)) 292 | - Minor modernization and cosmetic fix [\#201](https://github.com/voxpupuli/modulesync/pull/201) ([neomilium](https://github.com/neomilium)) 293 | 294 | ## [2.0.1](https://github.com/voxpupuli/modulesync/tree/2.0.1) (2020-10-06) 295 | 296 | [Full Changelog](https://github.com/voxpupuli/modulesync/compare/2.0.0...2.0.1) 297 | 298 | **Fixed bugs:** 299 | 300 | - Use remote\_branch for PRs when specified [\#194](https://github.com/voxpupuli/modulesync/pull/194) ([raphink](https://github.com/raphink)) 301 | 302 | **Merged pull requests:** 303 | 304 | - Allow newer puppet-blacksmith versions [\#197](https://github.com/voxpupuli/modulesync/pull/197) ([bastelfreak](https://github.com/bastelfreak)) 305 | 306 | ## [2.0.0](https://github.com/voxpupuli/modulesync/tree/2.0.0) (2020-08-18) 307 | 308 | [Full Changelog](https://github.com/voxpupuli/modulesync/compare/1.3.0...2.0.0) 309 | 310 | **Breaking changes:** 311 | 312 | - Drop support for Ruby 2.4 and older [\#191](https://github.com/voxpupuli/modulesync/pull/191) ([bastelfreak](https://github.com/bastelfreak)) 313 | 314 | **Implemented enhancements:** 315 | 316 | - Symbolize keys in managed\_modules except for module names [\#185](https://github.com/voxpupuli/modulesync/pull/185) ([raphink](https://github.com/raphink)) 317 | 318 | **Fixed bugs:** 319 | 320 | - GitLab MR: undefined method `\[\]' for nil:NilClass \(NoMethodError\) [\#187](https://github.com/voxpupuli/modulesync/issues/187) 321 | - msync fails with nilClass error [\#172](https://github.com/voxpupuli/modulesync/issues/172) 322 | - Fix NoMethodError for --pr option \(caused by `module_options = nil`\) / introduce --noop [\#188](https://github.com/voxpupuli/modulesync/pull/188) ([bittner](https://github.com/bittner)) 323 | - Allow empty module options in self.pr\(\) [\#186](https://github.com/voxpupuli/modulesync/pull/186) ([raphink](https://github.com/raphink)) 324 | 325 | ## [1.3.0](https://github.com/voxpupuli/modulesync/tree/1.3.0) (2020-07-03) 326 | 327 | * Expose --managed_modules_conf [#184](https://github.com/voxpupuli/modulesync/pull/184) 328 | * Allow absolute path for config files [#183](https://github.com/voxpupuli/modulesync/pull/183) 329 | * Add pr_target_branch option [#182](https://github.com/voxpupuli/modulesync/pull/182) 330 | * Allow to specify namespace in module_options [#181](https://github.com/voxpupuli/modulesync/pull/181) 331 | * Allow to override PR parameters per module [#178](https://github.com/voxpupuli/modulesync/pull/178) 332 | * Include the gitlab library (if we interact with gitlab), not github [#179](https://github.com/voxpupuli/modulesync/pull/179) 333 | 334 | ## 2020-07-03 - 1.2.0 335 | 336 | * Add support for GitLab merge requests (MRs) [#175](https://github.com/voxpupuli/modulesync/pull/175) 337 | 338 | ## 2020-05-01 - 1.1.0 339 | 340 | This release provides metadata in the ERB template scope which makes it easy to read files from inside the module. A possible application is reading metadata.json and generating CI configs based on that. 341 | 342 | * Add metadata to ERB template scope - [#168](https://github.com/voxpupuli/modulesync/pull/168) 343 | * Skip issuing a PR if one already exists for -b option - [#171](https://github.com/voxpupuli/modulesync/pull/171) 344 | * Correct the type on the pr-labels option to prevent a deprecation warning - [#173](https://github.com/voxpupuli/modulesync/pull/173) 345 | 346 | ## 2019-09-19 - 1.0.0 347 | 348 | This is the first stable release! 🎉 349 | 350 | * Use namespace in directory structure when cloning repositories - [#152](https://github.com/voxpupuli/modulesync/pull/152) 351 | * Fix minor typo in help output - [#165](https://github.com/voxpupuli/modulesync/pull/165) 352 | * Small improvements and fixes - [#166](https://github.com/voxpupuli/modulesync/pull/166) 353 | * Fix overwriting of :global values - [#169](https://github.com/voxpupuli/modulesync/pull/169) 354 | 355 | ## 2018-12-27 - 0.10.0 356 | 357 | This is another awesome release! 358 | 359 | * Add support to submit PRs to GitHub when changes are pushed - [#147](https://github.com/voxpupuli/modulesync/pull/147) 360 | * Fix "flat files" still mentioned in README - [#151](https://github.com/voxpupuli/modulesync/pull/151) 361 | 362 | ## 2018-02-15 - 0.9.0 363 | 364 | ## Summary 365 | 366 | This is an awesome release - Now honors the repo default branch[#142](https://github.com/voxpupuli/modulesync/pull/142) 367 | 368 | ### Bugfixes 369 | 370 | * Monkey patch ls_files until ruby-git/ruby-git#320 is resolved 371 | * Reraise exception rather than exit so we can rescue a derived StandardError when using skip_broken option 372 | 373 | ### Enhancements 374 | 375 | * Add new option to produce a failure exit code on warnings 376 | * Remove hard coding of managed_modules.yml which means that options passed to ModuleSync.update can override the filename 377 | 378 | ## 2017-11-03 - 0.8.2 379 | 380 | ### Summary 381 | 382 | This release fixes: 383 | * Bug that caused .gitignore file handle to be left open - [#131](https://github.com/voxpupuli/modulesync/pull/131). 384 | * Fixed switch_branch to use current_branch instead of master - [#130](https://github.com/voxpupuli/modulesync/pull/130). 385 | * Fixed bug where failed runs wouldn't return correct exit code - [#125](https://github.com/voxpupuli/modulesync/pull/125). 386 | * Fix typo in README link to Voxpupuli modulesync_config [#123](https://github.com/voxpupuli/modulesync/pull/123). 387 | 388 | ## 2017-05-08 - 0.8.1 389 | 390 | ### Summary 391 | 392 | This release fixes a nasty bug with CLI vs configuration file option handling: Before [#117](https://github.com/voxpupuli/modulesync/pull/117) it was not possible to override options set in `modulesync.yml` on the command line, which could cause confusion in many cases. Now the configuration file is only used to populate the default values of the options specified in the README, and setting them on the command line will properly use those new values. 393 | 394 | ## 2017-05-05 - 0.8.0 395 | 396 | ### Summary 397 | 398 | This release now prefers `.erb` suffixes on template files. To convert your moduleroot directory, run this command in your configs repo: 399 | 400 | find moduleroot/ -type f -exec git mv {} {}.erb \; 401 | 402 | Note that any `.erb`-suffixed configuration keys in `config_defaults.yml`, and `.sync.yml` need to be removed by hand. (This was unreleased functionality, will not affect most users.) 403 | 404 | #### Refactoring 405 | 406 | - Prefer `.erb` suffixes on template files, issue deprecation warning for templates without the extension 407 | - Require Ruby 2.0 or higher 408 | 409 | #### Bugfixes 410 | 411 | - Fix dependency on `git` gem for diff functionality 412 | - Fix error from `git` gem when diff contained line ending changes 413 | 414 | ## 2017-02-13 - 0.7.2 415 | 416 | Fixes an issue releasing 0.7.1, no functional changes. 417 | 418 | ## 2017-02-13 - 0.7.1 419 | 420 | Fixes an issue releasing 0.7.0, no functional changes. 421 | 422 | ## 2017-02-13 - 0.7.0 423 | 424 | ### Summary 425 | 426 | This is the first release from Vox Pupuli, which has taken over maintenance of 427 | modulesync. 428 | 429 | #### Features 430 | - New `msync update` arguments: 431 | - `--git-base` to override `git_base`, e.g. for read-only git clones 432 | - `-s` to skip the current module and continue on error 433 | - `-x` for a negative filter (blacklist) of modules not to update 434 | - Add `-a` argument to `msync hook` to pass additional arguments 435 | - Add `:git_base` and `:namespace` data to `@configs` hash 436 | - Allow `managed_modules.yml` to list modules with a different namespace 437 | - Entire directories can be listed with `unmanaged: true` 438 | 439 | #### Refactoring 440 | - Replace CLI optionparser with thor 441 | 442 | #### Bugfixes 443 | - Fix git 1.8.0 compatibility, detecting when no files are changed 444 | - Fix `delete: true` feature, now deletes files correctly 445 | - Fix handling of `:global` config entries, not interpreted as a path 446 | - Fix push without force to remote branch when no files have changed (#102) 447 | - Output template name when ERB rendering fails 448 | - Remove extraneous whitespace in `--noop` output 449 | 450 | ## 2015-08-13 - 0.6.1 451 | 452 | ### Summary 453 | 454 | This is a bugfix release to fix an issue caused by the --project-root flag. 455 | 456 | #### Bugfixes 457 | 458 | - Fix bug in git pull function (#55) 459 | 460 | ##2015-08-11 - 0.6.0 461 | 462 | ### Summary 463 | 464 | This release adds two new flags to help modulesync better integrate with CI tools. 465 | 466 | #### Features 467 | 468 | - Add --project-root flag 469 | - Create --offline flag to disable git functionality 470 | 471 | #### Bugfixes 472 | 473 | - Fix :remote option for repo 474 | 475 | #### Maintenance 476 | 477 | - Added tests 478 | 479 | ## 2015-06-30 - 0.5.0 480 | 481 | ### Summary 482 | 483 | This release adds the ability to sync a non-bare local git repo. 484 | 485 | #### Features 486 | 487 | - Allow one to sync non-bare local git repository 488 | 489 | ## 2015-06-24 - 0.4.0 490 | 491 | ### Summary 492 | 493 | This release adds a --remote-branch flag and adds a global key for template 494 | config. 495 | 496 | #### Features 497 | 498 | - Expose --remote-branch 499 | - Add a global config key 500 | 501 | #### Bugfixes 502 | 503 | - Fix markdown syntax in README 504 | 505 | ## 2015-03-12 - 0.3.0 506 | 507 | ### Summary 508 | 509 | This release contains a breaking change to some parameters exposed in 510 | modulesync.yml. In particular, it abandons the user of git_user and 511 | git_provider in favor of the parameter git_base to specify the base part of a 512 | git URL to pull from. It also adds support for gerrit by adding a remote_branch 513 | parameter for modulesync.yml that can differ from the local branch, plus a 514 | number of new flags for updating modules. 515 | 516 | #### Backwards-incompatible changes 517 | 518 | - Remove git_user and git_provider_address as parameters in favor of using 519 | git_base as a whole 520 | 521 | #### Features 522 | 523 | - Expose the puppet module name in the ERB templates 524 | - Add support for gerrit by: 525 | - Adding a --amend flag 526 | - Adding a remote_branch parameter for modulesync.yml config file that can 527 | differ from the local branch 528 | - Adding a script to handle the pre-commit hook for adding a commit id 529 | - Using git_base to specify an arbitrary git URL instead of an SCP-style one 530 | - Add a --force flag (usually needed with the --amend flag if not using gerrit) 531 | - Add --bump, --tag, --tag-pattern, and --changelog flags 532 | 533 | #### Bugfixes 534 | 535 | - Stop requiring .gitignore to exist 536 | - Fix non-master branch functionality 537 | - Add workarounds for older git versions 538 | 539 | ## 2014-11-16 - 0.2.0 540 | 541 | ### Summary 542 | 543 | This release adds the --filter flag to filter what modules to sync. 544 | Also fixes the README to document the very important -m flag. 545 | 546 | ## 2014-9-29 - 0.1.0 547 | 548 | ### Summary 549 | 550 | This release adds support for other SSH-based git servers, which means 551 | gitlab is now supported. 552 | 553 | 554 | \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* 555 | -------------------------------------------------------------------------------- /features/update.feature: -------------------------------------------------------------------------------- 1 | Feature: update 2 | ModuleSync needs to update module boilerplate 3 | 4 | Scenario: Adding a new file 5 | Given a basic setup with a puppet module "puppet-test" from "fakenamespace" 6 | And a file named "config_defaults.yml" with: 7 | """ 8 | --- 9 | test: 10 | name: aruba 11 | """ 12 | And a directory named "moduleroot" 13 | And a file named "moduleroot/test.erb" with: 14 | """ 15 | <%= @configs['name'] %> 16 | """ 17 | When I successfully run `msync update --noop` 18 | Then the output should match: 19 | """ 20 | Files added: 21 | test 22 | """ 23 | And the file named "modules/fakenamespace/puppet-test/test" should contain "aruba" 24 | 25 | Scenario: Using skip_broken option and adding a new file to repo without write access 26 | Given a basic setup with a puppet module "puppet-test" from "fakenamespace" 27 | And the puppet module "puppet-test" from "fakenamespace" is read-only 28 | And a file named "config_defaults.yml" with: 29 | """ 30 | --- 31 | test: 32 | name: aruba 33 | """ 34 | And a directory named "moduleroot" 35 | And a file named "moduleroot/test.erb" with: 36 | """ 37 | <%= @configs['name'] %> 38 | """ 39 | When I successfully run `msync update -s -m "Add test"` 40 | Then the puppet module "puppet-test" from "fakenamespace" should have no commits made by "Aruba" 41 | 42 | Scenario: Adding a new file to repo without write access 43 | Given a basic setup with a puppet module "puppet-test" from "fakenamespace" 44 | And the puppet module "puppet-test" from "fakenamespace" is read-only 45 | And a file named "config_defaults.yml" with: 46 | """ 47 | --- 48 | test: 49 | name: aruba 50 | """ 51 | And a directory named "moduleroot" 52 | And a file named "moduleroot/test.erb" with: 53 | """ 54 | <%= @configs['name'] %> 55 | """ 56 | When I run `msync update -m "Add test" -r` 57 | Then the exit status should be 1 58 | And the puppet module "puppet-test" from "fakenamespace" should have no commits made by "Aruba" 59 | 60 | Scenario: Adding a new file, without the .erb suffix 61 | Given a basic setup with a puppet module "puppet-test" from "fakenamespace" 62 | And a file named "config_defaults.yml" with: 63 | """ 64 | --- 65 | test: 66 | name: aruba 67 | """ 68 | And a directory named "moduleroot" 69 | And a file named "moduleroot/test" with: 70 | """ 71 | <%= @configs['name'] %> 72 | """ 73 | When I successfully run `msync update --noop` 74 | Then the output should match: 75 | """ 76 | Warning: using './moduleroot/test' as template without '.erb' suffix 77 | """ 78 | And the output should match: 79 | """ 80 | Files added: 81 | test 82 | """ 83 | And the file named "modules/fakenamespace/puppet-test/test" should contain "aruba" 84 | And the puppet module "puppet-test" from "fakenamespace" should have no commits made by "Aruba" 85 | 86 | Scenario: Adding a new file using global values 87 | Given a basic setup with a puppet module "puppet-test" from "fakenamespace" 88 | And a file named "config_defaults.yml" with: 89 | """ 90 | --- 91 | :global: 92 | name: aruba 93 | """ 94 | And a directory named "moduleroot" 95 | And a file named "moduleroot/test.erb" with: 96 | """ 97 | <%= @configs['name'] %> 98 | """ 99 | When I successfully run `msync update --noop` 100 | Then the output should match: 101 | """ 102 | Files added: 103 | test 104 | """ 105 | And the file named "modules/fakenamespace/puppet-test/test" should contain "aruba" 106 | And the puppet module "puppet-test" from "fakenamespace" should have no commits made by "Aruba" 107 | 108 | Scenario: Adding a new file overriding global values 109 | Given a basic setup with a puppet module "puppet-test" from "fakenamespace" 110 | And a file named "config_defaults.yml" with: 111 | """ 112 | --- 113 | :global: 114 | name: global 115 | 116 | test: 117 | name: aruba 118 | """ 119 | And a directory named "moduleroot" 120 | And a file named "moduleroot/test.erb" with: 121 | """ 122 | <%= @configs['name'] %> 123 | """ 124 | When I successfully run `msync update --noop` 125 | Then the output should match: 126 | """ 127 | Files added: 128 | test 129 | """ 130 | And the file named "modules/fakenamespace/puppet-test/test" should contain "aruba" 131 | And the puppet module "puppet-test" from "fakenamespace" should have no commits made by "Aruba" 132 | 133 | Scenario: Adding a new file ignoring global values 134 | Given a basic setup with a puppet module "puppet-test" from "fakenamespace" 135 | And a file named "config_defaults.yml" with: 136 | """ 137 | --- 138 | :global: 139 | key: global 140 | 141 | test: 142 | name: aruba 143 | """ 144 | And a directory named "moduleroot" 145 | And a file named "moduleroot/test.erb" with: 146 | """ 147 | <%= @configs['name'] %> 148 | """ 149 | When I successfully run `msync update --noop` 150 | Then the output should match: 151 | """ 152 | Files added: 153 | test 154 | """ 155 | And the file named "modules/fakenamespace/puppet-test/test" should contain "aruba" 156 | And the puppet module "puppet-test" from "fakenamespace" should have no commits made by "Aruba" 157 | 158 | Scenario: Adding a file that ERB can't parse 159 | Given a basic setup with a puppet module "puppet-test" from "fakenamespace" 160 | And a file named "config_defaults.yml" with: 161 | """ 162 | --- 163 | test: 164 | name: aruba 165 | """ 166 | And a directory named "moduleroot" 167 | And a file named "moduleroot/test.erb" with: 168 | """ 169 | <% @configs.each do |c| -%> 170 | <%= c['name'] %> 171 | <% end %> 172 | """ 173 | When I run `msync update --noop` 174 | Then the exit status should be 1 175 | And the puppet module "puppet-test" from "fakenamespace" should have no commits made by "Aruba" 176 | 177 | Scenario: Using skip_broken option with invalid files 178 | Given a basic setup with a puppet module "puppet-test" from "fakenamespace" 179 | And a file named "config_defaults.yml" with: 180 | """ 181 | --- 182 | test: 183 | name: aruba 184 | """ 185 | And a directory named "moduleroot" 186 | And a file named "moduleroot/test.erb" with: 187 | """ 188 | <% @configs.each do |c| -%> 189 | <%= c['name'] %> 190 | <% end %> 191 | """ 192 | When I successfully run `msync update --noop -s` 193 | Then the puppet module "puppet-test" from "fakenamespace" should have no commits made by "Aruba" 194 | 195 | Scenario: Using skip_broken and fail_on_warnings options with invalid files 196 | Given a basic setup with a puppet module "puppet-test" from "fakenamespace" 197 | And a file named "config_defaults.yml" with: 198 | """ 199 | --- 200 | test: 201 | name: aruba 202 | """ 203 | And a directory named "moduleroot" 204 | And a file named "moduleroot/test.erb" with: 205 | """ 206 | <% @configs.each do |c| -%> 207 | <%= c['name'] %> 208 | <% end %> 209 | """ 210 | When I run `msync update --noop --skip_broken --fail_on_warnings` 211 | Then the exit status should be 1 212 | And the puppet module "puppet-test" from "fakenamespace" should have no commits made by "Aruba" 213 | 214 | Scenario: Modifying an existing file 215 | Given a basic setup with a puppet module "puppet-test" from "fakenamespace" 216 | And the puppet module "puppet-test" from "fakenamespace" has a file named "Gemfile" with: 217 | """ 218 | source 'https://example.com' 219 | """ 220 | And a file named "config_defaults.yml" with: 221 | """ 222 | --- 223 | Gemfile: 224 | gem_source: https://somehost.com 225 | """ 226 | And a directory named "moduleroot" 227 | And a file named "moduleroot/Gemfile.erb" with: 228 | """ 229 | source '<%= @configs['gem_source'] %>' 230 | """ 231 | When I successfully run `msync update --noop` 232 | Then the output should match: 233 | """ 234 | Files changed: 235 | +diff --git a/Gemfile b/Gemfile 236 | """ 237 | And the file named "modules/fakenamespace/puppet-test/Gemfile" should contain: 238 | """ 239 | source 'https://somehost.com' 240 | """ 241 | And the puppet module "puppet-test" from "fakenamespace" should have no commits made by "Aruba" 242 | 243 | Scenario: Modifying an existing file and committing the change 244 | Given a basic setup with a puppet module "puppet-test" from "fakenamespace" 245 | And the puppet module "puppet-test" from "fakenamespace" has a file named "Gemfile" with: 246 | """ 247 | source 'https://example.com' 248 | """ 249 | And a file named "config_defaults.yml" with: 250 | """ 251 | --- 252 | Gemfile: 253 | gem_source: https://somehost.com 254 | """ 255 | And a directory named "moduleroot" 256 | And a file named "moduleroot/Gemfile.erb" with: 257 | """ 258 | source '<%= @configs['gem_source'] %>' 259 | """ 260 | When I successfully run `msync update -m "Update Gemfile" -r test` 261 | Then the puppet module "puppet-test" from "fakenamespace" should have only 1 commit made by "Aruba" 262 | And the puppet module "puppet-test" from "fakenamespace" should have 1 commit made by "Aruba" in branch "test" 263 | And the puppet module "puppet-test" from "fakenamespace" should have a branch "test" with a file named "Gemfile" which contains: 264 | """ 265 | source 'https://somehost.com' 266 | """ 267 | 268 | Scenario: Setting an existing file to unmanaged 269 | Given a basic setup with a puppet module "puppet-test" from "fakenamespace" 270 | And the puppet module "puppet-test" from "fakenamespace" has a file named "Gemfile" with: 271 | """ 272 | source 'https://rubygems.org' 273 | """ 274 | And a file named "config_defaults.yml" with: 275 | """ 276 | --- 277 | Gemfile: 278 | unmanaged: true 279 | gem_source: https://somehost.com 280 | """ 281 | And a directory named "moduleroot" 282 | And a file named "moduleroot/Gemfile.erb" with: 283 | """ 284 | source '<%= @configs['gem_source'] %>' 285 | """ 286 | When I successfully run `msync update --noop` 287 | Then the output should not match: 288 | """ 289 | Files changed: 290 | +diff --git a/Gemfile b/Gemfile 291 | """ 292 | And the output should match: 293 | """ 294 | Not managing 'Gemfile' in 'puppet-test' 295 | """ 296 | And the file named "modules/fakenamespace/puppet-test/Gemfile" should contain: 297 | """ 298 | source 'https://rubygems.org' 299 | """ 300 | And the puppet module "puppet-test" from "fakenamespace" should have no commits made by "Aruba" 301 | 302 | Scenario: Setting an existing file to deleted 303 | Given a basic setup with a puppet module "puppet-test" from "fakenamespace" 304 | And a file named "config_defaults.yml" with: 305 | """ 306 | --- 307 | Gemfile: 308 | delete: true 309 | """ 310 | And a directory named "moduleroot" 311 | And a file named "moduleroot/Gemfile.erb" with: 312 | """ 313 | source '<%= @configs['gem_source'] %>' 314 | """ 315 | And the puppet module "puppet-test" from "fakenamespace" has a file named "Gemfile" with: 316 | """ 317 | source 'https://rubygems.org' 318 | """ 319 | When I successfully run `msync update --noop` 320 | Then the output should match: 321 | """ 322 | Files changed: 323 | diff --git a/Gemfile b/Gemfile 324 | deleted file mode 100644 325 | """ 326 | And the puppet module "puppet-test" from "fakenamespace" should have no commits made by "Aruba" 327 | 328 | Scenario: Setting a non-existent file to deleted 329 | Given a basic setup with a puppet module "puppet-test" from "fakenamespace" 330 | And a file named "config_defaults.yml" with: 331 | """ 332 | --- 333 | doesntexist_file: 334 | delete: true 335 | """ 336 | And a directory named "moduleroot" 337 | When I successfully run `msync update -m 'deletes a file that doesnt exist!' -f puppet-test` 338 | Then the puppet module "puppet-test" from "fakenamespace" should have no commits made by "Aruba" 339 | 340 | Scenario: Setting a directory to unmanaged 341 | Given a basic setup with a puppet module "puppet-apache" from "puppetlabs" 342 | And I successfully run `msync clone` 343 | And a file named "config_defaults.yml" with: 344 | """ 345 | --- 346 | spec: 347 | unmanaged: true 348 | """ 349 | And a directory named "moduleroot/spec" 350 | And a file named "moduleroot/spec/spec_helper.rb.erb" with: 351 | """ 352 | some spec_helper fud 353 | """ 354 | And a directory named "modules/puppetlabs/puppet-apache/spec" 355 | And a file named "modules/puppetlabs/puppet-apache/spec/spec_helper.rb" with: 356 | """ 357 | This is a fake spec_helper! 358 | """ 359 | When I successfully run `msync update --offline` 360 | Then the output should contain: 361 | """ 362 | Not managing 'spec/spec_helper.rb' in 'puppet-apache' 363 | """ 364 | And the file named "modules/puppetlabs/puppet-apache/spec/spec_helper.rb" should contain: 365 | """ 366 | This is a fake spec_helper! 367 | """ 368 | And the puppet module "puppet-apache" from "puppetlabs" should have no commits made by "Aruba" 369 | 370 | Scenario: Adding a new file in a new subdirectory 371 | Given a basic setup with a puppet module "puppet-test" from "fakenamespace" 372 | And a file named "config_defaults.yml" with: 373 | """ 374 | --- 375 | spec/spec_helper.rb: 376 | require: 377 | - puppetlabs_spec_helper/module_helper 378 | """ 379 | And a file named "moduleroot/spec/spec_helper.rb.erb" with: 380 | """ 381 | <% @configs['require'].each do |required| -%> 382 | require '<%= required %>' 383 | <% end %> 384 | """ 385 | When I successfully run `msync update --noop` 386 | Then the output should match: 387 | """ 388 | Files added: 389 | spec/spec_helper.rb 390 | """ 391 | And the file named "modules/fakenamespace/puppet-test/spec/spec_helper.rb" should contain: 392 | """ 393 | require 'puppetlabs_spec_helper/module_helper' 394 | """ 395 | And the puppet module "puppet-test" from "fakenamespace" should have no commits made by "Aruba" 396 | 397 | Scenario: Updating offline 398 | Given a basic setup with a puppet module "puppet-test" from "fakenamespace" 399 | And I successfully run `msync clone` 400 | And a file named "config_defaults.yml" with: 401 | """ 402 | --- 403 | spec/spec_helper.rb: 404 | require: 405 | - puppetlabs_spec_helper/module_helper 406 | """ 407 | And a file named "moduleroot/spec/spec_helper.rb.erb" with: 408 | """ 409 | <% @configs['require'].each do |required| -%> 410 | require '<%= required %>' 411 | <% end %> 412 | """ 413 | When I successfully run `msync update --offline` 414 | Then the output should not match /Files (changed|added|deleted):/ 415 | And the puppet module "puppet-test" from "fakenamespace" should have no commits made by "Aruba" 416 | 417 | Scenario: Pulling a module that already exists in the modules directory 418 | Given a basic setup with a puppet module "puppet-test" from "fakenamespace" 419 | And a directory named "moduleroot" 420 | When I successfully run `msync update --message "First update run"` 421 | Then the puppet module "puppet-test" from "fakenamespace" should have no commits made by "Aruba" 422 | Given a file named "config_defaults.yml" with: 423 | """ 424 | --- 425 | spec/spec_helper.rb: 426 | require: 427 | - puppetlabs_spec_helper/module_helper 428 | """ 429 | And a file named "moduleroot/spec/spec_helper.rb.erb" with: 430 | """ 431 | <% @configs['require'].each do |required| -%> 432 | require '<%= required %>' 433 | <% end %> 434 | """ 435 | When I successfully run `msync update --noop` 436 | Then the output should match: 437 | """ 438 | Files added: 439 | spec/spec_helper.rb 440 | """ 441 | And the puppet module "puppet-test" from "fakenamespace" should have no commits made by "Aruba" 442 | 443 | Scenario: When running update without changes 444 | Given a basic setup with a puppet module "puppet-test" from "fakenamespace" 445 | And a directory named "moduleroot" 446 | When I successfully run `msync update --verbose --message "Running without changes"` 447 | Then the stdout should contain "There were no changes in 'modules/fakenamespace/puppet-test'. Not committing." 448 | And the puppet module "puppet-test" from "fakenamespace" should have no commits made by "Aruba" 449 | 450 | Scenario: When specifying configurations in managed_modules.yml 451 | Given a basic setup with a puppet module "puppet-test" from "fakenamespace" 452 | And a file named "managed_modules.yml" with: 453 | """ 454 | --- 455 | puppet-test: 456 | module_name: test 457 | """ 458 | And a file named "config_defaults.yml" with: 459 | """ 460 | --- 461 | test: 462 | name: aruba 463 | """ 464 | And a directory named "moduleroot" 465 | And a file named "moduleroot/test.erb" with: 466 | """ 467 | <%= @configs['name'] %> 468 | """ 469 | When I successfully run `msync update --noop` 470 | Then the output should match: 471 | """ 472 | Files added: 473 | test 474 | """ 475 | And the file named "modules/fakenamespace/puppet-test/test" should contain "aruba" 476 | And the puppet module "puppet-test" from "fakenamespace" should have no commits made by "Aruba" 477 | 478 | Scenario: When specifying configurations in managed_modules.yml and using a filter 479 | Given a mocked git configuration 480 | And a puppet module "puppet-test" from "fakenamespace" 481 | And a puppet module "puppet-blacksmith" from "fakenamespace" 482 | And a file named "managed_modules.yml" with: 483 | """ 484 | --- 485 | puppet-blacksmith: 486 | puppet-test: 487 | module_name: test 488 | """ 489 | And a file named "modulesync.yml" with: 490 | """ 491 | --- 492 | namespace: fakenamespace 493 | """ 494 | And a git_base option appended to "modulesync.yml" for local tests 495 | And a file named "config_defaults.yml" with: 496 | """ 497 | --- 498 | test: 499 | name: aruba 500 | """ 501 | And a directory named "moduleroot" 502 | And a file named "moduleroot/test.erb" with: 503 | """ 504 | <%= @configs['name'] %> 505 | """ 506 | When I successfully run `msync update --noop -f puppet-test` 507 | Then the output should match: 508 | """ 509 | Files added: 510 | test 511 | """ 512 | And the file named "modules/fakenamespace/puppet-test/test" should contain "aruba" 513 | And a directory named "modules/fakenamespace/puppet-blacksmith" should not exist 514 | And the puppet module "puppet-test" from "fakenamespace" should have no commits made by "Aruba" 515 | 516 | Scenario: When specifying configurations in managed_modules.yml and using a negative filter 517 | Given a mocked git configuration 518 | And a puppet module "puppet-test" from "fakenamespace" 519 | And a puppet module "puppet-blacksmith" from "fakenamespace" 520 | And a file named "managed_modules.yml" with: 521 | """ 522 | --- 523 | puppet-blacksmith: 524 | puppet-test: 525 | module_name: test 526 | """ 527 | And a file named "modulesync.yml" with: 528 | """ 529 | --- 530 | namespace: fakenamespace 531 | """ 532 | And a git_base option appended to "modulesync.yml" for local tests 533 | And a file named "config_defaults.yml" with: 534 | """ 535 | --- 536 | test: 537 | name: aruba 538 | """ 539 | And a directory named "moduleroot" 540 | And a file named "moduleroot/test.erb" with: 541 | """ 542 | <%= @configs['name'] %> 543 | """ 544 | When I successfully run `msync update --noop -x puppet-blacksmith` 545 | Then the output should match: 546 | """ 547 | Files added: 548 | test 549 | """ 550 | And the file named "modules/fakenamespace/puppet-test/test" should contain "aruba" 551 | And a directory named "modules/fakenamespace/puppet-blacksmith" should not exist 552 | And the puppet module "puppet-test" from "fakenamespace" should have no commits made by "Aruba" 553 | 554 | Scenario: Updating a module with a .sync.yml file 555 | Given a basic setup with a puppet module "puppet-test" from "fakenamespace" 556 | And a file named "config_defaults.yml" with: 557 | """ 558 | --- 559 | :global: 560 | global-default: some-default 561 | global-to-overwrite: to-be-overwritten 562 | spec/spec_helper.rb: 563 | require: 564 | - puppetlabs_spec_helper/module_helper 565 | """ 566 | And a file named "moduleroot/spec/spec_helper.rb.erb" with: 567 | """ 568 | <% @configs['require'].each do |required| -%> 569 | require '<%= required %>' 570 | <% end %> 571 | """ 572 | And a file named "moduleroot/global-test.md.erb" with: 573 | """ 574 | <%= @configs['global-default'] %> 575 | <%= @configs['global-to-overwrite'] %> 576 | <%= @configs['module-default'] %> 577 | """ 578 | And the puppet module "puppet-test" from "fakenamespace" has a file named ".sync.yml" with: 579 | """ 580 | --- 581 | :global: 582 | global-to-overwrite: it-is-overwritten 583 | module-default: some-value 584 | spec/spec_helper.rb: 585 | unmanaged: true 586 | """ 587 | When I successfully run `msync update --noop` 588 | Then the output should match: 589 | """ 590 | Not managing 'spec/spec_helper.rb' in 'puppet-test' 591 | """ 592 | And the file named "modules/fakenamespace/puppet-test/global-test.md" should contain: 593 | """ 594 | some-default 595 | it-is-overwritten 596 | some-value 597 | """ 598 | And the puppet module "puppet-test" from "fakenamespace" should have no commits made by "Aruba" 599 | 600 | Scenario: Module with custom namespace 601 | Given a mocked git configuration 602 | And a puppet module "puppet-test" from "fakenamespace" 603 | And a puppet module "puppet-lib-file_concat" from "electrical" 604 | And a file named "managed_modules.yml" with: 605 | """ 606 | --- 607 | - puppet-test 608 | - electrical/puppet-lib-file_concat 609 | """ 610 | And a file named "modulesync.yml" with: 611 | """ 612 | --- 613 | namespace: fakenamespace 614 | """ 615 | And a git_base option appended to "modulesync.yml" for local tests 616 | And a file named "config_defaults.yml" with: 617 | """ 618 | --- 619 | test: 620 | name: aruba 621 | """ 622 | And a directory named "moduleroot" 623 | And a file named "moduleroot/test.erb" with: 624 | """ 625 | <%= @configs['name'] %> 626 | """ 627 | When I successfully run `msync update --noop` 628 | Then the output should match: 629 | """ 630 | Files added: 631 | test 632 | """ 633 | And the file named "modules/fakenamespace/puppet-test/.git/config" should match /^\s+url = .*fakenamespace.puppet-test$/ 634 | And the file named "modules/electrical/puppet-lib-file_concat/.git/config" should match /^\s+url = .*electrical.puppet-lib-file_concat$/ 635 | And the puppet module "puppet-test" from "fakenamespace" should have no commits made by "Aruba" 636 | And the puppet module "puppet-lib-file_concat" from "electrical" should have no commits made by "Aruba" 637 | 638 | Scenario: Modifying an existing file with values exposed by the module 639 | Given a basic setup with a puppet module "puppet-test" from "fakenamespace" 640 | And a file named "config_defaults.yml" with: 641 | """ 642 | --- 643 | README.md: 644 | """ 645 | And a directory named "moduleroot" 646 | And a file named "moduleroot/README.md.erb" with: 647 | """ 648 | module: <%= @configs[:puppet_module] %> 649 | namespace: <%= @configs[:namespace] %> 650 | """ 651 | And the puppet module "puppet-test" from "fakenamespace" has a file named "README.md" with: 652 | """ 653 | Hello world! 654 | """ 655 | When I successfully run `msync update --noop` 656 | Then the output should match: 657 | """ 658 | Files changed: 659 | +diff --git a/README.md b/README.md 660 | """ 661 | And the file named "modules/fakenamespace/puppet-test/README.md" should contain: 662 | """ 663 | module: puppet-test 664 | namespace: fakenamespace 665 | """ 666 | And the puppet module "puppet-test" from "fakenamespace" should have no commits made by "Aruba" 667 | 668 | Scenario: Running the same update twice and pushing to a remote branch 669 | Given a basic setup with a puppet module "puppet-test" from "fakenamespace" 670 | And a file named "config_defaults.yml" with: 671 | """ 672 | --- 673 | Gemfile: 674 | gem_source: https://somehost.com 675 | """ 676 | And a directory named "moduleroot" 677 | And a file named "moduleroot/Gemfile.erb" with: 678 | """ 679 | source '<%= @configs['gem_source'] %>' 680 | """ 681 | When I successfully run `msync update -m "Update Gemfile" -r test` 682 | Then the puppet module "puppet-test" from "fakenamespace" should have only 1 commit made by "Aruba" 683 | And the puppet module "puppet-test" from "fakenamespace" should have 1 commit made by "Aruba" in branch "test" 684 | Given I remove the directory "modules" 685 | When I successfully run `msync update -m "Update Gemfile" -r test` 686 | Then the output should not contain "error" 687 | Then the output should not contain "rejected" 688 | And the puppet module "puppet-test" from "fakenamespace" should have only 1 commit made by "Aruba" 689 | And the puppet module "puppet-test" from "fakenamespace" should have 1 commit made by "Aruba" in branch "test" 690 | 691 | Scenario: Repository with a default branch other than master 692 | Given a basic setup with a puppet module "puppet-test" from "fakenamespace" 693 | And the puppet module "puppet-test" from "fakenamespace" has the default branch named "develop" 694 | And a file named "config_defaults.yml" with: 695 | """ 696 | --- 697 | Gemfile: 698 | gem_source: https://somehost.com 699 | """ 700 | And a directory named "moduleroot" 701 | And a file named "moduleroot/Gemfile.erb" with: 702 | """ 703 | source '<%= @configs['gem_source'] %>' 704 | """ 705 | When I successfully run `msync update --verbose -m "Update Gemfile"` 706 | Then the output should contain "Using repository's default branch: develop" 707 | And the puppet module "puppet-test" from "fakenamespace" should have only 1 commit made by "Aruba" 708 | And the puppet module "puppet-test" from "fakenamespace" should have 1 commit made by "Aruba" in branch "develop" 709 | 710 | Scenario: Adding a new file from a template using metadata 711 | Given a basic setup with a puppet module "puppet-test" from "fakenamespace" 712 | And a file named "config_defaults.yml" with: 713 | """ 714 | --- 715 | """ 716 | And a directory named "moduleroot" 717 | And a file named "moduleroot/test.erb" with: 718 | """ 719 | module: <%= @metadata[:module_name] %> 720 | namespace: <%= @metadata[:namespace] %> 721 | target: <%= @metadata[:target_file] %> 722 | workdir: <%= @metadata[:workdir] %> 723 | """ 724 | When I successfully run `msync update --noop` 725 | Then the file named "modules/fakenamespace/puppet-test/test" should contain: 726 | """ 727 | module: puppet-test 728 | namespace: fakenamespace 729 | target: modules/fakenamespace/puppet-test/test 730 | workdir: modules/fakenamespace/puppet-test 731 | """ 732 | And the puppet module "puppet-test" from "fakenamespace" should have no commits made by "Aruba" 733 | 734 | # This reproduces the issue: https://github.com/voxpupuli/modulesync/issues/81 735 | Scenario: Resync repositories after upstream branch deletion 736 | Given a basic setup with a puppet module "puppet-test" from "fakenamespace" 737 | And a file named "config_defaults.yml" with: 738 | """ 739 | --- 740 | test: 741 | name: aruba 742 | """ 743 | And a directory named "moduleroot" 744 | And a file named "moduleroot/test.erb" with: 745 | """ 746 | <%= @configs['name'] %> 747 | """ 748 | When I successfully run `msync update -m "No changes!" --branch delete-me` 749 | Then the puppet module "puppet-test" from "fakenamespace" should have 1 commit made by "Aruba" in branch "delete-me" 750 | When the branch "delete-me" of the puppet module "puppet-test" from "fakenamespace" is deleted 751 | And I successfully run `msync update -m "No changes!" --branch delete-me` 752 | Then the puppet module "puppet-test" from "fakenamespace" should have no commits made by "Aruba" 753 | --------------------------------------------------------------------------------