diff --git a/changelog.md b/changelog.md index 90ca447..40f15ed 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2026-03-24 - 2.8.1 - fix(registry) +align OCI and RubyGems API behavior and improve npm search result ordering + +- handle OCI version checks on /v2 and /v2/ endpoints +- return RubyGems versions JSON in the expected flat array format and update unyank coverage to use the HTTP endpoint +- prioritize exact and prefix matches in npm search results +- update documentation to reflect full upstream proxy support + ## 2026-03-24 - 2.8.0 - feat(core,storage,oci,registry-config) add streaming response support and configurable registry URLs across protocols diff --git a/readme.md b/readme.md index 8b1dbfd..7b5c3c8 100644 --- a/readme.md +++ b/readme.md @@ -41,7 +41,7 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community | Metadata API | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | Token Auth | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | Checksum Verification | ✅ | ✅ | ✅ | ✅ | — | ✅ | ✅ | -| Upstream Proxy | ✅ | ✅ | — | — | — | — | — | +| Upstream Proxy | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ### 🌐 Upstream Proxy & Caching - **Multi-Upstream Support**: Configure multiple upstream registries per protocol with priority ordering diff --git a/test/test.rubygems.nativecli.node.ts b/test/test.rubygems.nativecli.node.ts index 159436f..a30dbbe 100644 --- a/test/test.rubygems.nativecli.node.ts +++ b/test/test.rubygems.nativecli.node.ts @@ -148,11 +148,16 @@ async function runGemCommand( cwd: string, includeAuth: boolean = true ): Promise<{ stdout: string; stderr: string; exitCode: number }> { + // When not including auth, use a temp HOME without credentials + const effectiveHome = includeAuth ? gemHome : path.join(gemHome, 'noauth'); + if (!includeAuth) { + fs.mkdirSync(effectiveHome, { recursive: true }); + } + // Prepare environment variables const envVars = [ - `HOME="${gemHome}"`, + `HOME="${effectiveHome}"`, `GEM_HOME="${gemHome}"`, - includeAuth ? '' : 'RUBYGEMS_API_KEY=""', ].filter(Boolean).join(' '); // Build command with cd to correct directory and environment variables @@ -360,31 +365,33 @@ tap.test('RubyGems CLI: should unyank a version', async () => { const gemName = 'test-gem-cli'; const version = '1.0.0'; - const result = await runGemCommand( - `gem yank ${gemName} -v ${version} --undo --host ${registryUrl}/rubygems`, - testDir + // Use PUT /api/v1/gems/unyank via HTTP API (gem yank --undo removed in Ruby 4.0) + const response = await fetch( + `${registryUrl}/rubygems/api/v1/gems/unyank?gem_name=${gemName}&version=${version}`, + { + method: 'PUT', + headers: { + 'Authorization': rubygemsToken, + }, + } ); - console.log('gem unyank output:', result.stdout); - console.log('gem unyank stderr:', result.stderr); + console.log('gem unyank status:', response.status); - expect(result.exitCode).toEqual(0); + expect(response.status).toEqual(200); // Verify version is not yanked in /versions file - const response = await fetch(`${registryUrl}/rubygems/versions`); - const versionsData = await response.text(); + const versionsResponse = await fetch(`${registryUrl}/rubygems/versions`); + const versionsData = await versionsResponse.text(); console.log('Versions after unyank:', versionsData); - // Should not have '-' prefix anymore (or have both without prefix) - // Check that we have the version without yank marker + // Should not have '-' prefix anymore const lines = versionsData.trim().split('\n'); const gemLine = lines.find(line => line.startsWith(gemName)); if (gemLine) { - // Parse format: "gemname version[,version...] md5" const parts = gemLine.split(' '); const versions = parts[1]; - // Should have 1.0.0 without '-' prefix expect(versions).toContain('1.0.0'); expect(versions).not.toContain('-1.0.0'); } diff --git a/test/test.rubygems.ts b/test/test.rubygems.ts index 6233cd7..401973a 100644 --- a/test/test.rubygems.ts +++ b/test/test.rubygems.ts @@ -324,13 +324,9 @@ tap.test('RubyGems: should retrieve versions JSON (GET /rubygems/api/v1/versions expect(response.status).toEqual(200); expect(response.headers['Content-Type']).toEqual('application/json'); const json = await streamToJson(response.body); - expect(json).toBeTypeOf('object'); - - expect(json).toHaveProperty('name'); - expect(json.name).toEqual(testGemName); - expect(json).toHaveProperty('versions'); - expect(json.versions).toBeTypeOf('object'); - expect(json.versions.length).toBeGreaterThan(0); + expect(json).toBeInstanceOf(Array); + expect(json.length).toBeGreaterThan(0); + expect(json[0]).toHaveProperty('number'); }); tap.test('RubyGems: should retrieve dependencies JSON (GET /rubygems/api/v1/dependencies)', async () => { diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 356c744..79754d8 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@push.rocks/smartregistry', - version: '2.8.0', + version: '2.8.1', description: 'A composable TypeScript library implementing OCI, NPM, Maven, Cargo, Composer, PyPI, and RubyGems registries for building unified container and package registries' } diff --git a/ts/npm/classes.npmregistry.ts b/ts/npm/classes.npmregistry.ts index b53484f..06bdb27 100644 --- a/ts/npm/classes.npmregistry.ts +++ b/ts/npm/classes.npmregistry.ts @@ -749,6 +749,22 @@ export class NpmRegistry extends BaseRegistry { this.logger.log('error', 'handleSearch failed', { error: (error as Error).message }); } + // Sort results by relevance: exact match first, then prefix match, then substring match + if (text) { + const lowerText = text.toLowerCase(); + results.sort((a, b) => { + const aName = a.package.name.toLowerCase(); + const bName = b.package.name.toLowerCase(); + const aExact = aName === lowerText ? 0 : 1; + const bExact = bName === lowerText ? 0 : 1; + if (aExact !== bExact) return aExact - bExact; + const aPrefix = aName.startsWith(lowerText) ? 0 : 1; + const bPrefix = bName.startsWith(lowerText) ? 0 : 1; + if (aPrefix !== bPrefix) return aPrefix - bPrefix; + return aName.localeCompare(bName); + }); + } + // Apply pagination const paginatedResults = results.slice(from, from + size); diff --git a/ts/oci/classes.ociregistry.ts b/ts/oci/classes.ociregistry.ts index c52c35e..8eba511 100644 --- a/ts/oci/classes.ociregistry.ts +++ b/ts/oci/classes.ociregistry.ts @@ -126,7 +126,8 @@ export class OciRegistry extends BaseRegistry { }; // Route to appropriate handler - if (path === '/' || path === '') { + // OCI spec: GET /v2/ is the version check endpoint + if (path === '/' || path === '' || path === '/v2/' || path === '/v2') { return this.handleVersionCheck(); } diff --git a/ts/rubygems/helpers.rubygems.ts b/ts/rubygems/helpers.rubygems.ts index 2834a5d..cf46405 100644 --- a/ts/rubygems/helpers.rubygems.ts +++ b/ts/rubygems/helpers.rubygems.ts @@ -254,14 +254,12 @@ export function generateVersionsJson( uploadTime?: string; }> ): any { - return { - name: gemName, - versions: versions.map(v => ({ - number: v.version, - platform: v.platform || 'ruby', - built_at: v.uploadTime, - })), - }; + // RubyGems.org API returns a flat array at /api/v1/versions/{gem}.json + return versions.map(v => ({ + number: v.version, + platform: v.platform || 'ruby', + built_at: v.uploadTime, + })); } /**