Skip to content

Commit

Permalink
Add package.json auto version strategy
Browse files Browse the repository at this point in the history
  • Loading branch information
tzyl committed Dec 5, 2024
1 parent 287de1d commit 199e7d2
Show file tree
Hide file tree
Showing 9 changed files with 145 additions and 20 deletions.
5 changes: 5 additions & 0 deletions .changeset/giant-cows-play.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@osdk/cli": patch
---

Add package.json auto version strategy
4 changes: 3 additions & 1 deletion packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<gitTagPrefix>` 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.
Expand Down
13 changes: 9 additions & 4 deletions packages/cli/src/commands/site/deploy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 }
Expand Down Expand Up @@ -106,9 +108,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`,
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,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}`,
);
Expand Down
50 changes: 42 additions & 8 deletions packages/cli/src/util/autoVersion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,63 +14,93 @@
* 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");
});

it("should replace default prefix v from git describe output", async () => {
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");
});

it("should replace the prefix from the found git tag", async () => {
const validGitVersion = "@[email protected]";
execMock.mockResolvedValue(execReturnValue(validGitVersion));

const version = await autoVersion("@package@");
const version = await autoVersion({
type: "git-describe",
tagPrefix: "@package@",
});
expect(version).toBe("1.2.3");
});

it("should only replace the prefix if found at the start of the tag only", async () => {
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");
});

it("should throw an error if git describe returns a non-SemVer string", async () => {
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 () => {
execMock.mockImplementation(() => {
throw new Error("Command not found");
});

await expect(autoVersion()).rejects.toThrowError(
await expect(autoVersion({
type: "git-describe",
})).rejects.toThrowError(
"git is not installed",
);
});
Expand All @@ -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",
);
});
Expand All @@ -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.",
);
});
Expand Down
51 changes: 48 additions & 3 deletions packages/cli/src/util/autoVersion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<string> {
export async function autoVersion(config: AutoVersionConfig): Promise<string> {
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<string> {
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<string> {
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;
}

Expand Down Expand Up @@ -101,3 +137,12 @@ async function gitDescribe(matchPrefix: string | undefined): Promise<string> {

return gitVersion;
}

function validateVersion(version: string): void {
if (!isValidSemver(version)) {
throw new ExitProcessError(
2,
`The version string ${version} is not SemVer compliant.`,
);
}
}
25 changes: 24 additions & 1 deletion packages/cli/src/util/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
);
});

Expand Down
12 changes: 11 additions & 1 deletion packages/cli/src/util/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -65,6 +70,11 @@ const CONFIG_FILE_SCHEMA: JSONSchemaType<FoundryConfig> = {
tagPrefix: { type: "string", nullable: true },
},
},
{
properties: {
type: { const: "package-json", type: "string" },
},
},
],
required: ["type"],
},
Expand Down

0 comments on commit 199e7d2

Please sign in to comment.