diff --git a/backend/src/api/public/v1/akrites-external/openapi.yaml b/backend/src/api/public/v1/akrites-external/openapi.yaml index 7a7aec93ee..347cc647b0 100644 --- a/backend/src/api/public/v1/akrites-external/openapi.yaml +++ b/backend/src/api/public/v1/akrites-external/openapi.yaml @@ -976,6 +976,10 @@ components: enum: [failed] reason: type: string + description: > + Why the job failed. Inferred-repo jobs whose result has left + Temporal retention answer "Job result expired, resubmit the job"; + a new submission for the same purl is then accepted. example: Repository not found on GitHub MemberContributorsJobUnresolvable: diff --git a/backend/src/api/public/v1/packages/memberContributorCollectionPaths.test.ts b/backend/src/api/public/v1/packages/memberContributorCollectionPaths.test.ts index 194599eb9a..179c7cd836 100644 --- a/backend/src/api/public/v1/packages/memberContributorCollectionPaths.test.ts +++ b/backend/src/api/public/v1/packages/memberContributorCollectionPaths.test.ts @@ -5,6 +5,7 @@ import { collectionPathForPurlType, isWellFormedPurl, purlType, + canonicalizePurlType, } from './memberContributorCollectionPaths' describe('purlType', () => { @@ -21,6 +22,16 @@ describe('purlType', () => { }) }) +describe('canonicalizePurlType', () => { + it.each([ + ['pkg:Hackage/aeson', 'pkg:hackage/aeson'], + ['pkg:hackage/Aeson', 'pkg:hackage/Aeson'], + ['pkg:npm/%40Babel/core', 'pkg:npm/%40Babel/core'], + ])('canonicalizes %s to %s', (purl, expected) => { + expect(canonicalizePurlType(purl)).toBe(expected) + }) +}) + describe('collectionPathForPurlType', () => { it('maps every collected purl type to the github path', () => { for (const type of ['npm', 'maven', 'pypi', 'cargo', 'golang', 'nuget', 'composer', 'gem']) { diff --git a/backend/src/api/public/v1/packages/memberContributorCollectionPaths.ts b/backend/src/api/public/v1/packages/memberContributorCollectionPaths.ts index 0ec826d758..13eae8a471 100644 --- a/backend/src/api/public/v1/packages/memberContributorCollectionPaths.ts +++ b/backend/src/api/public/v1/packages/memberContributorCollectionPaths.ts @@ -19,6 +19,17 @@ export function purlType(purl: string): string { return type.toLowerCase() } +export function canonicalizePurlType(purl: string): string { + const schemeLength = 'pkg:'.length + const slash = purl.indexOf('/', schemeLength) + const typeEnd = slash === -1 ? purl.length : slash + return ( + purl.slice(0, schemeLength) + + purl.slice(schemeLength, typeEnd).toLowerCase() + + purl.slice(typeEnd) + ) +} + const WELL_FORMED_PURL = /^pkg:[a-z][a-z0-9.+-]*\/[^/?#]+/i function isDecodable(value: string): boolean { diff --git a/backend/src/api/public/v1/packages/submitMemberContributorsJob.test.ts b/backend/src/api/public/v1/packages/submitMemberContributorsJob.test.ts index b893143258..4815cfe252 100644 --- a/backend/src/api/public/v1/packages/submitMemberContributorsJob.test.ts +++ b/backend/src/api/public/v1/packages/submitMemberContributorsJob.test.ts @@ -392,11 +392,15 @@ describe('submitMemberContributorsJob, unknown ecosystem', () => { it.each(['pkg:hackage/aeson', 'pkg:Hackage/aeson'])( 'creates an llm repo resolution job for %s without a package lookup and starts the inferred-repo workflow', - async (purl) => { - const { req, res, status, json } = mockReqRes({ purl }) + async (requestedPurl) => { + const purl = 'pkg:hackage/aeson' + const { req, res, status, json } = mockReqRes({ purl: requestedPurl }) await submitMemberContributorsJob(req, res) + expect(findActiveMemberContributorJobByPurl.mock.calls).toEqual([[{}, purl]]) + expect(findLatestMemberContributorJobByPurl.mock.calls).toEqual([[{}, purl]]) + const [, input] = createMemberContributorJob.mock.calls[0] expect(createMemberContributorJob.mock.calls).toEqual([ [ diff --git a/backend/src/api/public/v1/packages/submitMemberContributorsJob.ts b/backend/src/api/public/v1/packages/submitMemberContributorsJob.ts index d937b630c5..35d4d49591 100644 --- a/backend/src/api/public/v1/packages/submitMemberContributorsJob.ts +++ b/backend/src/api/public/v1/packages/submitMemberContributorsJob.ts @@ -26,6 +26,7 @@ import { import { LLM_REPO_RESOLUTION_PATH, + canonicalizePurlType, collectionPathForPurlType, isWellFormedPurl, purlType, @@ -179,13 +180,14 @@ async function respondWithActiveJob( async function submitInferredRepoJob( qx: QueryExecutor, - purl: string, + requestedPurl: string, ecosystem: string, res: Response, ): Promise { - if (!isWellFormedPurl(purl)) { + if (!isWellFormedPurl(requestedPurl)) { throw new BadRequestError('Malformed purl') } + const purl = canonicalizePurlType(requestedPurl) const active = await findActiveMemberContributorJobByPurl(qx, purl) if (active) { await respondWithActiveJob(qx, active.id, MEMBER_CONTRIBUTORS_INFERRED_REPO_WORKFLOW, res)