Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add package.json auto version strategy #1052

Merged
merged 1 commit into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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`,
);
}

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 @@ -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}`,
);
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
Loading