diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c27cd86..27c354b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -19,7 +19,7 @@ jobs: - run: npm run lint - run: npm run format -- --check - run: npm run coverage - + - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v4 with: diff --git a/README.md b/README.md index e38abe5..63db2fd 100644 --- a/README.md +++ b/README.md @@ -164,13 +164,18 @@ This error only occurs on tagged releases of Zig. ## /v1/zls/index.json -The response body imitates Zig's [index.json](https://ziglang.org/download/index.json) except that there is no field for `master`. Development builds of ZLS should be queried by supplying the Zig version that is being used. +> [!CAUTION] +> This request has been moved to [index.json](https://builds.zigtools.org/index.json) + +## index.json + +The [index.json](https://builds.zigtools.org/index.json) imitates Zig's [index.json](https://ziglang.org/download/index.json) except that there is no field for `master`. Development builds of ZLS should be queried by supplying the Zig version that is being used.
Show Example ```bash - curl "https://releases.zigtools.org/v1/zls/index.json" + curl "https://builds.zigtools.org/index.json" ``` ```json diff --git a/src/index.ts b/src/index.ts index 307395c..298ea87 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ import { Env } from "./env"; import { handlePublish } from "./publish"; -import { handleSelectVersion, handleZLSIndex } from "./select-zls-version"; +import { handleSelectVersion } from "./select-zls-version"; export default { async fetch(request, env, _ctx) { @@ -13,8 +13,7 @@ export default { response = await handleSelectVersion(request, env); break; case "/v1/zls/index.json": - response = await handleZLSIndex(request, env); - break; + return Response.redirect(`${env.R2_PUBLIC_URL}/index.json`, 301); case "/v1/zls/publish": response = await handlePublish(request, env); break; diff --git a/src/publish.ts b/src/publish.ts index 0ac864f..7471433 100644 --- a/src/publish.ts +++ b/src/publish.ts @@ -8,6 +8,8 @@ import { Extension, ReleaseArtifact, VersionCompatibility, + ZLSIndex, + artifactsToRecord, getMagicNumberOfExtension, } from "./shared"; import { SemanticVersion } from "./semantic-version"; @@ -476,16 +478,22 @@ export async function handlePublish( } } - let skipArtifactUpload; + const taggedReleases = await env.ZIGTOOLS_DB.prepare( + // update the "explain query plan when searching all tagged releases" test when modifying the query + "SELECT ZLSVersion, JsonData FROM ZLSReleases WHERE IsRelease = 1 ORDER BY ZLSVersionMajor DESC, ZLSVersionMinor DESC, ZLSVersionPatch DESC", + ).all<{ ZLSVersion: string; JsonData: string }>(); + let artifactsAlreadyExist: boolean; if (zlsVersion.isRelease) { - const result = await env.ZIGTOOLS_DB.prepare( - "SELECT ZLSVersion,JsonData FROM ZLSReleases WHERE ZLSVersion = ?1", - ) - .bind(zlsVersionString) - .first<{ ZLSVersion: string }>(); - - skipArtifactUpload = result !== null; + artifactsAlreadyExist = taggedReleases.results + .map(({ ZLSVersion }) => ZLSVersion) + .includes(zlsVersionString); + if (!artifactsAlreadyExist) { + taggedReleases.results.push({ + ZLSVersion: zlsVersionString, + JsonData: JSON.stringify(newEntryValue), + }); + } } else { assert(zlsVersion.commitHeight !== undefined); const result = await env.ZIGTOOLS_DB.prepare( @@ -499,7 +507,7 @@ export async function handlePublish( ) .first<{ ZLSVersion: string }>(); - skipArtifactUpload = result !== null; + artifactsAlreadyExist = result !== null; if (result !== null && zlsVersionString !== result.ZLSVersion) { return new Response( @@ -535,9 +543,28 @@ export async function handlePublish( ), ]); - if (skipArtifactUpload) return new Response(); + if (artifactsAlreadyExist) return new Response(); + + const promises: Promise[] = []; - const promises: Promise[] = []; + { + const index: ZLSIndex = {}; + + for (const entry of taggedReleases.results) { + const jsonData = JSON.parse(entry.JsonData) as D2JsonData; + index[jsonData.zlsVersion] = { + date: new Date(jsonData.date).toISOString().slice(0, 10), + ...artifactsToRecord(env.R2_PUBLIC_URL, jsonData.artifacts), + }; + } + + promises.push( + env.ZIGTOOLS_BUILDS.put( + "index.json", + JSON.stringify(index, undefined, 2), + ), + ); + } for (let i = 0; i < artifacts.length; i++) { const artifact = artifacts[i]; diff --git a/src/select-zls-version.ts b/src/select-zls-version.ts index 4feef38..8391818 100644 --- a/src/select-zls-version.ts +++ b/src/select-zls-version.ts @@ -1,7 +1,12 @@ import assert from "node:assert"; import { Env } from "./env"; import { Order, SemanticVersion } from "./semantic-version"; -import { D2JsonData, ReleaseArtifact, VersionCompatibility } from "./shared"; +import { + ArtifactEntry, + D2JsonData, + VersionCompatibility, + artifactsToRecord, +} from "./shared"; /** * Similar to https://ziglang.org/download/index.json @@ -81,52 +86,6 @@ function selectZLSVersionFailureCodeToString( } } -/** - * Similar to https://ziglang.org/download/index.json - */ -export type ZLSIndexResponse = Record< - string, - { - /** `YYYY-MM-DD` */ - date: string; - [artifact: string]: ArtifactEntry | string | undefined; - } ->; - -export interface ArtifactEntry { - /** A download URL */ - tarball: string; - /** A SHA256 hash of the tarball */ - shasum: string; - /** Size of the tarball in bytes */ - size: string; -} - -function artifactsToRecord( - env: Env, - artifacts: ReleaseArtifact[], -): Record { - assert(artifacts.length !== 0); - const targets: Record = {}; - for (const artifact of artifacts) { - switch (artifact.extension) { - case "tar.gz": - continue; - case "tar.xz": - case "zip": - break; - } - assert(!(`${artifact.arch}-${artifact.os}` in targets)); - assert.strictEqual(artifact.fileShasum.length, 64); - targets[`${artifact.arch}-${artifact.os}`] = { - tarball: `${env.R2_PUBLIC_URL}/zls-${artifact.os}-${artifact.arch}-${artifact.version}.${artifact.extension}`, - shasum: artifact.fileShasum, - size: artifact.fileSize.toString(), - }; - } - return targets; -} - function failure(status: number, message: string): Response { return Response.json( { @@ -138,41 +97,6 @@ function failure(status: number, message: string): Response { ); } -/** `${ENDPOINT}/zls/index.json` */ -export async function handleZLSIndex( - request: Request, - env: Env, -): Promise { - if (request.method !== "GET") { - return failure(405, "method must be 'GET'"); // Method Not Allowed - } - - if (typeof env.R2_PUBLIC_URL !== "string" || !env.R2_PUBLIC_URL) { - return failure(500, "Internal Server Error"); // Internal Server Error - } - - const releases = await env.ZIGTOOLS_DB.prepare( - // update the "explain query plan when searching all tagged releases" test when modifying the query - "SELECT JsonData FROM ZLSReleases WHERE IsRelease = 1 ORDER BY ZLSVersionMajor DESC, ZLSVersionMinor DESC, ZLSVersionPatch DESC", - ).all<{ JsonData: string }>(); - - const response: ZLSIndexResponse = {}; - - for (const entry of releases.results) { - const jsonData = JSON.parse(entry.JsonData) as D2JsonData; - response[jsonData.zlsVersion] = { - date: new Date(jsonData.date).toISOString().slice(0, 10), - ...artifactsToRecord(env, jsonData.artifacts), - }; - } - - return Response.json(response, { - headers: { - "cache-control": "public, max-age=3600", // 1 hour - }, - }); -} - /** `${ENDPOINT}/zls/select-version?zig_version=0.12.0&compatibility=full` */ export async function handleSelectVersion( request: Request, @@ -238,7 +162,7 @@ export async function handleSelectVersion( response = { version: selectedVersion.zlsVersion, date: new Date(selectedVersion.date).toISOString().slice(0, 10), - ...artifactsToRecord(env, selectedVersion.artifacts), + ...artifactsToRecord(env.R2_PUBLIC_URL, selectedVersion.artifacts), }; } @@ -366,9 +290,11 @@ async function selectOnDevelopmentBuild( const developmentReleases = await env.ZIGTOOLS_DB.prepare( // update the "explain query plan when searching on development built" test when modifying the query "SELECT JsonData FROM ZLSReleases WHERE IsRelease = 0 AND ZLSVersionMajor = ?1 AND ZLSVersionMinor = ?2 ORDER BY ZLSVersionBuildID ASC", - ).bind(zigVersion.major, zigVersion.minor).all<{ JsonData: string; }>(); + ) + .bind(zigVersion.major, zigVersion.minor) + .all<{ JsonData: string }>(); - let releases: { JsonData: string; }[] = []; + let releases: { JsonData: string }[] = []; if (developmentReleases.results.length !== 0) { releases = developmentReleases.results; } else { @@ -378,16 +304,16 @@ async function selectOnDevelopmentBuild( // 3. ZLS has tagged a new release (e.g `0.13.0`) // 4. but no ZLS development builds have come out! // - // Querying with `0.13.0-dev.1+aaaaaaa` should return `0.13.0`. This - // should only happen for the latest tagged release while previous + // Querying with `0.13.0-dev.1+aaaaaaa` should return `0.13.0`. This + // should only happen for the latest tagged release while previous // releases will report 'unsupported'. // // This is why only the latest tagged release is selected. const latestTaggedRelease = await env.ZIGTOOLS_DB.prepare( // update the "explain query plan when searching all tagged releases" test when modifying the query "SELECT JsonData FROM ZLSReleases WHERE IsRelease = 1 ORDER BY ZLSVersionMajor DESC, ZLSVersionMinor DESC, ZLSVersionPatch DESC", - ).first<{ JsonData: string; }>(); - releases = (latestTaggedRelease != null) ? [latestTaggedRelease] : []; + ).first<{ JsonData: string }>(); + releases = latestTaggedRelease != null ? [latestTaggedRelease] : []; } if (releases.length == 0) { diff --git a/src/shared.ts b/src/shared.ts index f68a488..d40f1ee 100644 --- a/src/shared.ts +++ b/src/shared.ts @@ -1,4 +1,5 @@ import { Buffer } from "node:buffer"; +import assert from "node:assert"; export const xzMagicNumber = Buffer.from("FD377A585A00", "hex"); export const gzipMagicNumber = Buffer.from("1F8B", "hex"); @@ -40,6 +41,27 @@ export interface ReleaseArtifact { export type Extension = "tar.xz" | "tar.gz" | "zip"; +/** + * Similar to https://ziglang.org/download/index.json + */ +export type ZLSIndex = Record< + string, + { + /** `YYYY-MM-DD` */ + date: string; + [artifact: string]: ArtifactEntry | string | undefined; + } +>; + +export interface ArtifactEntry { + /** A download URL */ + tarball: string; + /** A SHA256 hash of the tarball */ + shasum: string; + /** Size of the tarball in bytes */ + size: string; +} + export interface SQLiteQueryPlanRow { id: number; parent: number; @@ -57,3 +79,28 @@ export function getMagicNumberOfExtension(extension: Extension): Buffer { return zipMagicNumber; } } + +export function artifactsToRecord( + R2_PUBLIC_URL: string, + artifacts: ReleaseArtifact[], +): Record { + assert(artifacts.length !== 0); + const targets: Record = {}; + for (const artifact of artifacts) { + switch (artifact.extension) { + case "tar.gz": + continue; + case "tar.xz": + case "zip": + break; + } + assert(!(`${artifact.arch}-${artifact.os}` in targets)); + assert.strictEqual(artifact.fileShasum.length, 64); + targets[`${artifact.arch}-${artifact.os}`] = { + tarball: `${R2_PUBLIC_URL}/zls-${artifact.os}-${artifact.arch}-${artifact.version}.${artifact.extension}`, + shasum: artifact.fileShasum, + size: artifact.fileSize.toString(), + }; + } + return targets; +} diff --git a/test/publish.test.ts b/test/publish.test.ts index 4fef2d2..8008f64 100644 --- a/test/publish.test.ts +++ b/test/publish.test.ts @@ -11,6 +11,7 @@ import { VersionCompatibility, xzMagicNumber, zipMagicNumber, + ZLSIndex, } from "../src/shared"; import { handlePublish } from "../src/publish"; @@ -825,6 +826,9 @@ describe("/v1/zls/publish", () => { const objects = await env.ZIGTOOLS_BUILDS.list({}); expect(objects.objects).toMatchObject([ + { + key: "index.json", + }, { key: "zls-linux-x86_64-0.1.0.tar.gz", size: gzipMagicNumber.length + 7, @@ -839,12 +843,12 @@ describe("/v1/zls/publish", () => { }, ]); - assert(objects.objects[0].checksums.sha256 !== undefined); assert(objects.objects[1].checksums.sha256 !== undefined); assert(objects.objects[2].checksums.sha256 !== undefined); + assert(objects.objects[3].checksums.sha256 !== undefined); expect( - Buffer.from(objects.objects[0].checksums.sha256).toString("hex"), + Buffer.from(objects.objects[1].checksums.sha256).toString("hex"), ).toBe( createHash("sha256") .update(gzipMagicNumber) @@ -852,7 +856,7 @@ describe("/v1/zls/publish", () => { .digest("hex"), ); expect( - Buffer.from(objects.objects[1].checksums.sha256).toString("hex"), + Buffer.from(objects.objects[2].checksums.sha256).toString("hex"), ).toBe( createHash("sha256") .update(xzMagicNumber) @@ -860,7 +864,7 @@ describe("/v1/zls/publish", () => { .digest("hex"), ); expect( - Buffer.from(objects.objects[2].checksums.sha256).toString("hex"), + Buffer.from(objects.objects[3].checksums.sha256).toString("hex"), ).toBe( createHash("sha256") .update(zipMagicNumber) @@ -923,6 +927,9 @@ describe("/v1/zls/publish", () => { const objects = await env.ZIGTOOLS_BUILDS.list({}); expect(objects.objects).toMatchObject([ + { + key: "index.json", + }, { key: "zls-linux-x86_64-0.1.0.tar.gz", size: gzipMagicNumber.length + 7, @@ -1230,7 +1237,7 @@ describe("/v1/zls/publish", () => { }); test("publish new successfull build with different Zig versions", async () => { - const date = Date.now(); + const date = 1729123200000; vi.setSystemTime(date); { @@ -1316,6 +1323,9 @@ describe("/v1/zls/publish", () => { const objects = await env.ZIGTOOLS_BUILDS.list({}); expect(objects.objects).toMatchObject([ + { + key: "index.json", + }, { key: "zls-linux-x86_64-0.11.0.tar.gz", size: gzipMagicNumber.length + 7, @@ -1326,11 +1336,11 @@ describe("/v1/zls/publish", () => { }, ]); - assert(objects.objects[0].checksums.sha256 !== undefined); assert(objects.objects[1].checksums.sha256 !== undefined); + assert(objects.objects[2].checksums.sha256 !== undefined); expect( - Buffer.from(objects.objects[0].checksums.sha256).toString("hex"), + Buffer.from(objects.objects[1].checksums.sha256).toString("hex"), ).toBe( createHash("sha256") .update(gzipMagicNumber) @@ -1338,12 +1348,31 @@ describe("/v1/zls/publish", () => { .digest("hex"), ); expect( - Buffer.from(objects.objects[1].checksums.sha256).toString("hex"), + Buffer.from(objects.objects[2].checksums.sha256).toString("hex"), ).toBe( createHash("sha256") .update(xzMagicNumber) .update("binary1") .digest("hex"), ); + + const response = await env.ZIGTOOLS_BUILDS.get("index.json"); + assert(response !== null); + + const zlsIndex = await response.json(); + + expect(zlsIndex).toStrictEqual({ + "0.11.0": { + date: "2024-10-17", + "x86_64-linux": { + shasum: createHash("sha256") + .update(xzMagicNumber) + .update("binary1") + .digest("hex"), + size: (xzMagicNumber.length + 7).toString(), + tarball: `${env.R2_PUBLIC_URL}/zls-linux-x86_64-0.11.0.tar.xz`, + }, + }, + }); }); }); diff --git a/test/select-zls-version.test.ts b/test/select-zls-version.test.ts index dc5ce45..5f30fa2 100644 --- a/test/select-zls-version.test.ts +++ b/test/select-zls-version.test.ts @@ -9,11 +9,9 @@ import { } from "../src/shared"; import { handleSelectVersion, - ZLSIndexResponse, SelectVersionResponse, SelectVersionFailureResponse, SelectVersionFailureCode, - handleZLSIndex, } from "../src/select-zls-version"; import { SemanticVersion } from "../src/semantic-version"; @@ -198,131 +196,14 @@ function shuffleArray(array: unknown[]) { } describe("/v1/zls/index.json", () => { - test("method should be 'GET'", async () => { + test("check for redirect", async () => { const response = await SELF.fetch("https://example.com/v1/zls/index.json", { - method: "POST", - }); - expect(await response.json()).toStrictEqual({ - error: "method must be 'GET'", - }); - expect(response.status).toBe(405); - }); - - test.each([null, "", {}, []])( - "check for invalid R2_PUBLIC_URL: %j", - async (value) => { - const response = await handleZLSIndex( - new Request("https://example.com/v1/zls/index.json"), - { - ...env, - R2_PUBLIC_URL: value as string, - }, - ); - expect(response.status).toBe(500); - }, - ); - - test("search on empty database", async () => { - const response = await SELF.fetch("https://example.com/v1/zls/index.json"); - expect(await response.json()).toStrictEqual({}); - expect(response.status).toBe(200); - }); - - describe("test on sample database", () => { - beforeEach(populateDatabase); - - test("search without version", async () => { - const response = await SELF.fetch( - "https://example.com/v1/zls/index.json", - ); - const body = await response.json(); - - expect(Object.keys(body)).toStrictEqual([ - "0.13.0", - "0.12.1", - "0.12.0", - "0.11.0", - ]); - expect(body).toStrictEqual({ - "0.11.0": { - date: "1970-01-01", - "x86_64-linux": { - tarball: `${env.R2_PUBLIC_URL}/zls-linux-x86_64-0.11.0.tar.xz`, - shasum: - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - size: "12", - }, - "aarch64-windows": { - tarball: `${env.R2_PUBLIC_URL}/zls-windows-aarch64-0.11.0.zip`, - shasum: - "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - size: "12", - }, - }, - "0.12.0": { - date: "1970-01-01", - "x86_64-linux": { - tarball: `${env.R2_PUBLIC_URL}/zls-linux-x86_64-0.11.0.tar.xz`, - shasum: - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - size: "12", - }, - "aarch64-windows": { - tarball: `${env.R2_PUBLIC_URL}/zls-windows-aarch64-0.11.0.zip`, - shasum: - "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - size: "12", - }, - }, - "0.12.1": { - date: "1970-01-01", - "x86_64-linux": { - tarball: `${env.R2_PUBLIC_URL}/zls-linux-x86_64-0.11.0.tar.xz`, - shasum: - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - size: "12", - }, - "aarch64-windows": { - tarball: `${env.R2_PUBLIC_URL}/zls-windows-aarch64-0.11.0.zip`, - shasum: - "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - size: "12", - }, - }, - "0.13.0": { - date: "1970-01-01", - "x86_64-linux": { - tarball: `${env.R2_PUBLIC_URL}/zls-linux-x86_64-0.11.0.tar.xz`, - shasum: - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - size: "12", - }, - "aarch64-windows": { - tarball: `${env.R2_PUBLIC_URL}/zls-windows-aarch64-0.11.0.zip`, - shasum: - "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - size: "12", - }, - }, - }); - expect(response.status).toBe(200); + redirect: "manual", }); - }); - - test("explain query plan when searching all tagged releases", async () => { - const response = await env.ZIGTOOLS_DB.prepare( - "EXPLAIN QUERY PLAN SELECT JsonData FROM ZLSReleases WHERE IsRelease = 1 ORDER BY ZLSVersionMajor DESC, ZLSVersionMinor DESC, ZLSVersionPatch DESC", - ).all(); - - // TODO test `response.meta.rows_read` on an example database - - expect(response.results).toMatchObject([ - { - notused: 0, - detail: - "SEARCH ZLSReleases USING INDEX idx_zls_releases_is_release_major_minor_patch (IsRelease=?)", - }, - ]); + expect(response.status).toBe(301); + expect(response.headers.get("location")).toBe( + `${env.R2_PUBLIC_URL}/index.json`, + ); }); }); @@ -573,6 +454,22 @@ describe("/v1/zls/select-version", () => { ); }); + test("explain query plan when searching all tagged releases", async () => { + const response = await env.ZIGTOOLS_DB.prepare( + "EXPLAIN QUERY PLAN SELECT ZLSVersion, JsonData FROM ZLSReleases WHERE IsRelease = 1 ORDER BY ZLSVersionMajor DESC, ZLSVersionMinor DESC, ZLSVersionPatch DESC", + ).all(); + + // TODO test `response.meta.rows_read` on an example database + + expect(response.results).toMatchObject([ + { + notused: 0, + detail: + "SEARCH ZLSReleases USING INDEX idx_zls_releases_is_release_major_minor_patch (IsRelease=?)", + }, + ]); + }); + test("explain query plan when searching on tagged release", async () => { const response = await env.ZIGTOOLS_DB.prepare( "EXPLAIN QUERY PLAN SELECT JsonData FROM ZLSReleases WHERE IsRelease = 1 AND ZLSVersionMajor = ?1 AND ZLSVersionMinor = ?2",