Class: GithubFlowReadinessService

Inherits:
Object
  • Object
show all
Defined in:
lib/core/github_flow_readiness_service.rb

Overview

Drives the readiness checks that gate ‘cpflow generate-github-actions`. The actual checks live in `GithubFlowReadiness::Checks`; this class is the host that owns the shared lockfile parser, package.json parser, HTTP version cache, and registry-check helpers used across multiple checks. Add a new check by creating a class with `call` under `GithubFlowReadiness::Checks` and registering it in `CHECKS`.

Defined Under Namespace

Classes: RegistryCheck

Constant Summary collapse

Result =

rubocop:disable Metrics/ClassLength

GithubFlowReadiness::Result
CHECKS =
[
  GithubFlowReadiness::Checks::RailsApp,
  GithubFlowReadiness::Checks::RubyVersion,
  GithubFlowReadiness::Checks::BundlerVersion,
  GithubFlowReadiness::Checks::Dockerfile,
  GithubFlowReadiness::Checks::SqliteProduction,
  GithubFlowReadiness::Checks::GemSources,
  GithubFlowReadiness::Checks::GemExactPins,
  GithubFlowReadiness::Checks::NpmExactPins
].freeze
PUBLIC_RUBYGEMS_REMOTE =
"https://rubygems.org"
REGISTRY_FETCH_THREADS =
8
REGISTRY_FETCH_TIMEOUT_SECONDS =
60

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(root_path: Dir.pwd) ⇒ GithubFlowReadinessService

Returns a new instance of GithubFlowReadinessService.



47
48
49
50
51
52
# File 'lib/core/github_flow_readiness_service.rb', line 47

def initialize(root_path: Dir.pwd)
  @root_path = Pathname.new(root_path)
  @package_json_parse_error = false
  @rubygems_versions_cache = build_registry_cache
  @npm_versions_cache = build_registry_cache
end

Instance Attribute Details

#root_pathObject (readonly)

Returns the value of attribute root_path.



45
46
47
# File 'lib/core/github_flow_readiness_service.rb', line 45

def root_path
  @root_path
end

Instance Method Details

#blockers?Boolean

Returns:

  • (Boolean)


58
59
60
# File 'lib/core/github_flow_readiness_service.rb', line 58

def blockers?
  results.any? { |result| result.status == :fail }
end

#exact_pin_registry_result(check) ⇒ Object



160
161
162
163
164
165
166
167
168
169
170
# File 'lib/core/github_flow_readiness_service.rb', line 160

def exact_pin_registry_result(check)
  return Result.new(status: :info, message: check.empty_message) if check.dependencies.empty?

  grouped = partition_dependencies(check.dependencies, check.availability_proc)
  results = []
  results << registry_unavailable_result(check, grouped[:unavailable]) if grouped[:unavailable].any?
  results << registry_unknown_result(check, grouped[:unknown]) if grouped[:unknown].any?
  return results if results.any?

  Result.new(status: :pass, message: registry_success_message(check))
end

#fetch_npm_versions(name) ⇒ Object

Stubbed in specs; keep public.



178
179
180
# File 'lib/core/github_flow_readiness_service.rb', line 178

def fetch_npm_versions(name)
  fetch_with_cache(npm_versions_cache, name) { fetch_versions_from_npm(name) }
end

#fetch_rubygems_versions(name) ⇒ Object

Stubbed in specs; keep public.



173
174
175
# File 'lib/core/github_flow_readiness_service.rb', line 173

def fetch_rubygems_versions(name)
  fetch_with_cache(rubygems_versions_cache, name) { fetch_versions_from_rubygems(name) }
end

#gem_dependenciesObject


Helpers exposed to check classes (and stubbed by specs).




74
75
76
# File 'lib/core/github_flow_readiness_service.rb', line 74

def gem_dependencies
  @gem_dependencies ||= load_gem_dependencies
end

#inferred_ruby_versionObject



85
86
87
88
# File 'lib/core/github_flow_readiness_service.rb', line 85

def inferred_ruby_version
  version_string = RepoIntrospection.inferred_ruby_version_string(root_path.to_s)
  Gem::Version.new(version_string) if version_string
end

#lockfile_bundler_versionObject



90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/core/github_flow_readiness_service.rb', line 90

def lockfile_bundler_version
  file_path = root_path.join("Gemfile.lock")
  return unless file_path.file?

  lines = file_path.readlines(chomp: true)
  bundler_index = lines.index("BUNDLED WITH")
  return unless bundler_index

  version = lines[(bundler_index + 1)..]&.find { |line| !line.strip.empty? }&.strip
  return unless version

  Gem::Version.new(version)
end

#npm_registry_checkObject



148
149
150
151
152
153
154
155
156
157
158
# File 'lib/core/github_flow_readiness_service.rb', line 148

def npm_registry_check
  RegistryCheck.new(
    dependencies: exact_npm_dependencies,
    empty_message: "No exact-pinned direct npm packages to verify.",
    missing_prefix: "Direct npm package versions not available on npm",
    unknown_prefix: "Could not verify some exact-pinned npm packages against npm",
    success_noun: "direct npm package",
    availability_proc: method(:npm_dependency_available?),
    registry_name: "npm"
  )
end

#package_json_parse_errorObject



121
122
123
124
125
126
127
# File 'lib/core/github_flow_readiness_service.rb', line 121

def package_json_parse_error
  # Calling `parsed_package_json` here is the explicit "make sure parsing has run"
  # trigger, so a reader of `package_json_parse_error` does not have to know that the
  # flag is populated lazily. Guarded so memoized state isn't re-fetched.
  parsed_package_json unless instance_variable_defined?(:@parsed_package_json)
  @package_json_parse_error
end

#package_json_parse_error_resultObject



129
130
131
132
133
134
# File 'lib/core/github_flow_readiness_service.rb', line 129

def package_json_parse_error_result
  Result.new(
    status: :warn,
    message: "Could not parse `package.json`; exact-pinned direct npm package readiness could not be fully verified."
  )
end

#parsed_package_jsonObject



108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/core/github_flow_readiness_service.rb', line 108

def parsed_package_json
  return @parsed_package_json if instance_variable_defined?(:@parsed_package_json)

  package_json_path = root_path.join("package.json")
  @package_json_parse_error = false
  return @parsed_package_json = nil unless package_json_path.file?

  @parsed_package_json = JSON.parse(package_json_path.read)
rescue JSON::ParserError
  @package_json_parse_error = true
  @parsed_package_json = nil
end

#public_rubygems_dependency?(dependency) ⇒ Boolean

Returns:

  • (Boolean)


78
79
80
81
82
83
# File 'lib/core/github_flow_readiness_service.rb', line 78

def public_rubygems_dependency?(dependency)
  return false unless dependency[:source_type] == :rubygems

  remotes = dependency[:source_remotes]
  remotes.empty? || remotes.all? { |remote| remote == PUBLIC_RUBYGEMS_REMOTE }
end

#resultsObject



54
55
56
# File 'lib/core/github_flow_readiness_service.rb', line 54

def results
  @results ||= CHECKS.flat_map { |klass| wrap_check_result(klass.new(self).call) }
end

#rubygems_registry_checkObject



136
137
138
139
140
141
142
143
144
145
146
# File 'lib/core/github_flow_readiness_service.rb', line 136

def rubygems_registry_check
  RegistryCheck.new(
    dependencies: exact_rubygems_dependencies,
    empty_message: "No exact-pinned direct Ruby gems to verify.",
    missing_prefix: "Direct Ruby gem versions not available on RubyGems",
    unknown_prefix: "Could not verify some exact-pinned Ruby gems against RubyGems",
    success_noun: "direct Ruby gem",
    availability_proc: method(:rubygems_requirement_available?),
    registry_name: "RubyGems"
  )
end

#sqlite_database_in_production?Boolean

Returns:

  • (Boolean)


104
105
106
# File 'lib/core/github_flow_readiness_service.rb', line 104

def sqlite_database_in_production?
  RepoIntrospection.sqlite_database_in_production?(root_path.to_s)
end

#summaryObject



62
63
64
65
66
67
68
# File 'lib/core/github_flow_readiness_service.rb', line 62

def summary
  if blockers?
    "Blockers found. Fix them before generating the Control Plane GitHub flow."
  else
    "No blocking readiness issues detected. Validate the real production build path before merging."
  end
end