Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 51 additions & 19 deletions kpm/lib/kpm/nexus_helper/github_api_calls.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,21 @@ module KPM
module NexusFacade
class GithubApiCalls < NexusApiCallsV2
def pull_artifact_endpoint(coordinates)
base_path, versioned_artifact, = build_base_path_and_coords(coordinates)
"#{base_path}/#{versioned_artifact}"
coords = parse_coordinates(coordinates)
resolved = resolve_snapshot_version(coordinates)
filename_version = resolved || coords[:version]
"#{artifact_base_path(coords)}/#{coords[:version]}/#{coords[:artifact_id]}-#{filename_version}.#{coords[:extension]}"
end

alias parent_get_artifact_info get_artifact_info
def get_artifact_info(coordinates)
super
coords = parse_coordinates(coordinates)
resolved = resolve_snapshot_version(coordinates)
filename_version = resolved || coords[:version]

versioned_artifact = "#{coords[:version]}/#{coords[:artifact_id]}-#{filename_version}.#{coords[:extension]}"
sha1 = fetch_sha1(coordinates, versioned_artifact)

_, versioned_artifact, coords = build_base_path_and_coords(coordinates)
sha1 = get_sha1(coordinates)
"<artifact-resolution>
<data>
<presentLocally>true</presentLocally>
Expand All @@ -33,8 +39,13 @@ def get_artifact_info(coordinates)
end

def get_artifact_info_endpoint(coordinates)
base_path, = build_base_path_and_coords(coordinates)
"#{base_path}/maven-metadata.xml"
coords = parse_coordinates(coordinates)
base_path = artifact_base_path(coords)
if coords[:version] =~ /-SNAPSHOT$/
"#{base_path}/#{coords[:version]}/maven-metadata.xml"
else
"#{base_path}/maven-metadata.xml"
end
end

def search_for_artifact_endpoint(_coordinates)
Expand All @@ -47,23 +58,44 @@ def build_query_params(_coordinates, _what_parameters = nil)

private

def get_sha1(coordinates)
base_path, versioned_artifact, = build_base_path_and_coords(coordinates)
endpoint = "#{base_path}/#{versioned_artifact}.sha1"
get_response_with_retries(coordinates, endpoint, nil)
# Resolves a SNAPSHOT version to its timestamped form using maven-metadata.xml.
# Returns nil for non-SNAPSHOT versions or when metadata is unavailable.
def resolve_snapshot_version(coordinates)
coords = parse_coordinates(coordinates)
return nil unless coords[:version] =~ /-SNAPSHOT$/

version_metadata = begin
parent_get_artifact_info(coordinates)
rescue StandardError
return nil
end

doc = REXML::Document.new(version_metadata)
timestamp = begin
doc.elements['//versioning/snapshot/timestamp'].text
rescue StandardError
nil
end
build_number = begin
doc.elements['//versioning/snapshot/buildNumber'].text
rescue StandardError
nil
end
return nil if timestamp.nil? || build_number.nil?

base_version = coords[:version].sub(/-SNAPSHOT$/, '')
"#{base_version}-#{timestamp}-#{build_number}"
end

def build_base_path_and_coords(coordinates)
def fetch_sha1(coordinates, versioned_artifact)
coords = parse_coordinates(coordinates)
endpoint = "#{artifact_base_path(coords)}/#{versioned_artifact}.sha1"
get_response_with_retries(coordinates, endpoint, nil)
end

# The url may contain the org and repo, e.g. 'https://maven.pkg.github.com/killbill/qualpay-java-client'
def artifact_base_path(coords)
org_and_repo = URI.parse(configuration[:url]).path

[
"#{org_and_repo}/#{coords[:group_id].gsub('.', '/')}/#{coords[:artifact_id]}",
"#{coords[:version]}/#{coords[:artifact_id]}-#{coords[:version]}.#{coords[:extension]}",
coords
]
"#{org_and_repo}/#{coords[:group_id].gsub('.', '/')}/#{coords[:artifact_id]}"
end
end
end
Expand Down
210 changes: 210 additions & 0 deletions kpm/spec/kpm/unit/github_api_calls_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
# frozen_string_literal: true

require 'spec_helper'
require 'rexml/document'

describe KPM::NexusFacade::GithubApiCalls do
let(:logger) do
logger = ::Logger.new(STDOUT)
logger.level = Logger::FATAL
logger
end
let(:config) { { url: 'https://maven.pkg.github.com/myorg/my-repo', token: 'fake-token' } }
let(:nexus_remote) { described_class.new(config, true, logger) }

let(:snapshot_coordinates) { 'com.example:my-plugin:jar:2.0.1-SNAPSHOT' }
let(:release_coordinates) { 'com.example:my-plugin:jar:1.0.0' }

let(:snapshot_metadata_xml) do
<<~XML
<metadata>
<groupId>com.example</groupId>
<artifactId>my-plugin</artifactId>
<version>2.0.1-SNAPSHOT</version>
<versioning>
<snapshot>
<timestamp>20240520.203819</timestamp>
<buildNumber>1</buildNumber>
</snapshot>
<snapshotVersions>
<snapshotVersion>
<extension>jar</extension>
<value>2.0.1-20240520.203819-1</value>
<updated>20240520203819</updated>
</snapshotVersion>
<snapshotVersion>
<extension>pom</extension>
<value>2.0.1-20240520.203819-1</value>
<updated>20240520203819</updated>
</snapshotVersion>
</snapshotVersions>
<lastUpdated>20240520203819</lastUpdated>
</versioning>
</metadata>
XML
end

describe '#get_artifact_info_endpoint' do
it 'returns version-level metadata URL for SNAPSHOT versions' do
endpoint = nexus_remote.get_artifact_info_endpoint(snapshot_coordinates)
expect(endpoint).to eq('/myorg/my-repo/com/example/my-plugin/2.0.1-SNAPSHOT/maven-metadata.xml')
end

it 'returns top-level metadata URL for release versions' do
endpoint = nexus_remote.get_artifact_info_endpoint(release_coordinates)
expect(endpoint).to eq('/myorg/my-repo/com/example/my-plugin/maven-metadata.xml')
end
end

describe '#pull_artifact_endpoint' do
context 'with a SNAPSHOT version' do
before do
allow(nexus_remote).to receive(:parent_get_artifact_info)
.with(snapshot_coordinates)
.and_return(snapshot_metadata_xml)
end

it 'uses SNAPSHOT directory with timestamped filename' do
endpoint = nexus_remote.pull_artifact_endpoint(snapshot_coordinates)
expect(endpoint).to eq('/myorg/my-repo/com/example/my-plugin/2.0.1-SNAPSHOT/my-plugin-2.0.1-20240520.203819-1.jar')
end
end

context 'with a release version' do
it 'uses the literal version' do
endpoint = nexus_remote.pull_artifact_endpoint(release_coordinates)
expect(endpoint).to eq('/myorg/my-repo/com/example/my-plugin/1.0.0/my-plugin-1.0.0.jar')
end
end

context 'when metadata fetch fails' do
before do
allow(nexus_remote).to receive(:parent_get_artifact_info)
.with(snapshot_coordinates)
.and_raise(StandardError, 'connection failed')
end

it 'falls back to the literal SNAPSHOT version' do
endpoint = nexus_remote.pull_artifact_endpoint(snapshot_coordinates)
expect(endpoint).to eq('/myorg/my-repo/com/example/my-plugin/2.0.1-SNAPSHOT/my-plugin-2.0.1-SNAPSHOT.jar')
end
end
end

describe '#get_artifact_info' do
let(:sha1_value) { 'abc123def456' }

context 'with a SNAPSHOT version' do
before do
allow(nexus_remote).to receive(:parent_get_artifact_info)
.with(snapshot_coordinates)
.and_return(snapshot_metadata_xml)
allow(nexus_remote).to receive(:get_response_with_retries)
.and_return(sha1_value)
end

it 'reports the original SNAPSHOT version' do
info = nexus_remote.get_artifact_info(snapshot_coordinates)
doc = REXML::Document.new(info)
expect(doc.elements['//version'].text).to eq('2.0.1-SNAPSHOT')
end

it 'marks the artifact as a snapshot' do
info = nexus_remote.get_artifact_info(snapshot_coordinates)
doc = REXML::Document.new(info)
expect(doc.elements['//snapshot'].text).to eq('true')
end

it 'uses SNAPSHOT directory with timestamped filename in repository path' do
info = nexus_remote.get_artifact_info(snapshot_coordinates)
doc = REXML::Document.new(info)
expect(doc.elements['//repositoryPath'].text).to eq('/com/example/2.0.1-SNAPSHOT/my-plugin-2.0.1-20240520.203819-1.jar')
end

it 'fetches sha1 using SNAPSHOT directory with timestamped filename' do
nexus_remote.get_artifact_info(snapshot_coordinates)
expect(nexus_remote).to have_received(:get_response_with_retries)
.with(snapshot_coordinates, '/myorg/my-repo/com/example/my-plugin/2.0.1-SNAPSHOT/my-plugin-2.0.1-20240520.203819-1.jar.sha1', nil)
end
end

context 'with a release version' do
before do
allow(nexus_remote).to receive(:get_response_with_retries)
.and_return(sha1_value)
end

it 'reports the release version' do
info = nexus_remote.get_artifact_info(release_coordinates)
doc = REXML::Document.new(info)
expect(doc.elements['//version'].text).to eq('1.0.0')
end

it 'marks the artifact as not a snapshot' do
info = nexus_remote.get_artifact_info(release_coordinates)
doc = REXML::Document.new(info)
expect(doc.elements['//snapshot'].text).to eq('false')
end

it 'uses the literal version in the repository path' do
info = nexus_remote.get_artifact_info(release_coordinates)
doc = REXML::Document.new(info)
expect(doc.elements['//repositoryPath'].text).to eq('/com/example/1.0.0/my-plugin-1.0.0.jar')
end
end
end

describe '#search_for_artifact_endpoint' do
it 'raises NoMethodError' do
expect { nexus_remote.search_for_artifact_endpoint(release_coordinates) }
.to raise_exception(NoMethodError, 'GitHub Packages has no search support')
end
end

describe 'SNAPSHOT with multiple builds' do
let(:metadata_with_multiple_builds) do
<<~XML
<metadata>
<groupId>com.example</groupId>
<artifactId>my-plugin</artifactId>
<version>2.0.1-SNAPSHOT</version>
<versioning>
<snapshot>
<timestamp>20240521.143000</timestamp>
<buildNumber>3</buildNumber>
</snapshot>
<snapshotVersions>
<snapshotVersion>
<extension>jar</extension>
<value>2.0.1-20240519.100000-1</value>
<updated>20240519100000</updated>
</snapshotVersion>
<snapshotVersion>
<extension>jar</extension>
<value>2.0.1-20240520.120000-2</value>
<updated>20240520120000</updated>
</snapshotVersion>
<snapshotVersion>
<extension>jar</extension>
<value>2.0.1-20240521.143000-3</value>
<updated>20240521143000</updated>
</snapshotVersion>
</snapshotVersions>
<lastUpdated>20240521143000</lastUpdated>
</versioning>
</metadata>
XML
end

before do
allow(nexus_remote).to receive(:parent_get_artifact_info)
.with(snapshot_coordinates)
.and_return(metadata_with_multiple_builds)
end

it 'uses the latest build from the snapshot element, not the first snapshotVersion' do
endpoint = nexus_remote.pull_artifact_endpoint(snapshot_coordinates)
expect(endpoint).to eq('/myorg/my-repo/com/example/my-plugin/2.0.1-SNAPSHOT/my-plugin-2.0.1-20240521.143000-3.jar')
end
end
end
Loading