diff --git a/src/services/versions/v1/README.md b/src/services/versions/v1/README.md index 75b2654..fb479cc 100644 --- a/src/services/versions/v1/README.md +++ b/src/services/versions/v1/README.md @@ -33,7 +33,8 @@ GET /versions/v1 "size_bytes": 15485760, "is_prerelease": false, "github_release_id": 987654321, - "changelog": "## 0.5.0\n- Major feature updates..." + "changelog": "## 0.5.0\n- Major feature updates...", + "digest": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" } }, "error_code": 0, @@ -140,3 +141,4 @@ When GitHub is unavailable and no cached data exists: - `details` includes the GitHub HTTP status and error code when available. - `GITHUB_TOKEN` is required for GitHub API access. - Releases before 0.5.0 read `src/composer.json`; newer releases use `composer.json`. +- `digest` is the SHA-256 digest of the release zip asset (`sha256:`), computed by GitHub; `null` if GitHub has not computed one for that asset. diff --git a/src/services/versions/v1/index.ts b/src/services/versions/v1/index.ts index 8b7818e..b7d6604 100644 --- a/src/services/versions/v1/index.ts +++ b/src/services/versions/v1/index.ts @@ -126,6 +126,7 @@ interface ReleaseAsset { name: string; browser_download_url: string; size: number; + digest: string | null; } function getReleaseZipAsset( @@ -483,7 +484,8 @@ export async function getReleases( size_bytes: zipAsset.size, is_prerelease: Boolean(release.prerelease), github_release_id: release.id ?? 0, - changelog: release.body || "" + changelog: release.body || "", + digest: zipAsset.digest ?? null }; return [tag, releaseDetails]; } @@ -583,6 +585,13 @@ function parseCachedReleases( try { const parsedCache = JSON.parse(cachedReleases); if (parsedCache && typeof parsedCache === "object") { + // Cache entries written before the `digest` field existed lack the + // key entirely; normalize them to the documented `null` fallback. + for (const release of Object.values(parsedCache as Releases)) { + if (release.digest === undefined) { + release.digest = null; + } + } return parsedCache as Releases; } } catch (parseError) { diff --git a/src/services/versions/v1/interfaces.ts b/src/services/versions/v1/interfaces.ts index a022c4b..e1e11a4 100644 --- a/src/services/versions/v1/interfaces.ts +++ b/src/services/versions/v1/interfaces.ts @@ -7,6 +7,9 @@ export type ReleaseDetails = { is_prerelease: boolean; github_release_id: number; changelog: string; + // SHA-256 digest of the release zip (`sha256:`), as computed by + // GitHub; null for older assets predating GitHub's digest support. + digest: string | null; }; export type Releases = { diff --git a/test/mocks/github-releases.ts b/test/mocks/github-releases.ts index 323d3cf..f50614a 100644 --- a/test/mocks/github-releases.ts +++ b/test/mocks/github-releases.ts @@ -59,7 +59,9 @@ export const mockGitHubReleases = [ name: "FOSSBilling.zip", browser_download_url: "https://github.com/FOSSBilling/FOSSBilling/releases/download/0.6.0/FOSSBilling.zip", - size: 1030000 + size: 1030000, + digest: + "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" } ] } diff --git a/test/services/versions/v1/errors.test.ts b/test/services/versions/v1/errors.test.ts index 706d175..f09278e 100644 --- a/test/services/versions/v1/errors.test.ts +++ b/test/services/versions/v1/errors.test.ts @@ -263,6 +263,8 @@ describe("Versions API v1 - Error Handling", () => { expect(data.error_code).toBe(0); expect(data.result["1.0.0"]).toBeDefined(); expect(data.stale).toBe(false); + // Cached before the `digest` field existed; should be normalized to null. + expect(data.result["1.0.0"]).toHaveProperty("digest", null); }); it("should handle GitHub API returning invalid JSON", async () => { diff --git a/test/services/versions/v1/index.test.ts b/test/services/versions/v1/index.test.ts index 6001fa2..9ef45b0 100644 --- a/test/services/versions/v1/index.test.ts +++ b/test/services/versions/v1/index.test.ts @@ -210,6 +210,10 @@ describe("Versions API v1", () => { expect(data.result).toHaveProperty("is_prerelease", false); expect(data.result).toHaveProperty("github_release_id"); expect(data.result).toHaveProperty("changelog"); + expect(data.result).toHaveProperty( + "digest", + "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" + ); }); }); @@ -227,6 +231,8 @@ describe("Versions API v1", () => { throw new Error("Expected version info for 0.5.0"); } expect(data.result).toHaveProperty("version", "0.5.0"); + // 0.5.0's mock asset predates GitHub's asset-digest feature. + expect(data.result).toHaveProperty("digest", null); }); it("should return 404 for non-existent version", async () => { diff --git a/test/utils/test-types.ts b/test/utils/test-types.ts index fd2848f..9149088 100644 --- a/test/utils/test-types.ts +++ b/test/utils/test-types.ts @@ -83,6 +83,7 @@ export interface VersionInfo { is_prerelease: boolean; github_release_id: number; changelog: string; + digest: string | null; } // GitHub Types