From 957a0e5408b2d3f96661fac8e40a32207a63e10b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20LUDWIG?= Date: Tue, 25 Aug 2026 09:39:39 +0800 Subject: [PATCH] feat: support renamed editorconfig-checker release assets while keeping legacy ec fallback --- src/index.ts | 5 +- src/program.spec.ts | 63 ++++++++++++++++++++++ src/program.ts | 24 +++++++++ src/release.spec.ts | 125 +++++++++++++++++++++++++++++++++++++++++++- src/release.ts | 45 +++++++++++----- 5 files changed, 245 insertions(+), 17 deletions(-) create mode 100644 src/program.spec.ts create mode 100644 src/program.ts diff --git a/src/index.ts b/src/index.ts index e057c19..cddbc1e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,9 @@ #!/usr/bin/env node import fs from "node:fs/promises" -import path from "node:path" import { spawnSync } from "node:child_process" import { COMBINED_PATH, VERSION } from "./constants" +import { findProgram } from "./program" import { downloadBinary, findRelease } from "./release" async function main() { @@ -24,8 +24,7 @@ async function main() { main().catch(console.error) async function execute() { - const [name] = await fs.readdir(path.join(COMBINED_PATH, "bin")) - const program = path.join(COMBINED_PATH, "bin", name) + const program = await findProgram(COMBINED_PATH, process.platform) await fs.chmod(program, 0o755) const { status } = spawnSync(program, process.argv.slice(2), { stdio: "inherit", diff --git a/src/program.spec.ts b/src/program.spec.ts new file mode 100644 index 0000000..c67ad28 --- /dev/null +++ b/src/program.spec.ts @@ -0,0 +1,63 @@ +import fs from "node:fs/promises" +import path from "node:path" +import { describe, it, beforeEach, afterEach } from "node:test" +import assert from "node:assert/strict" + +import tmp from "tmp-promise" +import { findProgram } from "./program" + +describe("findProgram", () => { + let directory: tmp.DirectoryResult + + beforeEach(async () => { + directory = await tmp.dir({ unsafeCleanup: true }) + }) + + afterEach(async () => { + await directory.cleanup() + }) + + it("should find the binary at the archive root", async () => { + // Arrange - Given + const input = { directory: directory.path, platform: "linux" } + const program = path.join(input.directory, "editorconfig-checker") + await fs.writeFile(program, "") + + // Act - When + const output = await findProgram(input.directory, input.platform) + + // Assert - Then + const expected = program + assert.equal(output, expected) + }) + + it("should find the .exe binary at the archive root on windows", async () => { + // Arrange - Given + const input = { directory: directory.path, platform: "win32" } + const program = path.join(input.directory, "editorconfig-checker.exe") + await fs.writeFile(program, "") + + // Act - When + const output = await findProgram(input.directory, input.platform) + + // Assert - Then + const expected = program + assert.equal(output, expected) + }) + + it("should fall back to the legacy bin/ directory", async () => { + // Arrange - Given + const input = { directory: directory.path, platform: "linux" } + const binPath = path.join(input.directory, "bin") + await fs.mkdir(binPath) + const program = path.join(binPath, "ec-linux-amd64") + await fs.writeFile(program, "") + + // Act - When + const output = await findProgram(input.directory, input.platform) + + // Assert - Then + const expected = program + assert.equal(output, expected) + }) +}) diff --git a/src/program.ts b/src/program.ts new file mode 100644 index 0000000..52722f0 --- /dev/null +++ b/src/program.ts @@ -0,0 +1,24 @@ +import fs from "node:fs/promises" +import path from "node:path" + +import { NAME } from "./constants" + +export async function findProgram(directory: string, platform: string) { + const executableName = platform === "win32" ? `${NAME}.exe` : NAME + const currentProgram = path.join(directory, executableName) + if (await exists(currentProgram)) { + return currentProgram + } + const legacyBinPath = path.join(directory, "bin") + const [legacyName] = await fs.readdir(legacyBinPath) + return path.join(legacyBinPath, legacyName) +} + +async function exists(filePath: string) { + try { + await fs.stat(filePath) + return true + } catch { + return false + } +} diff --git a/src/release.spec.ts b/src/release.spec.ts index 9c69864..dc6f9aa 100644 --- a/src/release.spec.ts +++ b/src/release.spec.ts @@ -14,7 +14,11 @@ import { describe, it, beforeEach, afterEach } from "node:test" import assert from "node:assert/strict" import { ProxyServer, createProxy } from "proxy" -import { proxiedFetch } from "./release" +import { + findFirstMatchingAsset, + getAssetPrefixes, + proxiedFetch, +} from "./release" const oldEnv = process.env @@ -84,3 +88,122 @@ describe("proxiedFetch", () => { assert.equal(proxyConnectionEstablished, true) }) }) + +describe("getAssetPrefixes", () => { + it("should prefer the current asset name and fall back to the legacy one", () => { + // Arrange - Given + const input = { platform: "linux", arch: "x64" } + + // Act - When + const output = getAssetPrefixes(input.platform, input.arch) + + // Assert - Then + const expected = ["editorconfig-checker-linux-amd64", "ec-linux-amd64"] + assert.deepEqual(output, expected) + }) + + it("should map win32 and x32 to the release naming", () => { + // Arrange - Given + const input = { platform: "win32", arch: "x32" } + + // Act - When + const output = getAssetPrefixes(input.platform, input.arch) + + // Assert - Then + const expected = ["editorconfig-checker-windows-386", "ec-windows-386"] + assert.deepEqual(output, expected) + }) + + it("should accept the universal darwin binary", () => { + // Arrange - Given + const input = { platform: "darwin", arch: "arm64" } + + // Act - When + const output = getAssetPrefixes(input.platform, input.arch) + + // Assert - Then + const expected = [ + "editorconfig-checker-darwin-arm64", + "editorconfig-checker-darwin-all", + "ec-darwin-arm64", + ] + assert.deepEqual(output, expected) + }) +}) + +describe("findFirstMatchingAsset", () => { + const legacyAssets = [ + { name: "checksums.txt" }, + { name: "ec-linux-amd64.tar.gz" }, + { name: "ec-linux-amd64.tar.gz.sbom.json" }, + ] + const currentAssets = [ + { name: "editorconfig-checker-darwin-all.tar.gz" }, + { name: "editorconfig-checker-linux-amd64.tar.gz" }, + { name: "editorconfig-checker-windows-amd64.tar.gz" }, + { name: "editorconfig-checker-windows-amd64.zip" }, + ] + + it("should pick the current asset when both are published", () => { + // Arrange - Given + const input = { + assets: [...legacyAssets, ...currentAssets], + assetPrefixes: getAssetPrefixes("linux", "x64"), + } + + // Act - When + const output = findFirstMatchingAsset(input.assets, input.assetPrefixes) + + // Assert - Then + const expected = { name: "editorconfig-checker-linux-amd64.tar.gz" } + assert.deepEqual(output, expected) + }) + + it("should pick the legacy asset for releases before the rename", () => { + // Arrange - Given + const input = { + assets: legacyAssets, + assetPrefixes: getAssetPrefixes("linux", "x64"), + } + + // Act - When + const output = findFirstMatchingAsset(input.assets, input.assetPrefixes) + + // Assert - Then + const expected = { name: "ec-linux-amd64.tar.gz" } + assert.deepEqual(output, expected) + }) + + it("should pick the universal binary on darwin", () => { + // Arrange - Given + const input = { + assets: currentAssets, + assetPrefixes: getAssetPrefixes("darwin", "arm64"), + } + + // Act - When + const output = findFirstMatchingAsset(input.assets, input.assetPrefixes) + + // Assert - Then + const expected = { name: "editorconfig-checker-darwin-all.tar.gz" } + assert.deepEqual(output, expected) + }) + + it("should ignore sbom and checksum files", () => { + // Arrange - Given + const input = { + assets: [ + { name: "checksums.txt" }, + { name: "ec-linux-amd64.tar.gz.sbom.json" }, + ], + assetPrefixes: getAssetPrefixes("linux", "x64"), + } + + // Act - When + const output = findFirstMatchingAsset(input.assets, input.assetPrefixes) + + // Assert - Then + const expected = undefined + assert.equal(output, expected) + }) +}) diff --git a/src/release.ts b/src/release.ts index d311f7a..489ad66 100644 --- a/src/release.ts +++ b/src/release.ts @@ -18,15 +18,13 @@ const octokit = new Octokit({ export async function findRelease(version: string) { const release = await getRelease(version) - const releasePrefix = getAssetPrefix() - const matchedAsset = release.data.assets.find(({ name }) => { - return ( - name.startsWith(releasePrefix) && - (name.endsWith(".tar.gz") || name.endsWith(".zip")) - ) - }) + const assetPrefixes = getAssetPrefixes(os.platform(), os.arch()) + const matchedAsset = findFirstMatchingAsset( + release.data.assets, + assetPrefixes, + ) if (!matchedAsset) { - throw new Error(`The binary '${releasePrefix}*' not found`) + throw new Error(`The binary '${assetPrefixes.join("*' or '")}*' not found`) } return [ release.data.name, @@ -54,7 +52,7 @@ export async function downloadBinary(assetId: number, assetFiletype: string) { const outputFile = createWriteStream(tmpfile.path) await pipeline(assetStream, outputFile) - if (assetFiletype === ".zip") { + if (assetFiletype === "zip") { const zip = new admzip(tmpfile.path) zip.extractAllTo(COMBINED_PATH, true) } else { @@ -87,16 +85,37 @@ function getRelease(version: string) { return getReleaseByTag({ owner: NAME, repo: NAME, tag: version }) } -function getAssetPrefix() { - let platform: string = os.platform() +export function findFirstMatchingAsset( + assets: ReadonlyArray, + assetPrefixes: string[], +) { + for (const assetPrefix of assetPrefixes) { + const matchedAsset = assets.find(({ name }) => { + return ( + name.startsWith(assetPrefix) && + (name.endsWith(".tar.gz") || name.endsWith(".zip")) + ) + }) + if (matchedAsset) { + return matchedAsset + } + } + return undefined +} + +export function getAssetPrefixes(platform: string, arch: string) { if (platform === "win32") { platform = "windows" } - let arch: string = os.arch() if (arch === "x32") { arch = "386" } else if (arch === "x64") { arch = "amd64" } - return `ec-${platform}-${arch}` + const currentAssetPrefixes = [`${NAME}-${platform}-${arch}`] + if (platform === "darwin") { + currentAssetPrefixes.push(`${NAME}-darwin-all`) + } + const legacyAssetPrefix = `ec-${platform}-${arch}` + return [...currentAssetPrefixes, legacyAssetPrefix] }