migration_signature 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 21b9a40066ce68440305cfca161d1f774d80be846ea19ea2938df5612e7c32cc
4
+ data.tar.gz: 03713b084e7381e46b17bcbfd67a7f3ca8647a1b8ef363d7a9862d40e999c3f7
5
+ SHA512:
6
+ metadata.gz: ac6c41cbc16270c4823ff650bfc6b71310bfbf35fc87755fd8acfff2dac71433bec24cd57978f38b4d4a9100ceeff8785c0107f3617c0e0edd1430e9857dd015
7
+ data.tar.gz: 7db7de487a64a60bb53a815d6b00b05690a2673178d9d02c7487a0157a00d695c5b1cba2d574f83dae0d37b08392d7e3ed87beb39f96f8b629e5f3463819ed10
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in migration_signature.gemspec
6
+ gemspec
7
+
8
+ gem 'rubocop'
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'migration_signature'
5
+ require 'optparse'
6
+
7
+ OptionParser.new do |parser|
8
+ parser.banner = <<~BANNER
9
+ Usage: migration_signature {check|buildall|build <file>}
10
+
11
+ check - Check all your migration files for present and correct migration signatures
12
+ buildall - Rebuild all migration signatures for all files (should only be used in setup)
13
+ build <file> - Rebuild a migration signature for a single migration
14
+
15
+ BANNER
16
+
17
+ parser.on('-h', '--help', 'Prints this help') do
18
+ puts parser
19
+ exit
20
+ end
21
+ end.parse!
22
+
23
+ action = ARGV[0]
24
+ unless action
25
+ warn 'You must supply one of {check|buildall|build <file>}'
26
+ exit 1
27
+ end
28
+
29
+ if action == 'check'
30
+ begin
31
+ MigrationSignature.check_all
32
+ rescue MigrationSignature::InvalidSignatureError => e
33
+ warn e.message
34
+ exit 1
35
+ end
36
+ elsif action == 'buildall'
37
+ MigrationSignature.build_all
38
+ elsif action == 'build'
39
+ file = ARGV[1]
40
+
41
+ unless file
42
+ warn 'You must supply a file to build a signature for'
43
+ exit 1
44
+ end
45
+
46
+ MigrationSignature.build_file(file)
47
+ else
48
+ warn 'You must supply one of {check|buildall|build <file>}'
49
+ exit 1
50
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MigrationSignature
4
+ class Config
5
+ DEFAULTS = {
6
+ 'ignore' => [],
7
+ 'rails_dir' => Dir.pwd,
8
+ 'migration_dir' => 'db/migrate'
9
+ }.freeze
10
+
11
+ CONFIG_FILE_PATH = "#{Dir.pwd}/.migration_signature.yml"
12
+
13
+ def self.load
14
+ return new(DEFAULTS) unless File.exist?(CONFIG_FILE_PATH)
15
+
16
+ require 'yaml'
17
+ hash = YAML.safe_load(File.read(CONFIG_FILE_PATH), [Regexp]) || {}
18
+
19
+ new(DEFAULTS.merge(hash))
20
+ end
21
+
22
+ def initialize(opts = DEFAULTS)
23
+ @opts = opts
24
+ end
25
+
26
+ def all_runnable_files
27
+ @all_runnable_files ||= begin
28
+ require 'pathname'
29
+
30
+ Dir["#{migration_dir}/*"].sort.reject do |f|
31
+ ignore?(f)
32
+ end
33
+ end
34
+ end
35
+
36
+ def migration_dir
37
+ "#{@opts['rails_dir']}/#{@opts['migration_dir']}"
38
+ end
39
+
40
+ def ignore?(file)
41
+ rails_root_file =
42
+ Pathname.new(file).relative_path_from(@opts['rails_dir']).to_s
43
+
44
+ return true if string_ignores.include?(file)
45
+ return true if string_ignores.include?(rails_root_file)
46
+ return true if regexp_ignores.any? { |ignore| ignore =~ file }
47
+ return true if regexp_ignores.any? { |ignore| ignore =~ rails_root_file }
48
+
49
+ false
50
+ end
51
+
52
+ private
53
+
54
+ def regexp_ignores
55
+ @regexp_ignores ||= @opts['ignore'].find_all { |i| i.is_a?(Regexp) }
56
+ end
57
+
58
+ def string_ignores
59
+ @string_ignores ||= @opts['ignore'].reject { |i| i.is_a?(Regexp) }
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MigrationSignature
4
+ class MigrationFile
5
+ MIGRATION_SIG_PREFIX = '# migration_signature: '
6
+
7
+ def self.resolve_full_name(path_or_version)
8
+ path =
9
+ if path_or_version =~ /^\d+$/
10
+ Dir["#{MigrationSignature.config.migration_dir}/#{path_or_version}*"]
11
+ .first
12
+ else
13
+ File.expand_path(path_or_version)
14
+ end
15
+
16
+ return path if path && File.exist?(path)
17
+
18
+ bad_basename = path ? path.split('/').last : path_or_version
19
+ raise("Could not find migration #{bad_basename}")
20
+ end
21
+
22
+ def initialize(path_or_version)
23
+ @path = self.class.resolve_full_name(path_or_version)
24
+ end
25
+
26
+ def ignore?
27
+ MigrationSignature.config.ignore?(@path)
28
+ end
29
+
30
+ def update_signature!
31
+ new_lines = lines.dup
32
+ unless signature?
33
+ new_lines.unshift("\n")
34
+ # add blank line between magic comments and content
35
+ if !new_lines[1].empty? && !new_lines[2].start_with?('#')
36
+ new_lines.unshift("\n")
37
+ end
38
+ end
39
+
40
+ new_lines[signature_line_number || 0] = new_source_signature_line
41
+ write_to_file(new_lines)
42
+ end
43
+
44
+ def validate_signature!
45
+ return true if valid_signature?
46
+
47
+ raise MigrationSignature::InvalidSignatureError,
48
+ 'Missing or invalid migration signature in migration: ' \
49
+ "#{basename}. Please re-run your migration to receive an " \
50
+ 'updated signature.'
51
+ end
52
+
53
+ def basename
54
+ File.basename(path)
55
+ end
56
+
57
+ private
58
+
59
+ attr_reader :path
60
+
61
+ def valid_signature?
62
+ return false unless signature?
63
+
64
+ comment_signature = lines[signature_line_number]
65
+ .sub(MIGRATION_SIG_PREFIX, '')
66
+ .strip
67
+ comment_signature == source_signature
68
+ end
69
+
70
+ def new_source_signature_line
71
+ "#{MIGRATION_SIG_PREFIX}#{source_signature}\n"
72
+ end
73
+
74
+ def source_signature
75
+ Digest::MD5.hexdigest(Parser::CurrentRuby.parse(lines.join).inspect)
76
+ end
77
+
78
+ def signature?
79
+ signature_line_number && signature_line_number > -1
80
+ end
81
+
82
+ def signature_line_number
83
+ lines.index { |l| l.start_with?(MIGRATION_SIG_PREFIX) }
84
+ end
85
+
86
+ def lines
87
+ @lines ||= File.read(path).lines
88
+ end
89
+
90
+ def write_to_file(new_lines)
91
+ File.write(path, new_lines.join)
92
+ remove_instance_variable(:@lines) if defined?(@lines)
93
+ true
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MigrationSignature
4
+ class Railtie < Rails::Railtie
5
+ initializer 'migration_signatures.prepend migrator' do
6
+ ActiveRecord::Migrator.prepend(MigrationSignature::MigratorEnhancement)
7
+ end
8
+ end
9
+
10
+ module MigratorEnhancement
11
+ def run
12
+ res = super
13
+ return res unless @direction == :up
14
+
15
+ MigrationSignature.build_file(@target_version.to_s)
16
+
17
+ puts('Migration signature successfully built')
18
+ res
19
+ end
20
+
21
+ def migrate
22
+ runnable = super
23
+
24
+ return runnable if @direction && @direction != :up
25
+
26
+ runnable.each do |migration|
27
+ MigrationSignature.build_file(migration.filename)
28
+ end
29
+
30
+ puts('Migration signatures successfully built')
31
+ runnable
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MigrationSignature
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'parser/current'
4
+ require 'digest/md5'
5
+ require_relative './migration_signature/config'
6
+ require_relative './migration_signature/migration_file'
7
+ require_relative './migration_signature/railtie' if defined?(Rails)
8
+
9
+ module MigrationSignature
10
+ class InvalidSignatureError < StandardError; end
11
+
12
+ def self.config
13
+ @config ||= MigrationSignature::Config.load
14
+ end
15
+
16
+ def self.check_all
17
+ config.all_runnable_files.each do |path|
18
+ MigrationSignature::MigrationFile.new(path).validate_signature!
19
+ end
20
+
21
+ true
22
+ end
23
+
24
+ def self.build_all
25
+ config.all_runnable_files.each do |path|
26
+ MigrationSignature::MigrationFile.new(path).update_signature!
27
+ end
28
+ end
29
+
30
+ def self.build_file(file)
31
+ mf = MigrationSignature::MigrationFile.new(file)
32
+
33
+ if mf.ignore?
34
+ warn "Tried to build signature for #{mf.basename}, but it is ignored."
35
+ return
36
+ end
37
+
38
+ mf.update_signature!
39
+ end
40
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'migration_signature/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'migration_signature'
9
+ spec.version = MigrationSignature::VERSION
10
+ spec.authors = ['Dave Allie']
11
+ spec.email = ['dave@tanda.co']
12
+
13
+ spec.summary = 'Generate signatures for migration files when they ' \
14
+ 'are run and check them in CI.'
15
+ spec.description = 'A small Rails utility to ensure all migration files ' \
16
+ 'have been tested before they are committed and run ' \
17
+ 'in production.'
18
+ spec.homepage = 'https://github.com/TandaHQ/migration_signature'
19
+ spec.license = 'MIT'
20
+
21
+ spec.files = Dir['exe/*'] + Dir['lib/**/*'] +
22
+ %w[Gemfile migration_signature.gemspec]
23
+ spec.bindir = 'exe'
24
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
25
+ spec.require_paths = ['lib']
26
+
27
+ spec.add_dependency 'parser', '>= 2'
28
+
29
+ spec.add_development_dependency 'bundler', '~> 2.0'
30
+ spec.add_development_dependency 'rake', '~> 10.0'
31
+ end
metadata ADDED
@@ -0,0 +1,97 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: migration_signature
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Dave Allie
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2019-06-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: parser
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ description: A small Rails utility to ensure all migration files have been tested
56
+ before they are committed and run in production.
57
+ email:
58
+ - dave@tanda.co
59
+ executables:
60
+ - migration_signature
61
+ extensions: []
62
+ extra_rdoc_files: []
63
+ files:
64
+ - Gemfile
65
+ - exe/migration_signature
66
+ - lib/migration_signature.rb
67
+ - lib/migration_signature/config.rb
68
+ - lib/migration_signature/migration_file.rb
69
+ - lib/migration_signature/railtie.rb
70
+ - lib/migration_signature/version.rb
71
+ - migration_signature.gemspec
72
+ homepage: https://github.com/TandaHQ/migration_signature
73
+ licenses:
74
+ - MIT
75
+ metadata: {}
76
+ post_install_message:
77
+ rdoc_options: []
78
+ require_paths:
79
+ - lib
80
+ required_ruby_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ required_rubygems_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ requirements: []
91
+ rubyforge_project:
92
+ rubygems_version: 2.7.9
93
+ signing_key:
94
+ specification_version: 4
95
+ summary: Generate signatures for migration files when they are run and check them
96
+ in CI.
97
+ test_files: []