Skip to content

Commit

Permalink
add Git::SearchCommits callable that sends backend API request to che…
Browse files Browse the repository at this point in the history
…ck which commits are already known to the backend
  • Loading branch information
anmarchenko committed Apr 5, 2024
1 parent 9716194 commit 6a57b34
Show file tree
Hide file tree
Showing 11 changed files with 325 additions and 20 deletions.
27 changes: 11 additions & 16 deletions lib/datadog/ci/ext/environment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
require_relative "git"
require_relative "environment/extractor"

require_relative "../utils/git"

module Datadog
module CI
module Ext
Expand All @@ -21,8 +23,6 @@ module Environment
TAG_NODE_NAME = "ci.node.name"
TAG_CI_ENV_VARS = "_dd.ci.env_vars"

HEX_NUMBER_REGEXP = /[0-9a-f]{40}/i.freeze

module_function

def tags(env)
Expand Down Expand Up @@ -57,24 +57,19 @@ def validate_repository_url(repo_url)
end

def validate_git_sha(git_sha)
message = "DD_GIT_COMMIT_SHA must be a full-length git SHA."
return if Utils::Git.valid_commit_sha?(git_sha)

if git_sha.nil? || git_sha.empty?
message += " No value was set and no SHA was automatically extracted."
Datadog.logger.error(message)
return
end
message = "DD_GIT_COMMIT_SHA must be a full-length git SHA."

if git_sha.length < Git::SHA_LENGTH
message += " Expected SHA length #{Git::SHA_LENGTH}, was #{git_sha.length}."
Datadog.logger.error(message)
return
message += if git_sha.nil? || git_sha.empty?
" No value was set and no SHA was automatically extracted."
elsif git_sha.length < Git::SHA_LENGTH
" Expected SHA length #{Git::SHA_LENGTH}, was #{git_sha.length}."
else
" Expected SHA to be a valid HEX number, got #{git_sha}."
end

unless HEX_NUMBER_REGEXP.match?(git_sha)
message += " Expected SHA to be a valid HEX number, got #{git_sha}."
Datadog.logger.error(message)
end
Datadog.logger.error(message)
end
end
end
Expand Down
3 changes: 3 additions & 0 deletions lib/datadog/ci/ext/transport.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ module Transport
TEST_COVERAGE_INTAKE_PATH = "/api/v2/citestcov"

DD_API_HOST_PREFIX = "api"

DD_API_SETTINGS_PATH = "/api/v2/libraries/tests/services/setting"
DD_API_SETTINGS_TYPE = "ci_app_test_service_libraries_settings"
DD_API_SETTINGS_RESPONSE_DIG_KEYS = %w[data attributes].freeze
Expand All @@ -36,6 +37,8 @@ module Transport
DD_API_SETTINGS_RESPONSE_REQUIRE_GIT_KEY = "require_git"
DD_API_SETTINGS_RESPONSE_DEFAULT = {DD_API_SETTINGS_RESPONSE_ITR_ENABLED_KEY => false}.freeze

DD_API_GIT_SEARCH_COMMITS_PATH = "/api/v2/git/repository/search_commits"

CONTENT_TYPE_MESSAGEPACK = "application/msgpack"
CONTENT_TYPE_JSON = "application/json"
CONTENT_TYPE_MULTIPART_FORM_DATA = "multipart/form-data"
Expand Down
77 changes: 77 additions & 0 deletions lib/datadog/ci/git/search_commits.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# frozen_string_literal: true

require "json"
require "set"

require_relative "../ext/transport"
require_relative "../utils/git"

module Datadog
module CI
module Git
class SearchCommits
class ApiError < StandardError; end

attr_reader :api

def initialize(api:)
@api = api
end

def call(repository_url, commits)
raise ApiError, "test visibility API is not configured" if api.nil?

http_response = api.api_request(
path: Ext::Transport::DD_API_GIT_SEARCH_COMMITS_PATH,
payload: request_payload(repository_url, commits)
)
raise ApiError, "Failed to search commits: #{http_response.body}" unless http_response.ok?

response_payload = parse_json_response(http_response)
extract_commits(response_payload)
end

private

def request_payload(repository_url, commits)
{
meta: {
repository_url: repository_url
},
data: commits.filter_map do |commit|
next unless Utils::Git.valid_commit_sha?(commit)

{
id: commit,
type: "commit"
}
end
}.to_json
end

def parse_json_response(http_response)
JSON.parse(http_response.payload)
rescue JSON::ParserError => e
raise ApiError, "Failed to parse search commits response: #{e}. Payload was: #{http_response.payload}"
end

def extract_commits(response_payload)
result = Set.new

response_payload.fetch("data").each do |commit_json|
raise ApiError, "Invalid commit type response #{commit_json}" unless commit_json["type"] == "commit"

commit_sha = commit_json["id"]
raise ApiError, "Invalid commit SHA response #{commit_sha}" unless Utils::Git.valid_commit_sha?(commit_sha)

result.add(commit_sha)
end

result
rescue KeyError => e
raise ApiError, "Malformed search commits response: #{e}. Payload was: #{response_payload}"
end
end
end
end
end
6 changes: 6 additions & 0 deletions lib/datadog/ci/utils/git.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ module Datadog
module CI
module Utils
module Git
def self.valid_commit_sha?(sha)
return false if sha.nil?

sha.match?(/\A[0-9a-f]{40}\Z/) || sha.match?(/\A[0-9a-f]{64}\Z/)
end

def self.normalize_ref(ref)
return nil if ref.nil?

Expand Down
2 changes: 0 additions & 2 deletions sig/datadog/ci/ext/environment.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ module Datadog

TAG_CI_ENV_VARS: String

HEX_NUMBER_REGEXP: Regexp

PROVIDERS: ::Array[Array[String | Symbol]]

def self?.tags: (untyped env) -> Hash[String, String]
Expand Down
4 changes: 3 additions & 1 deletion sig/datadog/ci/ext/transport.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ module Datadog

DD_API_HOST_PREFIX: "api"

DD_API_SETTINGS_PATH: "/api/v2/ci/libraries/tests/services/setting"
DD_API_SETTINGS_PATH: "/api/v2/libraries/tests/services/setting"

DD_API_SETTINGS_TYPE: "ci_app_test_service_libraries_settings"

Expand All @@ -48,6 +48,8 @@ module Datadog

DD_API_SETTINGS_RESPONSE_DEFAULT: Hash[String, untyped]

DD_API_GIT_SEARCH_COMMITS_PATH: "/api/v2/git/repository/search_commits"

CONTENT_TYPE_MESSAGEPACK: "application/msgpack"

CONTENT_TYPE_JSON: "application/json"
Expand Down
26 changes: 26 additions & 0 deletions sig/datadog/ci/git/search_commits.rbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module Datadog
module CI
module Git
class SearchCommits
@api: Datadog::CI::Transport::Api::Base?

attr_reader api: Datadog::CI::Transport::Api::Base?

class ApiError < StandardError
end

def initialize: (api: Datadog::CI::Transport::Api::Base?) -> void

def call: (String repository_url, Array[String] commits) -> Set[String]

private

def request_payload: (String repository_url, Array[String] commits) -> String

def parse_json_response: (Datadog::Core::Transport::Response response) -> Hash[String, untyped]

def extract_commits: (Hash[String, untyped] response) -> Set[String]
end
end
end
end
2 changes: 2 additions & 0 deletions sig/datadog/ci/utils/git.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ module Datadog
@root: String?
@repository_name: String?

def self.valid_commit_sha?: (String? sha) -> bool

def self.normalize_ref: (String? name) -> String?

def self.is_git_tag?: (String? ref) -> bool
Expand Down
156 changes: 156 additions & 0 deletions spec/datadog/ci/git/search_commits_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# frozen_string_literal: true

require_relative "../../../../lib/datadog/ci/git/search_commits"

RSpec.describe Datadog::CI::Git::SearchCommits do
let(:api) { double("api") }
subject(:search_commits) { described_class.new(api: api) }

describe "#call" do
let(:repository_url) { "https://datadoghq.com/git/test.git" }
let(:commits) { ["c7f893648f656339f62fb7b4d8a6ecdf7d063835"] }

context "when the API is not configured" do
let(:api) { nil }

it "raises an error" do
expect { search_commits.call(repository_url, commits) }
.to raise_error(Datadog::CI::Git::SearchCommits::ApiError, "test visibility API is not configured")
end
end

context "when the API is configured" do
before do
allow(api).to receive(:api_request).and_return(http_response)
end

context "when the API request fails" do
let(:http_response) { double("http_response", ok?: false, body: "error message") }

it "raises an error" do
expect { search_commits.call(repository_url, commits) }
.to raise_error(Datadog::CI::Git::SearchCommits::ApiError, "Failed to search commits: error message")
end
end

context "when the API request is successful" do
let(:http_response) { double("http_response", ok?: true, payload: response_payload) }
let(:response_payload) do
{
"data" => [
{
"id" => "c7f893648f656339f62fb7b4d8a6ecdf7d063835",
"type" => "commit"
}
]
}.to_json
end

it "returns the list of commit SHAs" do
expect(api).to receive(:api_request).with(
path: Datadog::CI::Ext::Transport::DD_API_GIT_SEARCH_COMMITS_PATH,
payload: "{\"meta\":{\"repository_url\":\"https://datadoghq.com/git/test.git\"},\"data\":[{\"id\":\"c7f893648f656339f62fb7b4d8a6ecdf7d063835\",\"type\":\"commit\"}]}"
).and_return(http_response)

expect(search_commits.call(repository_url, commits)).to eq(Set.new(["c7f893648f656339f62fb7b4d8a6ecdf7d063835"]))
end

context "when the request contains an invalid commit SHA" do
let(:commits) { ["INVALID_SHA", "c7f893648f656339f62fb7b4d8a6ecdf7d063835"] }

it "does not include the invalid commit SHA in the request" do
expect(api).to receive(:api_request).with(
path: Datadog::CI::Ext::Transport::DD_API_GIT_SEARCH_COMMITS_PATH,
payload: "{\"meta\":{\"repository_url\":\"https://datadoghq.com/git/test.git\"},\"data\":[{\"id\":\"c7f893648f656339f62fb7b4d8a6ecdf7d063835\",\"type\":\"commit\"}]}"
).and_return(http_response)

expect(search_commits.call(repository_url, commits)).to eq(Set.new(["c7f893648f656339f62fb7b4d8a6ecdf7d063835"]))
end
end

context "when the response contains an invalid commit type" do
let(:response_payload) do
{
"data" => [
{
"id" => "c7f893648f656339f62fb7b4d8a6ecdf7d063835",
"type" => "invalid"
}
]
}.to_json
end

it "raises an error" do
expect { search_commits.call(repository_url, commits) }
.to raise_error(
Datadog::CI::Git::SearchCommits::ApiError,
"Invalid commit type response {\"id\"=>\"c7f893648f656339f62fb7b4d8a6ecdf7d063835\", \"type\"=>\"invalid\"}"
)
end
end

context "when the response contains an invalid commit SHA" do
let(:response_payload) do
{
"data" => [
{
"id" => "INVALID_SHA",
"type" => "commit"
}
]
}.to_json
end

it "raises an error" do
expect { search_commits.call(repository_url, commits) }
.to raise_error(Datadog::CI::Git::SearchCommits::ApiError, "Invalid commit SHA response INVALID_SHA")
end
end

context "when the response is not a valid JSON" do
let(:response_payload) { "invalid json" }

it "raises an error" do
expect { search_commits.call(repository_url, commits) }
.to raise_error(
Datadog::CI::Git::SearchCommits::ApiError,
"Failed to parse search commits response: unexpected token at 'invalid json'. Payload was: invalid json"
)
end
end

context "when the response is missing the data key" do
let(:response_payload) { {}.to_json }

it "raises an error" do
expect { search_commits.call(repository_url, commits) }
.to raise_error(
Datadog::CI::Git::SearchCommits::ApiError,
"Malformed search commits response: key not found: \"data\". Payload was: {}"
)
end
end

context "when the response is missing the commit type" do
let(:response_payload) do
{
"data" => [
{
"id" => "c7f893648f656339f62fb7b4d8a6ecdf7d063835"
}
]
}.to_json
end

it "raises an error" do
expect { search_commits.call(repository_url, commits) }
.to raise_error(
Datadog::CI::Git::SearchCommits::ApiError,
"Invalid commit type response {\"id\"=>\"c7f893648f656339f62fb7b4d8a6ecdf7d063835\"}"
)
end
end
end
end
end
end
Loading

0 comments on commit 6a57b34

Please sign in to comment.