From 84c3324d0edaddac5cfe20e22aa3e0e993bfc1cc Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Tue, 4 Feb 2025 22:02:42 -0500 Subject: [PATCH 1/9] comment up goreleaser flow --- cli/combined-shim.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/cli/combined-shim.yml b/cli/combined-shim.yml index 67d9914a3269c..29bff222b46f1 100644 --- a/cli/combined-shim.yml +++ b/cli/combined-shim.yml @@ -2,6 +2,9 @@ project_name: turbo dist: dist +# We just call `npm-native-packages.js` once for each item in goos x goarch +# the scrip generates a scaffold version of each npm package +# goreleaser just copies binary from dist-{os}-{arch}/turbo(.exe) to bin/turbo builds: - id: turbo builder: prebuilt @@ -23,11 +26,15 @@ builds: pre: - cmd: ./scripts/npm-native-packages/npm-native-packages.js {{ .Os }} {{ .Arch }} {{ .Version }} binary: bin/turbo +# goreleaser creates a checksums.txt +# don't think this is in use anywhere? could be in GH release, but not going anywhere checksum: name_template: "checksums.txt" +# feature we do not use snapshot: name_template: "{{ incpatch .Version }}" archives: + # we do nothing with this archive - id: github name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}" wrap_in_directory: true @@ -47,6 +54,7 @@ archives: amd64: 64 format: tar.gz files: + # copy only these files into - LICENSE - src: "scripts/npm-native-packages/build/{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}/package.json" dst: "workaround/.." @@ -57,12 +65,14 @@ archives: - src: "scripts/npm-native-packages/build/{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}/bin/*" dst: "bin/" strip_parent: true +# used for GH releases only (which are disabled) changelog: sort: asc filters: exclude: - "^docs:" - "^test:" +# we do nothing with the release section since disabled release: github: owner: vercel @@ -75,4 +85,5 @@ publishers: - name: npm ids: - npm + # all this does is run this command for each archive we made above cmd: "npm publish{{ if .Prerelease }} --tag canary{{ end }} {{ abs .ArtifactPath }}" From 125c5a266ce0002a128bbf950b8726a4f2658662 Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Wed, 5 Feb 2025 15:23:00 -0500 Subject: [PATCH 2/9] feat(releaser): add @turbo/releaser --- packages/turbo-releaser/.eslintrc.js | 26 ++++ packages/turbo-releaser/cli/index.cjs | 20 +++ packages/turbo-releaser/package.json | 32 +++++ packages/turbo-releaser/src/index.ts | 45 +++++++ packages/turbo-releaser/src/native.test.ts | 126 ++++++++++++++++++ packages/turbo-releaser/src/native.ts | 65 +++++++++ .../turbo-releaser/src/operations.test.ts | 70 ++++++++++ packages/turbo-releaser/src/operations.ts | 54 ++++++++ packages/turbo-releaser/src/packager.test.ts | 55 ++++++++ packages/turbo-releaser/src/packager.ts | 35 +++++ packages/turbo-releaser/src/types.ts | 8 ++ packages/turbo-releaser/src/version.test.ts | 29 ++++ packages/turbo-releaser/src/version.ts | 11 ++ packages/turbo-releaser/template/README.md | 3 + packages/turbo-releaser/template/bin/turbo | 15 +++ packages/turbo-releaser/tsconfig.json | 8 ++ packages/turbo-releaser/tsup.config.ts | 9 ++ packages/turbo-releaser/turbo.json | 9 ++ pnpm-lock.yaml | 41 +++++- 19 files changed, 654 insertions(+), 7 deletions(-) create mode 100644 packages/turbo-releaser/.eslintrc.js create mode 100755 packages/turbo-releaser/cli/index.cjs create mode 100644 packages/turbo-releaser/package.json create mode 100644 packages/turbo-releaser/src/index.ts create mode 100644 packages/turbo-releaser/src/native.test.ts create mode 100644 packages/turbo-releaser/src/native.ts create mode 100644 packages/turbo-releaser/src/operations.test.ts create mode 100644 packages/turbo-releaser/src/operations.ts create mode 100644 packages/turbo-releaser/src/packager.test.ts create mode 100644 packages/turbo-releaser/src/packager.ts create mode 100644 packages/turbo-releaser/src/types.ts create mode 100644 packages/turbo-releaser/src/version.test.ts create mode 100644 packages/turbo-releaser/src/version.ts create mode 100644 packages/turbo-releaser/template/README.md create mode 100644 packages/turbo-releaser/template/bin/turbo create mode 100644 packages/turbo-releaser/tsconfig.json create mode 100644 packages/turbo-releaser/tsup.config.ts create mode 100644 packages/turbo-releaser/turbo.json diff --git a/packages/turbo-releaser/.eslintrc.js b/packages/turbo-releaser/.eslintrc.js new file mode 100644 index 0000000000000..1ea2c1f97c3c6 --- /dev/null +++ b/packages/turbo-releaser/.eslintrc.js @@ -0,0 +1,26 @@ +module.exports = { + extends: ["@turbo/eslint-config/library"], + overrides: [ + { + files: ["src/*.ts", "cli/index.cjs"], + rules: { + "no-console": "off", + }, + }, + { + files: ["src/native.ts", "src/operations.ts"], + rules: { + "import/no-default-export": "off", + }, + }, + { + files: ["src/*.test.ts"], + rules: { + // https://github.com/nodejs/node/issues/51292 + "@typescript-eslint/no-floating-promises": "off", + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/no-unsafe-argument": "off", + }, + }, + ], +}; diff --git a/packages/turbo-releaser/cli/index.cjs b/packages/turbo-releaser/cli/index.cjs new file mode 100755 index 0000000000000..d635b1dc6e50c --- /dev/null +++ b/packages/turbo-releaser/cli/index.cjs @@ -0,0 +1,20 @@ +#!/usr/bin/env node + +const { spawnSync } = require("node:child_process"); +const path = require("node:path"); + +const PATH_TO_DIST = path.resolve(__dirname, "../dist"); + +// Define the path to the CLI file +const cliPath = path.resolve(__dirname, PATH_TO_DIST, "index.js"); + +try { + const result = spawnSync("node", [cliPath, ...process.argv.slice(2)], { + stdio: "inherit", + }); + + process.exit(result.status); +} catch (error) { + console.error("Error loading turboreleaser CLI, please re-install", error); + process.exit(1); +} diff --git a/packages/turbo-releaser/package.json b/packages/turbo-releaser/package.json new file mode 100644 index 0000000000000..6917aaa0b75fd --- /dev/null +++ b/packages/turbo-releaser/package.json @@ -0,0 +1,32 @@ +{ + "name": "@turbo/releaser", + "private": true, + "version": "0.0.1", + "bin": { + "turboreleaser": "cli/index.cjs" + }, + "files": [ + "dist", + "template" + ], + "scripts": { + "build": "tsup", + "check-types": "tsc --noEmit", + "test": "node --import tsx --test src/*.test.ts", + "lint": "eslint src/", + "lint:prettier": "prettier -c . --cache --ignore-path=../../.prettierignore" + }, + "dependencies": { + "commander": "^11.0.0", + "tar": "6.1.13" + }, + "devDependencies": { + "@turbo/eslint-config": "workspace:*", + "@turbo/tsconfig": "workspace:*", + "@types/node": "^20", + "@types/tar": "^6.1.4", + "typescript": "5.5.4", + "tsup": "^6.7.0", + "tsx": "4.19.1" + } +} diff --git a/packages/turbo-releaser/src/index.ts b/packages/turbo-releaser/src/index.ts new file mode 100644 index 0000000000000..7b4e01d5c8516 --- /dev/null +++ b/packages/turbo-releaser/src/index.ts @@ -0,0 +1,45 @@ +import { Command } from "commander"; +import { packAndPublish } from "./packager"; +import type { Platform } from "./types"; +import { getVersionInfo } from "./version"; + +const supportedPlatforms: Array = [ + { os: "darwin", arch: "x64" }, + { os: "darwin", arch: "arm64" }, + { os: "linux", arch: "x64" }, + { os: "linux", arch: "arm64" }, + { os: "windows", arch: "x64" }, + { os: "windows", arch: "arm64" }, +]; + +const turboReleaser = new Command(); +turboReleaser + .requiredOption("--version-path ", "Path to the version.txt file") + .option("--skip-publish", "Skip publishing to NPM") + .action(main); + +async function main(options: { skipPublish: boolean; versionPath: string }) { + console.log("Command line options:", options); + console.log("Supported platforms:", supportedPlatforms); + + try { + const { version, npmTag } = await getVersionInfo(options.versionPath); + console.log(`Using version: ${version}, NPM tag: ${npmTag}`); + + await packAndPublish({ + platforms: supportedPlatforms, + version, + skipPublish: options.skipPublish as boolean, + npmTag, + }); + console.log("Packaging and publishing completed successfully"); + } catch (error) { + console.error("Error during packaging and publishing:", error); + process.exit(1); + } +} + +turboReleaser.parseAsync().catch((reason) => { + console.error("Unexpected error. Please report it as a bug:", reason); + process.exit(1); +}); diff --git a/packages/turbo-releaser/src/native.test.ts b/packages/turbo-releaser/src/native.test.ts new file mode 100644 index 0000000000000..0801a67101707 --- /dev/null +++ b/packages/turbo-releaser/src/native.test.ts @@ -0,0 +1,126 @@ +import { describe, it, mock } from "node:test"; +import assert from "node:assert/strict"; +import path from "node:path"; +import fs from "node:fs/promises"; +import native from "./native"; +import type { Platform } from "./types"; + +describe("generateNativePackage", () => { + const outputDir = "/path/to/output"; + + it("should generate package correctly for non-Windows platform", async (t) => { + const mockRm = mock.fn((_path: string) => Promise.resolve()); + const mockMkdir = mock.fn((_path: string) => Promise.resolve()); + const mockCopyFile = mock.fn((_src: string, _dst: string) => + Promise.resolve() + ); + const mockWriteFile = mock.fn((_path: string, _data: string) => + Promise.resolve() + ); + + t.mock.method(fs, "rm", mockRm); + t.mock.method(fs, "mkdir", mockMkdir); + t.mock.method(fs, "copyFile", mockCopyFile); + t.mock.method(fs, "writeFile", mockWriteFile); + + const platform: Platform = { os: "darwin", arch: "x64" }; + const version = "1.0.0"; + await native.generateNativePackage({ platform, version, outputDir }); + + // Assert rm was called correctly + assert.equal(mockRm.mock.calls.length, 1); + assert.equal(mockRm.mock.calls[0].arguments[0], outputDir); + + // Assert mkdir was called correctly + assert.equal(mockMkdir.mock.calls.length, 1); + assert.equal( + mockMkdir.mock.calls[0].arguments[0], + path.join(outputDir, "bin") + ); + + // Assert copyFile was called correctly + assert.equal(mockCopyFile.mock.calls.length, 1); + assert.ok( + mockCopyFile.mock.calls[0].arguments[0].endsWith("template/README.md") + ); + assert.equal( + mockCopyFile.mock.calls[0].arguments[1], + path.join(outputDir, "README.md") + ); + + // Assert writeFile was called correctly + assert.equal(mockWriteFile.mock.calls.length, 1); + const [filePath, content] = mockWriteFile.mock.calls[0].arguments; + assert.equal(filePath, path.join(outputDir, "package.json")); + + const packageJson = JSON.parse(content) as { + name: string; + version: string; + description: string; + os: Array; + cpu: Array; + }; + assert.equal(packageJson.name, `turbo-darwin-${native.archToHuman.x64}`); + assert.equal(packageJson.version, version); + assert.equal( + packageJson.description, + "The darwin-x64 binary for turbo, a monorepo build system." + ); + assert.deepEqual(packageJson.os, ["darwin"]); + assert.deepEqual(packageJson.cpu, ["x64"]); + }); + + it("should handle Windows platform correctly", async (t) => { + const mockRm = mock.fn((_path: string) => Promise.resolve()); + const mockMkdir = mock.fn((_path: string) => Promise.resolve()); + const mockCopyFile = mock.fn((_src: string, _dst: string) => + Promise.resolve() + ); + const mockWriteFile = mock.fn((_path: string, _data: string) => + Promise.resolve() + ); + + t.mock.method(fs, "rm", mockRm); + t.mock.method(fs, "mkdir", mockMkdir); + t.mock.method(fs, "copyFile", mockCopyFile); + t.mock.method(fs, "writeFile", mockWriteFile); + + await native.generateNativePackage({ + platform: { os: "windows", arch: "x64" }, + version: "1.0.0", + outputDir, + }); + + assert.equal(mockCopyFile.mock.calls.length, 2); + assert.ok( + mockCopyFile.mock.calls[0].arguments[0].endsWith("template/bin/turbo") + ); + assert.equal( + mockCopyFile.mock.calls[0].arguments[1], + path.join(outputDir, "bin", "turbo") + ); + }); + + it("should propagate errors", async (t) => { + const mockRm = mock.fn(() => { + throw new Error("Failed to remove directory"); + }); + t.mock.method(fs, "rm", mockRm); + + await assert.rejects( + native.generateNativePackage({ + platform: { os: "linux", arch: "x64" }, + version: "1.2.0", + outputDir, + }), + { message: "Failed to remove directory" } + ); + }); +}); + +describe("archToHuman", () => { + it("should map architectures correctly", () => { + assert.equal(native.archToHuman.x64, "64"); + assert.equal(native.archToHuman.arm64, "arm64"); + }); +}); diff --git a/packages/turbo-releaser/src/native.ts b/packages/turbo-releaser/src/native.ts new file mode 100644 index 0000000000000..5bb75b66dff91 --- /dev/null +++ b/packages/turbo-releaser/src/native.ts @@ -0,0 +1,65 @@ +import { rm, mkdir, copyFile, writeFile } from "node:fs/promises"; +import path from "node:path"; +import type { SupportedArch, HumanArch, Platform } from "./types"; + +export const archToHuman: Record = { + x64: "64", + arm64: "arm64", +}; + +const templateDir = path.join(__dirname, "..", "template"); + +async function generateNativePackage({ + platform, + version, + outputDir, +}: { + platform: Platform; + version: string; + outputDir: string; +}) { + const { os, arch } = platform; + console.log(`Generating native package for ${os}-${arch}...`); + + console.log(`Cleaning output directory: ${outputDir}`); + await rm(outputDir, { recursive: true, force: true }); + await mkdir(path.join(outputDir, "bin"), { recursive: true }); + + if (os === "windows") { + console.log("Copying Windows-specific files..."); + await copyFile( + path.join(templateDir, "bin", "turbo"), + path.join(outputDir, "bin", "turbo") + ); + } + + console.log("Copying README.md..."); + await copyFile( + path.join(templateDir, "README.md"), + path.join(outputDir, "README.md") + ); + + console.log("Generating package.json..."); + const packageJson = { + name: `turbo-${os}-${archToHuman[arch]}`, + version, + description: `The ${os}-${arch} binary for turbo, a monorepo build system.`, + repository: "https://github.com/vercel/turborepo", + bugs: "https://github.com/vercel/turborepo/issues", + homepage: "https://turbo.build/repo", + license: "MIT", + os: [os], + cpu: [arch], + preferUnplugged: true, + }; + await writeFile( + path.join(outputDir, "package.json"), + JSON.stringify(packageJson, null, 2) + ); + + console.log(`Native package generated successfully in ${outputDir}`); +} + +// Exported asn an object instead of export keyword, so that these functions +// can be mocked in tests. +export default { generateNativePackage, archToHuman }; diff --git a/packages/turbo-releaser/src/operations.test.ts b/packages/turbo-releaser/src/operations.test.ts new file mode 100644 index 0000000000000..b00602ed415a2 --- /dev/null +++ b/packages/turbo-releaser/src/operations.test.ts @@ -0,0 +1,70 @@ +import { describe, it, mock } from "node:test"; +import fs from "node:fs/promises"; +import assert from "node:assert"; +import path from "node:path"; +import tar from "tar"; +import native from "./native"; +import type { Platform } from "./types"; +import operations from "./operations"; + +describe("packPlatform", () => { + it("should pack a platform correctly", async (t) => { + const mockGenerateNativePackage = mock.fn(); + const mockMkdir = mock.fn(); + const mockCopyFile = mock.fn(); + const mockTarCreate = mock.fn(); + + t.mock.method(native, "generateNativePackage", mockGenerateNativePackage); + t.mock.method(fs, "mkdir", mockMkdir); + t.mock.method(fs, "copyFile", mockCopyFile); + t.mock.method(tar, "create", mockTarCreate); + + const platform: Platform = { os: "darwin", arch: "x64" }; + const version = "1.0.0"; + + const result = await operations.packPlatform(platform, version); + + assert.equal(mockGenerateNativePackage.mock.calls.length, 1); + assert.equal(mockMkdir.mock.calls.length, 1); + assert.equal(mockCopyFile.mock.calls.length, 1); + assert.equal(mockTarCreate.mock.calls.length, 1); + + assert.ok(result.endsWith("darwin-x64-1.0.0.tar.gz")); + assert.ok(path.isAbsolute(result)); + }); + + it("should pack a Windows with .exe", async (t) => { + const mockGenerateNativePackage = mock.fn(); + const mockMkdir = mock.fn(); + const mockCopyFile = mock.fn((_src: string, _dst: string) => + Promise.resolve() + ); + const mockTarCreate = mock.fn(); + + t.mock.method(native, "generateNativePackage", mockGenerateNativePackage); + t.mock.method(fs, "mkdir", mockMkdir); + t.mock.method(fs, "copyFile", mockCopyFile); + t.mock.method(tar, "create", mockTarCreate); + + const platform: Platform = { os: "windows", arch: "x64" }; + const version = "1.0.0"; + + const result = await operations.packPlatform(platform, version); + + assert.ok( + mockCopyFile.mock.calls[0].arguments[0].endsWith("turbo.exe"), + "source ends with .exe" + ); + assert.ok( + mockCopyFile.mock.calls[0].arguments[1].endsWith("turbo.exe"), + "destination ends with .exe" + ); + assert.equal(mockGenerateNativePackage.mock.calls.length, 1); + assert.equal(mockMkdir.mock.calls.length, 1); + assert.equal(mockCopyFile.mock.calls.length, 1); + assert.equal(mockTarCreate.mock.calls.length, 1); + + assert.ok(result.endsWith("windows-x64-1.0.0.tar.gz")); + assert.ok(path.isAbsolute(result)); + }); +}); diff --git a/packages/turbo-releaser/src/operations.ts b/packages/turbo-releaser/src/operations.ts new file mode 100644 index 0000000000000..b65e399643404 --- /dev/null +++ b/packages/turbo-releaser/src/operations.ts @@ -0,0 +1,54 @@ +import path from "node:path"; +import fs from "node:fs/promises"; +import { execSync } from "node:child_process"; +import tar from "tar"; +import native from "./native"; +import type { Platform } from "./types"; + +async function packPlatform( + platform: Platform, + version: string +): Promise { + const { os, arch } = platform; + console.log(`Packing platform: ${os}-${arch}`); + const scaffoldDir = path.join("dist", `${os}-${arch}-${version}`); + + console.log("Generating native package..."); + await native.generateNativePackage({ + platform, + version, + outputDir: scaffoldDir, + }); + + console.log("Moving prebuilt binary..."); + const binaryName = os === "windows" ? "turbo.exe" : "turbo"; + const sourcePath = path.join(`dist-${os}-${arch}`, binaryName); + const destPath = path.join(scaffoldDir, "bin", binaryName); + await fs.mkdir(path.dirname(destPath), { recursive: true }); + await fs.copyFile(sourcePath, destPath); + + console.log("Creating tar.gz..."); + const tarName = `${os}-${arch}-${version}.tar.gz`; + const tarPath = path.join("dist", tarName); + await tar.create( + { + gzip: true, + file: tarPath, + cwd: scaffoldDir, + }, + ["package.json", "README.md", "bin"] + ); + + console.log(`Artifact created: ${tarPath}`); + return path.resolve(tarPath); +} + +function publishArtifacts(artifacts: Array, npmTag: string) { + for (const artifact of artifacts) { + const publishCommand = `npm publish "${artifact}" --tag ${npmTag}`; + console.log(`Executing: ${publishCommand}`); + execSync(publishCommand, { stdio: "inherit" }); + } +} + +export default { packPlatform, publishArtifacts }; diff --git a/packages/turbo-releaser/src/packager.test.ts b/packages/turbo-releaser/src/packager.test.ts new file mode 100644 index 0000000000000..c9568d838e2e3 --- /dev/null +++ b/packages/turbo-releaser/src/packager.test.ts @@ -0,0 +1,55 @@ +import { describe, it, mock } from "node:test"; +import assert from "node:assert/strict"; +import { packAndPublish } from "./packager"; +import type { Platform } from "./types"; +import operations from "./operations"; + +describe("packager", () => { + describe("packAndPublish", () => { + it("should pack and publish for all platforms when skipPublish is false", async (t) => { + const mockPackPlatform = mock.fn(() => + Promise.resolve("/path/to/artifact.tgz") + ); + const mockPublishArtifacts = mock.fn((_paths: Array) => + Promise.resolve() + ); + t.mock.method(operations, "packPlatform", mockPackPlatform); + t.mock.method(operations, "publishArtifacts", mockPublishArtifacts); + + const platforms: Array = [ + { os: "darwin", arch: "x64" }, + { os: "linux", arch: "arm64" }, + ]; + const version = "1.0.0"; + const npmTag = "latest"; + + await packAndPublish({ platforms, version, skipPublish: false, npmTag }); + + assert.equal(mockPackPlatform.mock.calls.length, 2); + assert.equal(mockPublishArtifacts.mock.calls.length, 1); + assert.deepEqual(mockPublishArtifacts.mock.calls[0].arguments, [ + ["/path/to/artifact.tgz", "/path/to/artifact.tgz"], + "latest", + ]); + }); + + it("should pack but not publish when skipPublish is true", async (t) => { + const mockPackPlatform = mock.fn(() => + Promise.resolve("/path/to/artifact.tgz") + ); + const mockPublishArtifacts = mock.fn(); + + t.mock.method(operations, "packPlatform", mockPackPlatform); + t.mock.method(operations, "publishArtifacts", mockPublishArtifacts); + + const platforms: Array = [{ os: "darwin", arch: "x64" }]; + const version = "1.0.0"; + const npmTag = "latest"; + + await packAndPublish({ platforms, version, skipPublish: true, npmTag }); + + assert.equal(mockPackPlatform.mock.calls.length, 1); + assert.equal(mockPublishArtifacts.mock.calls.length, 0); + }); + }); +}); diff --git a/packages/turbo-releaser/src/packager.ts b/packages/turbo-releaser/src/packager.ts new file mode 100644 index 0000000000000..bc4fa98f5b17d --- /dev/null +++ b/packages/turbo-releaser/src/packager.ts @@ -0,0 +1,35 @@ +import type { Platform } from "./types"; +import operations from "./operations"; + +interface PackAndPublishOptions { + platforms: Array; + version: string; + skipPublish: boolean; + npmTag: string; +} + +export async function packAndPublish({ + platforms, + version, + skipPublish, + npmTag, +}: PackAndPublishOptions) { + console.log("Starting packAndPublish process..."); + const artifacts: Array = []; + + for (const platform of platforms) { + console.log(`Processing platform: ${platform.os}-${platform.arch}`); + // eslint-disable-next-line no-await-in-loop -- We trade of slightly faster releases with more legible logging + const artifact = await operations.packPlatform(platform, version); + artifacts.push(artifact); + } + + console.log("All platforms processed. Artifacts:", artifacts); + + if (!skipPublish) { + console.log("Publishing artifacts..."); + operations.publishArtifacts(artifacts, npmTag); + } else { + console.log("Skipping publish step."); + } +} diff --git a/packages/turbo-releaser/src/types.ts b/packages/turbo-releaser/src/types.ts new file mode 100644 index 0000000000000..f75d9bba2c69c --- /dev/null +++ b/packages/turbo-releaser/src/types.ts @@ -0,0 +1,8 @@ +export type SupportedOS = "darwin" | "linux" | "windows"; +export type SupportedArch = "x64" | "arm64"; +export type HumanArch = "64" | "arm64"; + +export interface Platform { + os: SupportedOS; + arch: SupportedArch; +} diff --git a/packages/turbo-releaser/src/version.test.ts b/packages/turbo-releaser/src/version.test.ts new file mode 100644 index 0000000000000..0319c23b2adb1 --- /dev/null +++ b/packages/turbo-releaser/src/version.test.ts @@ -0,0 +1,29 @@ +import { describe, it, mock } from "node:test"; +import assert from "node:assert"; +import fs from "node:fs/promises"; +import { getVersionInfo } from "./version"; + +describe("getVersionInfo", () => { + it("should read version and npm tag from version.txt", async (t) => { + const mockReadFile = mock.fn((_path, _encoding) => { + return Promise.resolve("1.0.0\nbeta\n"); + }); + t.mock.method(fs, "readFile", mockReadFile); + const result = await getVersionInfo("some-path/version.txt"); + assert.deepStrictEqual(result, { version: "1.0.0", npmTag: "beta" }); + assert.equal( + mockReadFile.mock.calls[0].arguments[0], + "some-path/version.txt" + ); + }); + + it("should throw an error if version.txt is not found", async (t) => { + const mockReadFile = mock.fn((_path, _encoding) => { + return Promise.reject(new Error("File not found")); + }); + t.mock.method(fs, "readFile", mockReadFile); + await assert.rejects(() => getVersionInfo("version.txt"), { + message: "File not found", + }); + }); +}); diff --git a/packages/turbo-releaser/src/version.ts b/packages/turbo-releaser/src/version.ts new file mode 100644 index 0000000000000..a727355f54c2b --- /dev/null +++ b/packages/turbo-releaser/src/version.ts @@ -0,0 +1,11 @@ +import fs from "node:fs/promises"; + +export async function getVersionInfo(versionPath: string): Promise<{ + version: string; + npmTag: string; +}> { + const versionFile = await fs.readFile(versionPath, "utf-8"); + const [version, npmTag] = versionFile.trim().split("\n"); + console.log(`Version: ${version}, NPM Tag: ${npmTag}`); + return { version, npmTag }; +} diff --git a/packages/turbo-releaser/template/README.md b/packages/turbo-releaser/template/README.md new file mode 100644 index 0000000000000..27416bc7b03fd --- /dev/null +++ b/packages/turbo-releaser/template/README.md @@ -0,0 +1,3 @@ +# `turbo` + +This is a platform-specific binary for Turborepo, a monorepo build system. See https://github.com/vercel/turborepo for details. diff --git a/packages/turbo-releaser/template/bin/turbo b/packages/turbo-releaser/template/bin/turbo new file mode 100644 index 0000000000000..4557a07e212c5 --- /dev/null +++ b/packages/turbo-releaser/template/bin/turbo @@ -0,0 +1,15 @@ +#!/usr/bin/env node + +// Unfortunately even though npm shims "bin" commands on Windows with auto- +// generated forwarding scripts, it doesn't strip the ".exe" from the file name +// first. So it's possible to publish executables via npm on all platforms +// except Windows. I consider this a npm bug. +// +// My workaround is to add this script as another layer of indirection. It'll +// be slower because node has to boot up just to shell out to the actual exe, +// but Windows is somewhat of a second-class platform to npm so it's the best +// I can do I think. +const path = require('path'); +const turbo_exe = path.join(__dirname, 'turbo.exe'); +const child_process = require('child_process'); +child_process.spawnSync(turbo_exe, process.argv.slice(2), { stdio: 'inherit' }); diff --git a/packages/turbo-releaser/tsconfig.json b/packages/turbo-releaser/tsconfig.json new file mode 100644 index 0000000000000..e6f9fc6aa70b6 --- /dev/null +++ b/packages/turbo-releaser/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@turbo/tsconfig/library.json", + "compilerOptions": { + "rootDir": ".", + "module": "ESNext", + "lib": ["ESNext"] + } +} diff --git a/packages/turbo-releaser/tsup.config.ts b/packages/turbo-releaser/tsup.config.ts new file mode 100644 index 0000000000000..b78e5518aef2a --- /dev/null +++ b/packages/turbo-releaser/tsup.config.ts @@ -0,0 +1,9 @@ +import { defineConfig, Options } from "tsup"; + +export default defineConfig((options: Options) => ({ + entry: ["src/index.ts"], + format: ["cjs"], + clean: true, + minify: true, + ...options, +})); diff --git a/packages/turbo-releaser/turbo.json b/packages/turbo-releaser/turbo.json new file mode 100644 index 0000000000000..07dd354a7722d --- /dev/null +++ b/packages/turbo-releaser/turbo.json @@ -0,0 +1,9 @@ +{ + "extends": ["//"], + "tasks": { + "build": { + "dependsOn": ["^build"], + "outputs": ["dist"] + } + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9c6f595b5617c..e492f4ff58b24 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -560,6 +560,37 @@ importers: specifier: 5.5.4 version: 5.5.4 + packages/turbo-releaser: + dependencies: + commander: + specifier: ^11.0.0 + version: 11.0.0 + tar: + specifier: 6.1.13 + version: 6.1.13 + devDependencies: + '@turbo/eslint-config': + specifier: workspace:* + version: link:../eslint-config + '@turbo/tsconfig': + specifier: workspace:* + version: link:../tsconfig + '@types/node': + specifier: ^20 + version: 20.11.30 + '@types/tar': + specifier: ^6.1.4 + version: 6.1.4 + tsup: + specifier: ^6.7.0 + version: 6.7.0(ts-node@10.9.2)(typescript@5.5.4) + tsx: + specifier: 4.19.1 + version: 4.19.1 + typescript: + specifier: 5.5.4 + version: 5.5.4 + packages/turbo-repository: devDependencies: '@napi-rs/cli': @@ -4188,7 +4219,6 @@ packages: /chownr@2.0.0: resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} engines: {node: '>=10'} - dev: true /ci-info@3.8.0: resolution: {integrity: sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==} @@ -6360,7 +6390,6 @@ packages: engines: {node: '>= 8'} dependencies: minipass: 3.3.6 - dev: true /fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -8893,14 +8922,12 @@ packages: engines: {node: '>=8'} dependencies: yallist: 4.0.0 - dev: true /minipass@4.0.0: resolution: {integrity: sha512-g2Uuh2jEKoht+zvO6vJqXmYpflPqzRBT+Th2h01DKh5z7wbY/AZ2gCQ78cP70YoHPyFdY30YBV5WxgLOEwOykw==} engines: {node: '>=8'} dependencies: yallist: 4.0.0 - dev: true /minipass@7.1.2: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} @@ -8913,7 +8940,6 @@ packages: dependencies: minipass: 3.3.6 yallist: 4.0.0 - dev: true /mixin-deep@1.3.2: resolution: {integrity: sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==} @@ -8932,7 +8958,7 @@ packages: /mkdirp@1.0.4: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} engines: {node: '>=10'} - dev: true + hasBin: true /ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} @@ -10720,7 +10746,6 @@ packages: minizlib: 2.1.2 mkdirp: 1.0.4 yallist: 4.0.0 - dev: true /test-exclude@6.0.0: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} @@ -11140,6 +11165,7 @@ packages: /tsx@4.19.1: resolution: {integrity: sha512-0flMz1lh74BR4wOvBjuh9olbnwqCPc35OOlfyzHba0Dc+QNUeWX/Gq2YTbnwcWPO3BMd8fkzRVrHcsR+a7z7rA==} engines: {node: '>=18.0.0'} + hasBin: true dependencies: esbuild: 0.23.1 get-tsconfig: 4.7.6 @@ -11288,6 +11314,7 @@ packages: /typescript@5.5.4: resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} engines: {node: '>=14.17'} + hasBin: true /uglify-js@3.17.4: resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==} From 795482263a1aa30c28127c694ef9bf323ba4d5d6 Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Wed, 5 Feb 2025 16:34:44 -0500 Subject: [PATCH 3/9] chore(release): swap out goreleaser for turboreleaser --- .github/workflows/turborepo-release.yml | 10 ---------- cli/Makefile | 3 +-- cli/package.json | 6 +++++- cli/turbo.json | 4 ++++ pnpm-lock.yaml | 6 +++++- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/.github/workflows/turborepo-release.yml b/.github/workflows/turborepo-release.yml index d256109fc29bf..2aec5878fa013 100644 --- a/.github/workflows/turborepo-release.yml +++ b/.github/workflows/turborepo-release.yml @@ -223,15 +223,6 @@ jobs: git config --global user.name 'Turbobot' git config --global user.email 'turbobot@vercel.com' - - name: Install GoReleaser - uses: goreleaser/goreleaser-action@v6 - with: - distribution: goreleaser-pro - version: v1.18.2 - install-only: true - env: - GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} - - name: Download Rust artifacts uses: actions/download-artifact@v4 with: @@ -249,7 +240,6 @@ jobs: - name: Perform Release run: cd cli && make publish-turbo SKIP_PUBLISH=${{ inputs.dry_run && '--skip-publish' || '' }} env: - GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} # Upload published artifacts in case they are needed for debugging later diff --git a/cli/Makefile b/cli/Makefile index 77658cbf29ea5..b5d56caad51d6 100644 --- a/cli/Makefile +++ b/cli/Makefile @@ -54,8 +54,7 @@ publish-turbo: build npm config set --location=project "//registry.npmjs.org/:_authToken" $(NPM_TOKEN) # Publishes the native npm modules. - # TODO: do this without goreleaser. - goreleaser release --rm-dist -f combined-shim.yml $(SKIP_PUBLISH) + turbo release-native -- $(SKIP_PUBLISH) # Split packing from the publish step so that npm locates the correct .npmrc file. cd $(CLI_DIR)/../packages/turbo && pnpm pack --pack-destination=$(CLI_DIR)/../ diff --git a/cli/package.json b/cli/package.json index deff9108f9185..197d0c130731e 100644 --- a/cli/package.json +++ b/cli/package.json @@ -5,6 +5,10 @@ "scripts": { "clean": "cargo clean --package turbo", "build": "cargo build --package turbo", - "build:release": "cargo build --package turbo --profile release-turborepo" + "build:release": "cargo build --package turbo --profile release-turborepo", + "release-native": "turboreleaser --version-path ../version.txt" + }, + "dependencies": { + "@turbo/releaser": "workspace:*" } } diff --git a/cli/turbo.json b/cli/turbo.json index 92b936b9c0dcc..9e1e5aa12ac4a 100644 --- a/cli/turbo.json +++ b/cli/turbo.json @@ -39,6 +39,10 @@ "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY" ] + }, + "release-native": { + "dependsOn": ["@turbo/releaser#build"], + "cache": false } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e492f4ff58b24..92a9dc7791a66 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,7 +46,11 @@ importers: specifier: ^0.36.0 version: 0.36.0 - cli: {} + cli: + dependencies: + '@turbo/releaser': + specifier: workspace:* + version: link:../packages/turbo-releaser docs: dependencies: From 57841ee435fb24ff24f79cffa93a38f5f84900c7 Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Wed, 5 Feb 2025 16:35:15 -0500 Subject: [PATCH 4/9] chore(release): delete goreleaser workflow --- cli/combined-shim.yml | 89 ------------------------------------------- 1 file changed, 89 deletions(-) delete mode 100644 cli/combined-shim.yml diff --git a/cli/combined-shim.yml b/cli/combined-shim.yml deleted file mode 100644 index 29bff222b46f1..0000000000000 --- a/cli/combined-shim.yml +++ /dev/null @@ -1,89 +0,0 @@ -project_name: turbo - -dist: dist - -# We just call `npm-native-packages.js` once for each item in goos x goarch -# the scrip generates a scaffold version of each npm package -# goreleaser just copies binary from dist-{os}-{arch}/turbo(.exe) to bin/turbo -builds: - - id: turbo - builder: prebuilt - tags: - - rust - - staticbinary - goos: - - linux - - windows - - darwin - goarch: - - amd64 - - arm64 - goamd64: - - v1 - prebuilt: - path: dist-{{ .Os }}-{{ .Arch }}/turbo{{ .Ext }} - hooks: - pre: - - cmd: ./scripts/npm-native-packages/npm-native-packages.js {{ .Os }} {{ .Arch }} {{ .Version }} - binary: bin/turbo -# goreleaser creates a checksums.txt -# don't think this is in use anywhere? could be in GH release, but not going anywhere -checksum: - name_template: "checksums.txt" -# feature we do not use -snapshot: - name_template: "{{ incpatch .Version }}" -archives: - # we do nothing with this archive - - id: github - name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}" - wrap_in_directory: true - replacements: - amd64: 64 - format: tar.gz - format_overrides: - - goos: windows - format: zip - files: - - LICENSE - - README.md - - id: npm - name_template: "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}" - wrap_in_directory: true - replacements: - amd64: 64 - format: tar.gz - files: - # copy only these files into - - LICENSE - - src: "scripts/npm-native-packages/build/{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}/package.json" - dst: "workaround/.." - strip_parent: true - - src: "scripts/npm-native-packages/build/{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}/README.md" - dst: "workaround/.." - strip_parent: true - - src: "scripts/npm-native-packages/build/{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}/bin/*" - dst: "bin/" - strip_parent: true -# used for GH releases only (which are disabled) -changelog: - sort: asc - filters: - exclude: - - "^docs:" - - "^test:" -# we do nothing with the release section since disabled -release: - github: - owner: vercel - name: turborepo - ids: - - github - prerelease: auto - disable: true -publishers: - - name: npm - ids: - - npm - # all this does is run this command for each archive we made above - cmd: "npm publish{{ if .Prerelease }} --tag canary{{ end }} {{ abs .ArtifactPath }}" From 11dcd8d8930d63437e5b8af0b244827e31a8260b Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Wed, 5 Feb 2025 18:39:53 -0500 Subject: [PATCH 5/9] chore(release): remove goreleaser arch naming from rust artifacts --- .github/workflows/turborepo-release.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/turborepo-release.yml b/.github/workflows/turborepo-release.yml index 2aec5878fa013..8fc3713ef5590 100644 --- a/.github/workflows/turborepo-release.yml +++ b/.github/workflows/turborepo-release.yml @@ -233,9 +233,9 @@ jobs: mv rust-artifacts/turbo-aarch64-apple-darwin cli/dist-darwin-arm64 mv rust-artifacts/turbo-aarch64-unknown-linux-musl cli/dist-linux-arm64 cp -r rust-artifacts/turbo-x86_64-pc-windows-msvc cli/dist-windows-arm64 - mv rust-artifacts/turbo-x86_64-unknown-linux-musl cli/dist-linux-amd64 - mv rust-artifacts/turbo-x86_64-apple-darwin cli/dist-darwin-amd64 - mv rust-artifacts/turbo-x86_64-pc-windows-msvc cli/dist-windows-amd64 + mv rust-artifacts/turbo-x86_64-unknown-linux-musl cli/dist-linux-x64 + mv rust-artifacts/turbo-x86_64-apple-darwin cli/dist-darwin-x64 + mv rust-artifacts/turbo-x86_64-pc-windows-msvc cli/dist-windows-x64 - name: Perform Release run: cd cli && make publish-turbo SKIP_PUBLISH=${{ inputs.dry_run && '--skip-publish' || '' }} From 2cf87253e36f624390de61aba579ceb6dce161ca Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Thu, 6 Feb 2025 08:28:27 -0500 Subject: [PATCH 6/9] chore(release): delete npm-native-packages script/templates --- cli/scripts/npm-native-packages/.gitignore | 1 - .../npm-native-packages.js | 51 ------------------- .../npm-native-packages/template/README.md | 3 -- .../npm-native-packages/template/bin/turbo | 15 ------ .../template/template.package.json | 12 ----- 5 files changed, 82 deletions(-) delete mode 100644 cli/scripts/npm-native-packages/.gitignore delete mode 100755 cli/scripts/npm-native-packages/npm-native-packages.js delete mode 100644 cli/scripts/npm-native-packages/template/README.md delete mode 100644 cli/scripts/npm-native-packages/template/bin/turbo delete mode 100644 cli/scripts/npm-native-packages/template/template.package.json diff --git a/cli/scripts/npm-native-packages/.gitignore b/cli/scripts/npm-native-packages/.gitignore deleted file mode 100644 index 84c048a73cc2e..0000000000000 --- a/cli/scripts/npm-native-packages/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build/ diff --git a/cli/scripts/npm-native-packages/npm-native-packages.js b/cli/scripts/npm-native-packages/npm-native-packages.js deleted file mode 100755 index 52b7463b40d1b..0000000000000 --- a/cli/scripts/npm-native-packages/npm-native-packages.js +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env node - -const fs = require("fs"); -const path = require("path"); - -// Map to node os and arch names. -const nodeOsLookup = { - darwin: "darwin", - linux: "linux", - windows: "win32", -}; - -const nodeArchLookup = { - amd64: "x64", - arm64: "arm64", -}; - -const humanizedArchLookup = { - amd64: "64", - arm64: "arm64", -}; - -const template = require("./template/template.package.json"); -const os = process.argv[2]; -const arch = process.argv[3]; -const version = process.argv[4]; - -template.name = `turbo-${os}-${humanizedArchLookup[arch]}`; -template.description = `The ${os}-${humanizedArchLookup[arch]} binary for turbo, a monorepo build system.`; -template.os = [nodeOsLookup[os]]; -template.cpu = [nodeArchLookup[arch]]; -template.version = version; - -const outputPath = path.join(__dirname, "build", template.name); -fs.rmSync(outputPath, { recursive: true, force: true }); -fs.mkdirSync(path.join(outputPath, "bin"), { recursive: true }); - -if (os === "windows") { - fs.copyFileSync( - path.join(__dirname, "template", "bin", "turbo"), - path.join(outputPath, "bin", "turbo") - ); -} -fs.copyFileSync( - path.join(__dirname, "template", "README.md"), - path.join(outputPath, "README.md") -); -fs.writeFileSync( - path.join(outputPath, "package.json"), - JSON.stringify(template, null, 2) -); diff --git a/cli/scripts/npm-native-packages/template/README.md b/cli/scripts/npm-native-packages/template/README.md deleted file mode 100644 index 27416bc7b03fd..0000000000000 --- a/cli/scripts/npm-native-packages/template/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# `turbo` - -This is a platform-specific binary for Turborepo, a monorepo build system. See https://github.com/vercel/turborepo for details. diff --git a/cli/scripts/npm-native-packages/template/bin/turbo b/cli/scripts/npm-native-packages/template/bin/turbo deleted file mode 100644 index 4557a07e212c5..0000000000000 --- a/cli/scripts/npm-native-packages/template/bin/turbo +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env node - -// Unfortunately even though npm shims "bin" commands on Windows with auto- -// generated forwarding scripts, it doesn't strip the ".exe" from the file name -// first. So it's possible to publish executables via npm on all platforms -// except Windows. I consider this a npm bug. -// -// My workaround is to add this script as another layer of indirection. It'll -// be slower because node has to boot up just to shell out to the actual exe, -// but Windows is somewhat of a second-class platform to npm so it's the best -// I can do I think. -const path = require('path'); -const turbo_exe = path.join(__dirname, 'turbo.exe'); -const child_process = require('child_process'); -child_process.spawnSync(turbo_exe, process.argv.slice(2), { stdio: 'inherit' }); diff --git a/cli/scripts/npm-native-packages/template/template.package.json b/cli/scripts/npm-native-packages/template/template.package.json deleted file mode 100644 index c6f9700b183e7..0000000000000 --- a/cli/scripts/npm-native-packages/template/template.package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "turbo-{{Os}}-{{Arch}}", - "version": "{{Version}", - "description": "The {{Os}}-{{Arch}} binary for turbo, a monorepo build system.", - "repository": "https://github.com/vercel/turborepo", - "bugs": "https://github.com/vercel/turborepo/issues", - "homepage": "https://turbo.build/repo", - "license": "MIT", - "os": ["{{Os}}"], - "cpu": ["{{Arch}}"], - "preferUnplugged": true -} From e316aca3454f7cca4cc74a33849776f514599932 Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Thu, 6 Feb 2025 11:07:53 -0500 Subject: [PATCH 7/9] fix(releaser): include LICENSE in native packages --- packages/turbo-releaser/src/native.test.ts | 11 +++++++++-- packages/turbo-releaser/src/native.ts | 19 ++++++++++--------- packages/turbo-releaser/template/LICENSE | 7 +++++++ 3 files changed, 26 insertions(+), 11 deletions(-) create mode 100644 packages/turbo-releaser/template/LICENSE diff --git a/packages/turbo-releaser/src/native.test.ts b/packages/turbo-releaser/src/native.test.ts index 0801a67101707..b0a1de655495f 100644 --- a/packages/turbo-releaser/src/native.test.ts +++ b/packages/turbo-releaser/src/native.test.ts @@ -39,7 +39,7 @@ describe("generateNativePackage", () => { ); // Assert copyFile was called correctly - assert.equal(mockCopyFile.mock.calls.length, 1); + assert.equal(mockCopyFile.mock.calls.length, 2); assert.ok( mockCopyFile.mock.calls[0].arguments[0].endsWith("template/README.md") ); @@ -47,6 +47,13 @@ describe("generateNativePackage", () => { mockCopyFile.mock.calls[0].arguments[1], path.join(outputDir, "README.md") ); + assert.ok( + mockCopyFile.mock.calls[1].arguments[0].endsWith("template/LICENSE") + ); + assert.equal( + mockCopyFile.mock.calls[1].arguments[1], + path.join(outputDir, "LICENSE") + ); // Assert writeFile was called correctly assert.equal(mockWriteFile.mock.calls.length, 1); @@ -91,7 +98,7 @@ describe("generateNativePackage", () => { outputDir, }); - assert.equal(mockCopyFile.mock.calls.length, 2); + assert.equal(mockCopyFile.mock.calls.length, 3); assert.ok( mockCopyFile.mock.calls[0].arguments[0].endsWith("template/bin/turbo") ); diff --git a/packages/turbo-releaser/src/native.ts b/packages/turbo-releaser/src/native.ts index 5bb75b66dff91..1ea2924ee0745 100644 --- a/packages/turbo-releaser/src/native.ts +++ b/packages/turbo-releaser/src/native.ts @@ -25,19 +25,20 @@ async function generateNativePackage({ await rm(outputDir, { recursive: true, force: true }); await mkdir(path.join(outputDir, "bin"), { recursive: true }); - if (os === "windows") { - console.log("Copying Windows-specific files..."); + const copyFromTemplate = async (part: string, ...parts: Array) => { + console.log("Copying ", path.join(part, ...parts)); await copyFile( - path.join(templateDir, "bin", "turbo"), - path.join(outputDir, "bin", "turbo") + path.join(templateDir, part, ...parts), + path.join(outputDir, part, ...parts) ); + }; + + if (os === "windows") { + await copyFromTemplate("bin", "turbo"); } - console.log("Copying README.md..."); - await copyFile( - path.join(templateDir, "README.md"), - path.join(outputDir, "README.md") - ); + await copyFromTemplate("README.md"); + await copyFromTemplate("LICENSE"); console.log("Generating package.json..."); const packageJson = { diff --git a/packages/turbo-releaser/template/LICENSE b/packages/turbo-releaser/template/LICENSE new file mode 100644 index 0000000000000..5c3db8bb6f857 --- /dev/null +++ b/packages/turbo-releaser/template/LICENSE @@ -0,0 +1,7 @@ +Copyright (c) 2024 Vercel, Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. From 1172ed92126b55d1f23e540e4d8b417bbcb41728 Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Thu, 6 Feb 2025 11:35:08 -0500 Subject: [PATCH 8/9] fix(releaser): tarball directory not loose files --- packages/turbo-releaser/src/operations.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/turbo-releaser/src/operations.ts b/packages/turbo-releaser/src/operations.ts index b65e399643404..822c37a2d46ed 100644 --- a/packages/turbo-releaser/src/operations.ts +++ b/packages/turbo-releaser/src/operations.ts @@ -11,7 +11,9 @@ async function packPlatform( ): Promise { const { os, arch } = platform; console.log(`Packing platform: ${os}-${arch}`); - const scaffoldDir = path.join("dist", `${os}-${arch}-${version}`); + const npmDirName = `turbo-${os}-${arch}`; + const tarballDir = path.join("dist", `${os}-${arch}-${version}`); + const scaffoldDir = path.join(tarballDir, npmDirName); console.log("Generating native package..."); await native.generateNativePackage({ @@ -34,9 +36,9 @@ async function packPlatform( { gzip: true, file: tarPath, - cwd: scaffoldDir, + cwd: tarballDir, }, - ["package.json", "README.md", "bin"] + [npmDirName] ); console.log(`Artifact created: ${tarPath}`); From 10ea8a687e7cd3ad3b23ee6c27ce6c32b00ee9a9 Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Fri, 7 Feb 2025 11:59:15 -0500 Subject: [PATCH 9/9] fix(releaser): make sure binary is executable --- packages/turbo-releaser/src/e2e.test.ts | 54 +++++++++++++++++++ packages/turbo-releaser/src/native.test.ts | 5 ++ packages/turbo-releaser/src/native.ts | 16 +++++- .../turbo-releaser/src/operations.test.ts | 17 +++++- packages/turbo-releaser/src/operations.ts | 29 +++++++--- packages/turbo-releaser/src/packager.ts | 2 +- packages/turbo-releaser/src/types.ts | 1 + packages/turbo-releaser/template/bin/turbo | 0 8 files changed, 112 insertions(+), 12 deletions(-) create mode 100644 packages/turbo-releaser/src/e2e.test.ts mode change 100644 => 100755 packages/turbo-releaser/template/bin/turbo diff --git a/packages/turbo-releaser/src/e2e.test.ts b/packages/turbo-releaser/src/e2e.test.ts new file mode 100644 index 0000000000000..1682ef2baf514 --- /dev/null +++ b/packages/turbo-releaser/src/e2e.test.ts @@ -0,0 +1,54 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import path from "node:path"; +import { tmpdir, arch as osArch, platform } from "node:os"; +import { mkdir, realpath, rm, writeFile } from "node:fs/promises"; +import { execSync } from "node:child_process"; +import operations from "./operations"; +import type { SupportedOS, SupportedArch } from "./types"; + +test("produces installable archive", async () => { + const tempDir = path.join(await realpath(tmpdir()), "turboreleaser-e2e-test"); + await rm(tempDir, { recursive: true, force: true }); + await mkdir(tempDir, { recursive: true }); + + // Need to match actual values otherwise npm will refuse to run + const os = platform() as SupportedOS; + const arch = osArch() as SupportedArch; + + // make a fake turbo binary + const platformPath = `dist-${os}-${arch}`; + await mkdir(path.join(tempDir, platformPath)); + await writeFile( + path.join(tempDir, platformPath, "turbo"), + "#!/bin/bash\necho Invoked fake turbo!" + ); + + const tarPath = await operations.packPlatform({ + platform: { os, arch }, + version: "0.1.2", + srcDir: tempDir, + }); + assert.ok(path.isAbsolute(tarPath)); + + // Make a fake repo to install the tarball in + const fakeRepo = path.join(tempDir, "fake-repo"); + await mkdir(fakeRepo); + await writeFile( + path.join(fakeRepo, "package.json"), + JSON.stringify({ + name: "fake-repo", + scripts: { "test-turbo-install": "turbo" }, + }) + ); + execSync(`npm install ${tarPath}`, { cwd: fakeRepo }); + const output = execSync("npm run test-turbo-install", { + stdio: "pipe", + cwd: fakeRepo, + encoding: "utf-8", + }); + assert.equal( + output, + "\n> test-turbo-install\n> turbo\n\nInvoked fake turbo!\n" + ); +}); diff --git a/packages/turbo-releaser/src/native.test.ts b/packages/turbo-releaser/src/native.test.ts index b0a1de655495f..e4c100cc09812 100644 --- a/packages/turbo-releaser/src/native.test.ts +++ b/packages/turbo-releaser/src/native.test.ts @@ -106,6 +106,11 @@ describe("generateNativePackage", () => { mockCopyFile.mock.calls[0].arguments[1], path.join(outputDir, "bin", "turbo") ); + const actualPackageJsonContents = mockWriteFile.mock.calls[0].arguments[1]; + const actualPackageJson = JSON.parse(actualPackageJsonContents) as { + os: Array; + }; + assert.equal(actualPackageJson.os[0], "win32"); }); it("should propagate errors", async (t) => { diff --git a/packages/turbo-releaser/src/native.ts b/packages/turbo-releaser/src/native.ts index 1ea2924ee0745..44d87f29192e2 100644 --- a/packages/turbo-releaser/src/native.ts +++ b/packages/turbo-releaser/src/native.ts @@ -1,12 +1,24 @@ import { rm, mkdir, copyFile, writeFile } from "node:fs/promises"; import path from "node:path"; -import type { SupportedArch, HumanArch, Platform } from "./types"; +import type { + SupportedArch, + HumanArch, + Platform, + SupportedOS, + NpmOs, +} from "./types"; export const archToHuman: Record = { x64: "64", arm64: "arm64", }; +export const nodeOSLookup: Record = { + darwin: "darwin", + linux: "linux", + windows: "win32", +}; + const templateDir = path.join(__dirname, "..", "template"); async function generateNativePackage({ @@ -49,7 +61,7 @@ async function generateNativePackage({ bugs: "https://github.com/vercel/turborepo/issues", homepage: "https://turbo.build/repo", license: "MIT", - os: [os], + os: [nodeOSLookup[os]], cpu: [arch], preferUnplugged: true, }; diff --git a/packages/turbo-releaser/src/operations.test.ts b/packages/turbo-releaser/src/operations.test.ts index b00602ed415a2..153aa63f5efab 100644 --- a/packages/turbo-releaser/src/operations.test.ts +++ b/packages/turbo-releaser/src/operations.test.ts @@ -12,21 +12,28 @@ describe("packPlatform", () => { const mockGenerateNativePackage = mock.fn(); const mockMkdir = mock.fn(); const mockCopyFile = mock.fn(); + const mockStat = mock.fn(() => Promise.resolve({ mode: 0 })); + const mockChmod = mock.fn(); const mockTarCreate = mock.fn(); t.mock.method(native, "generateNativePackage", mockGenerateNativePackage); t.mock.method(fs, "mkdir", mockMkdir); t.mock.method(fs, "copyFile", mockCopyFile); + t.mock.method(fs, "stat", mockStat); + t.mock.method(fs, "chmod", mockChmod); t.mock.method(tar, "create", mockTarCreate); const platform: Platform = { os: "darwin", arch: "x64" }; const version = "1.0.0"; - const result = await operations.packPlatform(platform, version); + const result = await operations.packPlatform({ platform, version }); assert.equal(mockGenerateNativePackage.mock.calls.length, 1); assert.equal(mockMkdir.mock.calls.length, 1); assert.equal(mockCopyFile.mock.calls.length, 1); + assert.equal(mockStat.mock.calls.length, 1); + assert.equal(mockChmod.mock.calls.length, 1); + assert.equal(mockChmod.mock.calls[0].arguments[1], 0o111); assert.equal(mockTarCreate.mock.calls.length, 1); assert.ok(result.endsWith("darwin-x64-1.0.0.tar.gz")); @@ -39,17 +46,21 @@ describe("packPlatform", () => { const mockCopyFile = mock.fn((_src: string, _dst: string) => Promise.resolve() ); + const mockStat = mock.fn(() => Promise.resolve({ mode: 0 })); + const mockChmod = mock.fn(); const mockTarCreate = mock.fn(); t.mock.method(native, "generateNativePackage", mockGenerateNativePackage); t.mock.method(fs, "mkdir", mockMkdir); + t.mock.method(fs, "stat", mockStat); + t.mock.method(fs, "chmod", mockChmod); t.mock.method(fs, "copyFile", mockCopyFile); t.mock.method(tar, "create", mockTarCreate); const platform: Platform = { os: "windows", arch: "x64" }; const version = "1.0.0"; - const result = await operations.packPlatform(platform, version); + const result = await operations.packPlatform({ platform, version }); assert.ok( mockCopyFile.mock.calls[0].arguments[0].endsWith("turbo.exe"), @@ -63,6 +74,8 @@ describe("packPlatform", () => { assert.equal(mockMkdir.mock.calls.length, 1); assert.equal(mockCopyFile.mock.calls.length, 1); assert.equal(mockTarCreate.mock.calls.length, 1); + assert.equal(mockChmod.mock.calls.length, 1); + assert.equal(mockChmod.mock.calls[0].arguments[1], 0o111); assert.ok(result.endsWith("windows-x64-1.0.0.tar.gz")); assert.ok(path.isAbsolute(result)); diff --git a/packages/turbo-releaser/src/operations.ts b/packages/turbo-releaser/src/operations.ts index 822c37a2d46ed..647c44ea8ba0a 100644 --- a/packages/turbo-releaser/src/operations.ts +++ b/packages/turbo-releaser/src/operations.ts @@ -5,14 +5,24 @@ import tar from "tar"; import native from "./native"; import type { Platform } from "./types"; -async function packPlatform( - platform: Platform, - version: string -): Promise { +export interface PackOptions { + platform: Platform; + version: string; + // Directory where prebuilt `turbo` binaries are located + // It will also be where `dist` is created and tarballs are put. + // Defaults to cwd + srcDir?: string; +} + +async function packPlatform({ + platform, + version, + srcDir = process.cwd(), +}: PackOptions): Promise { const { os, arch } = platform; console.log(`Packing platform: ${os}-${arch}`); const npmDirName = `turbo-${os}-${arch}`; - const tarballDir = path.join("dist", `${os}-${arch}-${version}`); + const tarballDir = path.join(srcDir, "dist", `${os}-${arch}-${version}`); const scaffoldDir = path.join(tarballDir, npmDirName); console.log("Generating native package..."); @@ -24,14 +34,19 @@ async function packPlatform( console.log("Moving prebuilt binary..."); const binaryName = os === "windows" ? "turbo.exe" : "turbo"; - const sourcePath = path.join(`dist-${os}-${arch}`, binaryName); + const sourcePath = path.join(srcDir, `dist-${os}-${arch}`, binaryName); const destPath = path.join(scaffoldDir, "bin", binaryName); await fs.mkdir(path.dirname(destPath), { recursive: true }); await fs.copyFile(sourcePath, destPath); + // Make sure the binary we copied is executable + const stat = await fs.stat(destPath); + const currMode = stat.mode; + // eslint-disable-next-line no-bitwise -- necessary for enabling the executable bits + await fs.chmod(destPath, currMode | 0o111); console.log("Creating tar.gz..."); const tarName = `${os}-${arch}-${version}.tar.gz`; - const tarPath = path.join("dist", tarName); + const tarPath = path.join(srcDir, "dist", tarName); await tar.create( { gzip: true, diff --git a/packages/turbo-releaser/src/packager.ts b/packages/turbo-releaser/src/packager.ts index bc4fa98f5b17d..b7cc8ab17f39f 100644 --- a/packages/turbo-releaser/src/packager.ts +++ b/packages/turbo-releaser/src/packager.ts @@ -20,7 +20,7 @@ export async function packAndPublish({ for (const platform of platforms) { console.log(`Processing platform: ${platform.os}-${platform.arch}`); // eslint-disable-next-line no-await-in-loop -- We trade of slightly faster releases with more legible logging - const artifact = await operations.packPlatform(platform, version); + const artifact = await operations.packPlatform({ platform, version }); artifacts.push(artifact); } diff --git a/packages/turbo-releaser/src/types.ts b/packages/turbo-releaser/src/types.ts index f75d9bba2c69c..5b966d31ff010 100644 --- a/packages/turbo-releaser/src/types.ts +++ b/packages/turbo-releaser/src/types.ts @@ -1,6 +1,7 @@ export type SupportedOS = "darwin" | "linux" | "windows"; export type SupportedArch = "x64" | "arm64"; export type HumanArch = "64" | "arm64"; +export type NpmOs = "darwin" | "linux" | "win32"; export interface Platform { os: SupportedOS; diff --git a/packages/turbo-releaser/template/bin/turbo b/packages/turbo-releaser/template/bin/turbo old mode 100644 new mode 100755