diff --git a/.changeset/giant-cows-play.md b/.changeset/giant-cows-play.md new file mode 100644 index 000000000..8b43d2110 --- /dev/null +++ b/.changeset/giant-cows-play.md @@ -0,0 +1,5 @@ +--- +"@osdk/cli": patch +--- + +Add package.json auto version strategy diff --git a/packages/cli/README.md b/packages/cli/README.md index 854d74090..f16d1dd69 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -80,13 +80,15 @@ Auto Version options | Option | Description | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| --autoVersion | Enable auto versioning [string][choices: "git-describe"] | +| --autoVersion | Enable auto versioning [string][choices: "git-describe", "package-json"] | | --gitTagPrefix | Prefix to match git tags on when 'git-describe' auto versioning is used. If not provided, all tags are matched and the prefix 'v ' is stripped if present. [string] | `--version` and `--autoVersion` are mutually exclusive and only one can be passed. If `git-describe` is used for `--autoVersion`, the CLI will try to infer the version by running the `git describe` command with optionally `--match=` set if `--gitTagPrefix` is passed. +If `package-json` is used for `--autoVersion`, the CLI will try to infer the version by looking at the `version` field of the nearest `package.json` file. The current working directory will be traversed up to the root directory and the first `package.json` file, if found, will be used. + ### `version` subcommand The version subcommand allows users to manage their site versions. diff --git a/packages/cli/src/commands/site/deploy/index.ts b/packages/cli/src/commands/site/deploy/index.ts index e9e3d6634..6a9431afa 100644 --- a/packages/cli/src/commands/site/deploy/index.ts +++ b/packages/cli/src/commands/site/deploy/index.ts @@ -37,7 +37,9 @@ const command: CommandModule< const siteConfig: SiteConfig | undefined = config?.foundryConfig.site; const directory = siteConfig?.directory; const autoVersion = siteConfig?.autoVersion; - const gitTagPrefix = autoVersion?.tagPrefix; + const gitTagPrefix = autoVersion?.type === "git-describe" + ? autoVersion.tagPrefix + : undefined; const uploadOnly = siteConfig?.uploadOnly; return argv @@ -64,7 +66,7 @@ const command: CommandModule< autoVersion: { coerce: (autoVersion) => autoVersion as AutoVersionConfigType, type: "string", - choices: ["git-describe"], + choices: ["git-describe", "package-json"], description: "Enable auto versioning", ...(autoVersion != null) ? { default: autoVersion.type } @@ -121,9 +123,12 @@ const command: CommandModule< } const autoVersionType = args.autoVersion ?? autoVersion; - if (autoVersionType !== "git-describe") { + if ( + autoVersionType !== "git-describe" + && autoVersionType !== "package-json" + ) { throw new YargsCheckError( - `Only 'git-describe' is supported for autoVersion`, + `Only 'git-describe' and 'package-json' are supported for autoVersion`, ); } diff --git a/packages/cli/src/commands/site/deploy/logDeployCommandConfigFileOverride.ts b/packages/cli/src/commands/site/deploy/logDeployCommandConfigFileOverride.ts index 58de175f1..c1f4338ca 100644 --- a/packages/cli/src/commands/site/deploy/logDeployCommandConfigFileOverride.ts +++ b/packages/cli/src/commands/site/deploy/logDeployCommandConfigFileOverride.ts @@ -38,7 +38,8 @@ export async function logDeployCommandConfigFileOverride( } if ( - config?.autoVersion?.tagPrefix != null + config?.autoVersion?.type === "git-describe" + && config.autoVersion.tagPrefix != null && args.gitTagPrefix != null && args.gitTagPrefix !== config.autoVersion.tagPrefix ) { diff --git a/packages/cli/src/commands/site/deploy/siteDeployCommand.mts b/packages/cli/src/commands/site/deploy/siteDeployCommand.mts index 56dfa898f..cfdf850a5 100644 --- a/packages/cli/src/commands/site/deploy/siteDeployCommand.mts +++ b/packages/cli/src/commands/site/deploy/siteDeployCommand.mts @@ -58,7 +58,7 @@ export default async function siteDeployCommand( if (typeof selectedVersion === "string") { siteVersion = selectedVersion; } else { - siteVersion = await findAutoVersion(selectedVersion.tagPrefix); + siteVersion = await findAutoVersion(selectedVersion); consola.info( `Auto version inferred next version to be: ${siteVersion}`, ); diff --git a/packages/cli/src/util/autoVersion.test.ts b/packages/cli/src/util/autoVersion.test.ts index e63c6c0c4..6892f734f 100644 --- a/packages/cli/src/util/autoVersion.test.ts +++ b/packages/cli/src/util/autoVersion.test.ts @@ -14,23 +14,41 @@ * limitations under the License. */ +import { findUp } from "find-up"; import { exec } from "node:child_process"; +import { promises as fsPromises } from "node:fs"; import { promisify } from "node:util"; import { describe, expect, it, vi } from "vitest"; import { autoVersion } from "./autoVersion.js"; +vi.mock("find-up"); vi.mock("node:child_process"); +vi.mock("node:fs"); const execAsync = promisify(exec); describe("autoVersion", () => { const execMock = vi.mocked(execAsync); const execReturnValue = (out: string) => ({ stdout: out, stderr: "" }); + it("should return a valid SemVer version from package.json", async () => { + const validPackageJsonVersion = "1.2.3"; + vi.mocked(findUp).mockResolvedValue("/path/package.json"); + vi.mocked(fsPromises.readFile).mockResolvedValue( + JSON.stringify({ version: validPackageJsonVersion }), + ); + const version = await autoVersion({ + type: "package-json", + }); + expect(version).toBe("1.2.3"); + }); + it("should return a valid SemVer version from git describe", async () => { const validGitVersion = "1.2.3"; execMock.mockResolvedValue(execReturnValue(validGitVersion)); - const version = await autoVersion(); + const version = await autoVersion({ + type: "git-describe", + }); expect(version).toBe("1.2.3"); }); @@ -38,7 +56,9 @@ describe("autoVersion", () => { const validGitVersion = "v1.2.3"; execMock.mockResolvedValue(execReturnValue(validGitVersion)); - const version = await autoVersion(); + const version = await autoVersion({ + type: "git-describe", + }); expect(version).toBe("1.2.3"); }); @@ -46,7 +66,10 @@ describe("autoVersion", () => { const validGitVersion = "@package@1.2.3"; execMock.mockResolvedValue(execReturnValue(validGitVersion)); - const version = await autoVersion("@package@"); + const version = await autoVersion({ + type: "git-describe", + tagPrefix: "@package@", + }); expect(version).toBe("1.2.3"); }); @@ -54,7 +77,10 @@ describe("autoVersion", () => { const validGitVersion = "1.2.3-package"; execMock.mockResolvedValue(execReturnValue(validGitVersion)); - const version = await autoVersion("-package"); + const version = await autoVersion({ + type: "git-describe", + tagPrefix: "@package@", + }); expect(version).toBe("1.2.3-package"); }); @@ -62,7 +88,9 @@ describe("autoVersion", () => { const nonSemVerGitVersion = "not-semver"; execMock.mockResolvedValue(execReturnValue(nonSemVerGitVersion)); - await expect(autoVersion()).rejects.toThrowError(); + await expect(autoVersion({ + type: "git-describe", + })).rejects.toThrowError(); }); it("should throw an error if git isn't found", async () => { @@ -70,7 +98,9 @@ describe("autoVersion", () => { throw new Error("Command not found"); }); - await expect(autoVersion()).rejects.toThrowError( + await expect(autoVersion({ + type: "git-describe", + })).rejects.toThrowError( "git is not installed", ); }); @@ -80,7 +110,9 @@ describe("autoVersion", () => { throw new Error("fatal: not a git repository"); }); - await expect(autoVersion()).rejects.toThrowError( + await expect(autoVersion({ + type: "git-describe", + })).rejects.toThrowError( "the current directory is not a git repository", ); }); @@ -90,7 +122,9 @@ describe("autoVersion", () => { throw new Error("fatal: no names found, cannot describe anything."); }); - await expect(autoVersion()).rejects.toThrowError( + await expect(autoVersion({ + type: "git-describe", + })).rejects.toThrowError( "no matching tags were found.", ); }); diff --git a/packages/cli/src/util/autoVersion.ts b/packages/cli/src/util/autoVersion.ts index c3b15e10f..e708584bf 100644 --- a/packages/cli/src/util/autoVersion.ts +++ b/packages/cli/src/util/autoVersion.ts @@ -15,8 +15,11 @@ */ import { ExitProcessError, isValidSemver } from "@osdk/cli.common"; +import { findUp } from "find-up"; import { exec } from "node:child_process"; +import { promises as fsPromises } from "node:fs"; import { promisify } from "node:util"; +import type { AutoVersionConfig } from "./config.js"; /** * Gets the version string using git describe. If the @param tagPrefix is empty, git describe will return the @@ -25,20 +28,53 @@ import { promisify } from "node:util"; * @returns A promise that resolves to the version string. * @throws An error if the version string is not SemVer compliant or if the version cannot be determined. */ -export async function autoVersion(tagPrefix: string = ""): Promise { +export async function autoVersion(config: AutoVersionConfig): Promise { + switch (config.type) { + case "git-describe": + return gitDescribeAutoVersion(config.tagPrefix); + case "package-json": + return packageJsonAutoVersion(); + default: + const value: never = config; + throw new Error( + `Unexpected auto version config: (${JSON.stringify(value)})`, + ); + } +} + +async function gitDescribeAutoVersion(tagPrefix: string = ""): Promise { const [matchPrefix, prefixRegex] = tagPrefix !== "" ? [tagPrefix, new RegExp(`^${tagPrefix}`)] : [undefined, new RegExp(`^v?`)]; const gitVersion = await gitDescribe(matchPrefix); const version = gitVersion.trim().replace(prefixRegex, ""); - if (!isValidSemver(version)) { + validateVersion(version); + return version; +} + +async function packageJsonAutoVersion(): Promise { + const packageJsonPath = await findUp("package.json"); + if (!packageJsonPath) { throw new ExitProcessError( 2, - `The version string ${version} is not SemVer compliant.`, + `Couldn't find package.json file in the current working directory or its parents: ${process.cwd()}`, + ); + } + + let packageJson; + try { + const fileContent = await fsPromises.readFile(packageJsonPath, "utf-8"); + packageJson = JSON.parse(fileContent); + } catch (error) { + throw new ExitProcessError( + 2, + `Couldn't read or parse package.json file ${packageJsonPath}. Error: ${error}`, ); } + const version = packageJson.version; + validateVersion(version); return version; } @@ -101,3 +137,12 @@ async function gitDescribe(matchPrefix: string | undefined): Promise { return gitVersion; } + +function validateVersion(version: string): void { + if (!isValidSemver(version)) { + throw new ExitProcessError( + 2, + `The version string ${version} is not SemVer compliant.`, + ); + } +} diff --git a/packages/cli/src/util/config.test.ts b/packages/cli/src/util/config.test.ts index d182a95d0..8eb80e9a3 100644 --- a/packages/cli/src/util/config.test.ts +++ b/packages/cli/src/util/config.test.ts @@ -74,6 +74,29 @@ describe("loadFoundryConfig", () => { }); }); + it("should load and parse package.json auto version strategy", async () => { + const correctConfig = { + foundryUrl: "http://localhost", + site: { + application: "test-app", + directory: "/test/directory", + autoVersion: { + type: "package-json", + }, + }, + }; + + vi.mocked(fsPromises.readFile).mockResolvedValue( + JSON.stringify(correctConfig), + ); + await expect(loadFoundryConfig()).resolves.toEqual({ + configFilePath: "/path/foundry.config.json", + foundryConfig: { + ...correctConfig, + }, + }); + }); + it("should throw an error if autoVersion type isn't allowed", async () => { const inCorrectConfig = { foundryUrl: "http://localhost", @@ -110,7 +133,7 @@ describe("loadFoundryConfig", () => { ); await expect(loadFoundryConfig()).rejects.toThrow( - "The configuration file does not match the expected schema: data/site/autoVersion must have required property 'type'", + "The configuration file does not match the expected schema: data/site/autoVersion must match exactly one schema in oneOf", ); }); diff --git a/packages/cli/src/util/config.ts b/packages/cli/src/util/config.ts index e9a7d5c18..ee621e19e 100644 --- a/packages/cli/src/util/config.ts +++ b/packages/cli/src/util/config.ts @@ -23,7 +23,12 @@ export interface GitDescribeAutoVersionConfig { type: "git-describe"; tagPrefix?: string; } -export type AutoVersionConfig = GitDescribeAutoVersionConfig; +export interface PackageJsonAutoVersionConfig { + type: "package-json"; +} +export type AutoVersionConfig = + | GitDescribeAutoVersionConfig + | PackageJsonAutoVersionConfig; export type AutoVersionConfigType = AutoVersionConfig["type"]; export interface SiteConfig { application: string; @@ -65,6 +70,11 @@ const CONFIG_FILE_SCHEMA: JSONSchemaType = { tagPrefix: { type: "string", nullable: true }, }, }, + { + properties: { + type: { const: "package-json", type: "string" }, + }, + }, ], required: ["type"], },