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.



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

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.



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

def root_path
  @root_path
end

Instance Method Details

#blockers?Boolean

Returns:

  • (Boolean)


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

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

#exact_pin_registry_result(check) ⇒ Object



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

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.



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

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.



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

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).




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

def gem_dependencies
  @gem_dependencies ||= load_gem_dependencies
end

#inferred_ruby_versionObject



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

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



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

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



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

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



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

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



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

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



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

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)


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

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



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

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

#rubygems_registry_checkObject



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

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)


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

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

#summaryObject



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

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