diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 042e62c9ec5..057076b4494 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -47,6 +47,17 @@ jobs: with: bundler-cache: true + - name: Restore API cache + if: | + (matrix.test_type == 'collections' && steps.collections.outputs.changed) || + (matrix.test_type == 'all' && steps.all.outputs.changed) + uses: actions/cache@v4 + with: + path: .api-cache.json + key: api-cache-${{ matrix.test_type }}-${{ github.run_id }} + restore-keys: | + api-cache-${{ matrix.test_type }}- + - name: Build and test with Rake if: | (matrix.test_type == 'topics' && steps.topics.outputs.changed) || diff --git a/.gitignore b/.gitignore index a4c640a221d..71b5d53b1ee 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ vendor .bundle .idea .tool-versions +.api-cache.json diff --git a/test/test_helper.rb b/test/test_helper.rb index c878c64d302..53b2984e6f7 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -270,6 +270,89 @@ def add_message(type, file, line_number, message) client.messages << "::#{type} file=#{file},line=#{line_number}::#{message}" end +CACHE_FILE = File.expand_path("../.api-cache.json", __dir__) +CACHE_TTL_SECONDS = 24 * 60 * 60 # 24 hours + +def load_api_cache! + return unless File.exist?(CACHE_FILE) + + data = JSON.parse(File.read(CACHE_FILE)) + now = Time.now.to_i + ttl = CACHE_TTL_SECONDS + + if data["repos"] + data["repos"].each do |key, entry| + cached_at = entry["cached_at"] + next unless cached_at + next if now - cached_at.to_i > ttl + + result = entry["value"] + # Reconstruct a minimal object that responds to .full_name + cached = if result.nil? + nil + else + next unless result["full_name"] + + Struct.new(:full_name).new(result["full_name"]) + end + NewOctokit.class_variable_get(:@@repos)[key] = cached + end + end + + if data["users"] + data["users"].each do |key, entry| + cached_at = entry["cached_at"] + next unless cached_at + next if now - cached_at.to_i > ttl + + result = entry["value"] + cached = if result.nil? + nil + else + next unless result["login"] + + Struct.new(:login).new(result["login"]) + end + NewOctokit.class_variable_get(:@@users)[key] = cached + end + end +rescue JSON::ParserError, StandardError => e + warn "Failed to load API cache: #{e.message}" +end + +def save_api_cache! + now = Time.now.to_i + repos_data = {} + users_data = {} + + NewOctokit.class_variable_get(:@@repos).each do |key, value| + next if key == :skip_requests + next if value == true + + repos_data[key.to_s] = { + "cached_at" => now, + "value" => value.nil? ? nil : { "full_name" => value.respond_to?(:full_name) ? value.full_name : value.to_s }, + } + end + + NewOctokit.class_variable_get(:@@users).each do |key, value| + next if key == :skip_requests + next if value == true + + users_data[key.to_s] = { + "cached_at" => now, + "value" => value.nil? ? nil : { "login" => value.respond_to?(:login) ? value.login : value.to_s }, + } + end + + File.write(CACHE_FILE, JSON.pretty_generate({ "repos" => repos_data, "users" => users_data })) +rescue StandardError => e + warn "Failed to save API cache: #{e.message}" +end + +# Load cached API results at startup +load_api_cache! + Minitest.after_run do warn "Repo checks were rate limited during this CI run" if NewOctokit.repos_skipped? warn "User checks were rate limited during this CI run" if NewOctokit.users_skipped? @@ -279,4 +362,7 @@ def add_message(type, file, line_number, message) NewOctokit.messages.each do |message| puts message end + + # Persist cache for next CI run + save_api_cache! end