Module: RepoIntrospection

Defined in:
lib/core/repo_introspection.rb

Constant Summary collapse

DEFAULT_APP_PREFIX =
"my-app"
RUBY_VERSION_DIRECTIVE_PATTERN =
/^\s*ruby\s+['"\d]/
RUBY_VERSION_DIRECTIVE_PREFIX =
/^\s*ruby\s+/

Class Method Summary collapse

Class Method Details

.database_connection_config?(config) ⇒ Boolean

Returns:

  • (Boolean)


127
128
129
# File 'lib/core/repo_introspection.rb', line 127

def self.database_connection_config?(config)
  config.is_a?(Hash) && (config.key?("adapter") || config.key?("url"))
end

.direct_sqlite_database_config?(config) ⇒ Boolean

Returns true/false when a direct ‘url` or `adapter` key is conclusive, or nil when neither key is present so the caller can check nested sub-configs. rubocop:disable Style/ReturnNilInPredicateMethodDefinition – ternary nil/true/false is load-bearing for the caller

Returns:

  • (Boolean)


107
108
109
110
111
112
113
114
# File 'lib/core/repo_introspection.rb', line 107

def self.direct_sqlite_database_config?(config)
  url = config["url"]
  return sqlite_database_url?(url) if url.is_a?(String) && !url.strip.empty?

  return sqlite_adapter_in_hash?(config) if config["adapter"].is_a?(String)

  nil
end

.inferred_app_prefix(root) ⇒ Object

Returns a Control Plane-safe app prefix derived from the basename of ‘root`: lower-cased, with non-alphanumeric runs collapsed to dashes and stripped from the ends. Falls back to DEFAULT_APP_PREFIX when the result is empty.



65
66
67
68
69
70
71
72
# File 'lib/core/repo_introspection.rb', line 65

def self.inferred_app_prefix(root)
  sanitized = File.basename(root)
                  .downcase
                  .gsub(/[^a-z0-9]+/, "-")
                  .gsub(/\A-+|-+\z/, "")

  sanitized.empty? ? DEFAULT_APP_PREFIX : sanitized
end

.inferred_ruby_version_string(root) ⇒ Object

Returns the first Ruby version string the repo declares, checked in the order Bundler itself uses: ‘.ruby-version`, then `.tool-versions`, then `Gemfile`. Returns nil when no source declares a version. Both `Command::Generator` and `GithubFlowReadinessService` call into this so a future format change (e.g. `.tool-versions`) only updates here.



21
22
23
24
25
# File 'lib/core/repo_introspection.rb', line 21

def self.inferred_ruby_version_string(root)
  ruby_version_from_ruby_version_file(root) ||
    ruby_version_from_tool_versions(root) ||
    ruby_version_from_gemfile(root)
end

.nested_sqlite_database_config?(config) ⇒ Boolean

rubocop:enable Style/ReturnNilInPredicateMethodDefinition

Returns:

  • (Boolean)


117
118
119
120
121
122
123
124
125
# File 'lib/core/repo_introspection.rb', line 117

def self.nested_sqlite_database_config?(config)
  # In Rails multi-database configs, hash values with adapter/url keys are named
  # connections such as primary:, cache:, or queue:. Scalar and incidental hash
  # settings are ignored.
  sub_configs = config.values.select { |value| database_connection_config?(value) }
  return false if sub_configs.empty?

  sub_configs.all? { |sub| sqlite_database_config?(sub) }
end

.parse_ruby_version_string(source) ⇒ Object

Pure string → version-string extractor. Strips a leading ‘ruby-` prefix and returns the first `MAJOR.MINOR` found in the source, or nil.



12
13
14
15
# File 'lib/core/repo_introspection.rb', line 12

def self.parse_ruby_version_string(source)
  normalized = source.strip.sub(/\Aruby-/, "")
  normalized[/\d+\.\d+(?:\.\d+)?/]
end

.ruby_version_from_gemfile(root) ⇒ Object



44
45
46
47
48
49
50
51
52
53
54
# File 'lib/core/repo_introspection.rb', line 44

def self.ruby_version_from_gemfile(root)
  path = File.join(root, "Gemfile")
  return unless File.file?(path)

  ruby_lines = File.readlines(path, chomp: true).grep(RUBY_VERSION_DIRECTIVE_PREFIX)
  ruby_line = ruby_lines.find { |line| line.match?(RUBY_VERSION_DIRECTIVE_PATTERN) }
  warn_dynamic_ruby_directive if ruby_lines.any? && ruby_line.nil?
  return unless ruby_line

  parse_ruby_version_string(ruby_line.sub(RUBY_VERSION_DIRECTIVE_PREFIX, ""))
end

.ruby_version_from_ruby_version_file(root) ⇒ Object



27
28
29
30
31
32
# File 'lib/core/repo_introspection.rb', line 27

def self.ruby_version_from_ruby_version_file(root)
  path = File.join(root, ".ruby-version")
  return unless File.file?(path)

  parse_ruby_version_string(File.read(path))
end

.ruby_version_from_tool_versions(root) ⇒ Object



34
35
36
37
38
39
40
41
42
# File 'lib/core/repo_introspection.rb', line 34

def self.ruby_version_from_tool_versions(root)
  path = File.join(root, ".tool-versions")
  return unless File.file?(path)

  ruby_line = File.readlines(path, chomp: true).find { |line| line.match?(RUBY_VERSION_DIRECTIVE_PREFIX) }
  return unless ruby_line

  parse_ruby_version_string(ruby_line.sub(RUBY_VERSION_DIRECTIVE_PREFIX, ""))
end

.safe_load_database_yml(raw_contents) ⇒ Object



131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/core/repo_introspection.rb', line 131

def self.safe_load_database_yml(raw_contents)
  # ERB conditionals can change YAML structure, so avoid guessing. Output-only
  # ERB is stubbed as a scalar so common Rails defaults like `pool: <%= ... %>`
  # still parse, but control-flow ERB returns unknown. `<%- ... %>` is a
  # whitespace-trimming code tag, not an output tag, so treat it as unknown too.
  # Callers treat unknown as non-SQLite and
  # emit the default Postgres scaffold rather than guessing wrong.
  return nil if raw_contents.match?(/<%(?![=#])/m)

  stubbed = raw_contents.gsub(/<%=.*?%>/m, "__erb__").gsub(/<%#.*?%>/m, "")
  YAML.safe_load(stubbed, aliases: true, permitted_classes: [Symbol])
rescue Psych::SyntaxError
  nil
end

.sqlite_adapter_in_hash?(config) ⇒ Boolean

Returns:

  • (Boolean)


146
147
148
149
150
151
# File 'lib/core/repo_introspection.rb', line 146

def self.sqlite_adapter_in_hash?(config)
  return false unless config.is_a?(Hash)

  adapter = config["adapter"]
  adapter.is_a?(String) && adapter.strip.start_with?("sqlite3")
end

.sqlite_database_config?(config) ⇒ Boolean

Determines whether a database config hash uses SQLite. Handles both the single-database shape (top-level ‘adapter`/`url`) and Rails 6.1+ multi-database shape where each connection sits one level deeper (`primary:`, `cache:`, etc.). Returns false on any explicit non-SQLite adapter so a mixed config (e.g. Postgres primary + SQLite cache) keeps the Postgres scaffold rather than a volume scaffold.

Returns:

  • (Boolean)


95
96
97
98
99
100
101
102
# File 'lib/core/repo_introspection.rb', line 95

def self.sqlite_database_config?(config)
  return false unless config.is_a?(Hash)

  direct_result = direct_sqlite_database_config?(config)
  return direct_result unless direct_result.nil?

  nested_sqlite_database_config?(config)
end

.sqlite_database_in_production?(root) ⇒ Boolean

Returns true if ‘config/database.yml` under `root` configures SQLite for production. YAML merge keys such as `<<: *default` are resolved by safe_load, so only the final production hash should be inspected.

Returns:

  • (Boolean)


77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/core/repo_introspection.rb', line 77

def self.sqlite_database_in_production?(root)
  path = File.join(root, "config/database.yml")
  return false unless File.file?(path)

  parsed = safe_load_database_yml(File.read(path))
  return false unless parsed.is_a?(Hash)

  production = parsed["production"]
  return false unless production.is_a?(Hash)

  sqlite_database_config?(production)
end

.sqlite_database_url?(url) ⇒ Boolean

Returns:

  • (Boolean)


153
154
155
# File 'lib/core/repo_introspection.rb', line 153

def self.sqlite_database_url?(url)
  url.strip.downcase.start_with?("sqlite:", "sqlite3:")
end

.warn_dynamic_ruby_directiveObject



56
57
58
59
60
# File 'lib/core/repo_introspection.rb', line 56

def self.warn_dynamic_ruby_directive
  return unless ENV["CPFLOW_DEBUG"]

  warn "cpflow: Gemfile has a dynamic `ruby` directive; falling back to the default Ruby version"
end