diff --git a/.github/workflows/turborepo-release.yml b/.github/workflows/turborepo-release.yml index 654a8f9d2a741..328b2fdc93912 100644 --- a/.github/workflows/turborepo-release.yml +++ b/.github/workflows/turborepo-release.yml @@ -214,15 +214,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: @@ -233,14 +224,13 @@ 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' || '' }} 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/combined-shim.yml b/cli/combined-shim.yml deleted file mode 100644 index 67d9914a3269c..0000000000000 --- a/cli/combined-shim.yml +++ /dev/null @@ -1,78 +0,0 @@ -project_name: turbo - -dist: dist - -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 -checksum: - name_template: "checksums.txt" -snapshot: - name_template: "{{ incpatch .Version }}" -archives: - - 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: - - 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 -changelog: - sort: asc - filters: - exclude: - - "^docs:" - - "^test:" -release: - github: - owner: vercel - name: turborepo - ids: - - github - prerelease: auto - disable: true -publishers: - - name: npm - ids: - - npm - cmd: "npm publish{{ if .Prerelease }} --tag canary{{ end }} {{ abs .ArtifactPath }}" 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/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/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 -} 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/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/cli/scripts/npm-native-packages/template/README.md b/packages/turbo-releaser/template/README.md similarity index 100% rename from cli/scripts/npm-native-packages/template/README.md rename to packages/turbo-releaser/template/README.md diff --git a/cli/scripts/npm-native-packages/template/bin/turbo b/packages/turbo-releaser/template/bin/turbo similarity index 100% rename from cli/scripts/npm-native-packages/template/bin/turbo rename to packages/turbo-releaser/template/bin/turbo 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..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: @@ -560,6 +564,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 +4223,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 +6394,6 @@ packages: engines: {node: '>= 8'} dependencies: minipass: 3.3.6 - dev: true /fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -8893,14 +8926,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 +8944,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 +8962,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 +10750,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 +11169,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 +11318,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==}