diff --git a/src/__tests__/fixtures/mock-deps.ts b/src/__tests__/fixtures/mock-deps.ts index 4fd4c836..d7ce9cc0 100644 --- a/src/__tests__/fixtures/mock-deps.ts +++ b/src/__tests__/fixtures/mock-deps.ts @@ -41,6 +41,15 @@ export function createMockOctokit( parent: null, }, }), + getLatestRelease: + (overrides.rest?.repos + ?.getLatestRelease as OctokitClient["rest"]["repos"]["getLatestRelease"]) ?? + jest.fn().mockResolvedValue({ + data: { + tag_name: "v1.2.1", + name: "v1.2.1", + }, + }), listLanguages: (overrides.rest?.repos ?.listLanguages as OctokitClient["rest"]["repos"]["listLanguages"]) ?? diff --git a/src/__tests__/unit/helper.test.ts b/src/__tests__/unit/helper.test.ts index 74d05ae6..3ceed02d 100644 --- a/src/__tests__/unit/helper.test.ts +++ b/src/__tests__/unit/helper.test.ts @@ -36,6 +36,7 @@ describe("createHelpers - calculateMetaData", () => { const result = await helpers.calculateMetaData(); expect(result.name).toBe("test-repo"); + expect(result.version).toBe("1.2.1"); expect(result.description).toBe("A test repository"); expect(result.repositoryURL).toBe( "https://github.com/test-owner/test-repo", @@ -47,6 +48,27 @@ describe("createHelpers - calculateMetaData", () => { expect(result.tags).toEqual(["test", "automation"]); }); + it("returns an empty version when the release lookup fails", async () => { + const mockOctokit = createMockOctokit({ + rest: { + repos: { + getLatestRelease: jest + .fn() + .mockRejectedValue(new Error("rate limited")), + }, + }, + }); + + deps = createMockDeps({ octokit: mockOctokit }); + const helpers = createHelpers(deps); + const result = await helpers.calculateMetaData(); + + expect(result.version).toBe(""); + expect(deps.log.warning).toHaveBeenCalledWith( + expect.stringContaining("Failed to fetch latest release version"), + ); + }); + it("reports private visibility for private repos", async () => { const mockOctokit = createMockOctokit({ rest: { diff --git a/src/__tests__/unit/main.test.ts b/src/__tests__/unit/main.test.ts index 475666be..324baac2 100644 --- a/src/__tests__/unit/main.test.ts +++ b/src/__tests__/unit/main.test.ts @@ -54,6 +54,29 @@ describe("getMetaData", () => { expect(result.feedbackMechanism).toContain("/issues"); }); + it("preserves an existing version when the latest release is unavailable", async () => { + const releaseOctokit = createMockOctokit({ + rest: { + repos: { + getLatestRelease: jest + .fn() + .mockRejectedValue(new Error("not found")), + }, + }, + }); + + const deps = createMockDeps({ octokit: releaseOctokit }); + const helpers = createHelpers(deps); + + const existing = { + ...validCodeJSON, + version: "7.8.9", + } as any; + const result = await getMetaData(helpers, deps, existing); + + expect(result.version).toBe("7.8.9"); + }); + it("sets Archival status when isArchived", async () => { const deps = createMockDeps({ isArchived: true }); const helpers = createHelpers(deps); @@ -109,6 +132,29 @@ describe("getMetaData", () => { URL: "https://github.com/upstream-owner/upstream-repo", }); }); + + it("uses the latest release version when available", async () => { + const releaseOctokit = createMockOctokit({ + rest: { + repos: { + getLatestRelease: jest.fn().mockResolvedValue({ + data: { + tag_name: "v2.4.6", + name: "Release 2.4.6", + }, + }), + }, + }, + }); + + const deps = createMockDeps({ octokit: releaseOctokit }); + const helpers = createHelpers(deps); + + const result = await getMetaData(helpers, deps, null); + + expect(result.version).toBe("2.4.6"); + }); + }); describe("runWithDeps", () => { @@ -161,6 +207,12 @@ describe("runWithDeps", () => { get: jest .fn() .mockResolvedValue({ data: { default_branch: "main" } }), + getLatestRelease: jest.fn().mockResolvedValue({ + data: { + tag_name: "v1.2.1", + name: "v1.2.1", + }, + }), listLanguages: jest.fn().mockResolvedValue({ data: {} }), getContent: jest .fn() diff --git a/src/helper.ts b/src/helper.ts index 57be7b12..4d81143d 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -15,13 +15,15 @@ export function createHelpers(deps: Dependencies) { //=============================================== async function calculateMetaData(): Promise> { try { - const [laborHours, basicInfo] = await Promise.all([ + const [laborHours, basicInfo, version] = await Promise.all([ getLaborHours(), getBasicInfo(), + getVersion(), ]); return { name: basicInfo.title, + version: version, description: basicInfo.description, repositoryURL: basicInfo.url, repositoryVisibility: basicInfo.repositoryVisibility, @@ -44,6 +46,42 @@ export function createHelpers(deps: Dependencies) { } } + async function getVersion(): Promise { + try { + const release = await octokit.rest.repos.getLatestRelease({ owner, repo }); + const versionFromRelease = normalizeVersionString(release.data.tag_name); + + if (versionFromRelease !== "") { + return versionFromRelease; + } + + const releaseName = release.data.name; + if (typeof releaseName === "string") { + const versionFromName = normalizeVersionString(releaseName); + + if (versionFromName !== "") { + return versionFromName; + } + } + + log.warning("Latest release did not include a usable version string."); + } catch (error) { + log.warning(`Failed to fetch latest release version: ${error}`); + } + + return ""; + } + + function normalizeVersionString(value: string): string { + const trimmedValue = value.trim(); + + if (trimmedValue.length > 1 && /^v\d/i.test(trimmedValue)) { + return trimmedValue.replace(/^v/i, ""); + } + + return trimmedValue; + } + async function getBasicInfo(): Promise { try { const [repoData, languagesData] = await Promise.all([ diff --git a/src/main.ts b/src/main.ts index 496e5729..d36115cc 100644 --- a/src/main.ts +++ b/src/main.ts @@ -94,6 +94,7 @@ async function getMetaData( existingCodeJSON?: CodeJSON | null, ): Promise> { const partialCodeJSON = await helpers.calculateMetaData(); + const version = existingCodeJSON?.version || partialCodeJSON.version; // preserve existing feedback mechanisms if they exist, otherwise default to GitHub Issues const feedbackMechanism = @@ -150,6 +151,7 @@ async function getMetaData( return { name: partialCodeJSON.name, + version: version, description: description, status: status, repositoryURL: partialCodeJSON.repositoryURL, diff --git a/src/types/Dependencies.ts b/src/types/Dependencies.ts index c2c28d18..d6db88da 100644 --- a/src/types/Dependencies.ts +++ b/src/types/Dependencies.ts @@ -32,6 +32,10 @@ export interface OctokitClient { rest: { repos: { get: (params: { owner: string; repo: string }) => Promise; + getLatestRelease: (params: { + owner: string; + repo: string; + }) => Promise; listLanguages: (params: { owner: string; repo: string; @@ -91,6 +95,13 @@ export interface LanguagesResponse { data: Record; } +export interface ReleaseResponse { + data: { + tag_name: string; + name: string | null; + }; +} + export interface ContentResponse { data: { sha?: string } | Array; }