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

Address minor TODOs #55

Merged
merged 9 commits into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
4 changes: 3 additions & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,14 @@
"consola": "^3.2.3",
"find-up": "^6.3.0",
"open": "^9.1.0",
"semver": "^7.6.0",
"yargs": "^17.7.2"
},
"devDependencies": {
"@types/archiver": "^6.0.0",
"@types/ngeohash": "^0.6.4",
"@types/node": "^18.0.0",
"@types/semver": "^7.5.7",
"@types/yargs": "^17.0.29",
"ts-expect": "^1.3.0",
"typescript": "^5.2.2"
Expand All @@ -54,7 +56,7 @@
"imports": {
"#net": "./src/net/index.mts"
},
"keywords": [ ],
"keywords": [],
"bin": {
"osdk": "./bin/osdk.mjs"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export default async function siteDeployCommand(
if (version != null) {
siteVersion = version;
} else {
siteVersion = await findAutoVersion(gitTagPrefix);
siteVersion = findAutoVersion(gitTagPrefix);
zeyadkhaled marked this conversation as resolved.
Show resolved Hide resolved
consola.info(
`Auto version inferred next version to be: ${siteVersion}`,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@ export default async function versionListCommand(
}

consola.success("Found versions:");
for (const version of versions) {

const semver = await import("semver");
const sortedVersions = semver.rsort(versions.filter(v => semver.valid(v)));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can probably assume the versions are enforced semver given the timelines?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think its better to leave it and I could clarify with a comment. I personally have few broken apps that have LOOSE versions stored

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I guess the point is that after a certain point in time in the future we're not going to have the control to filter this and it will be on the service layer to decide how to handle this instead

for (const version of sortedVersions) {
consola.log(
` - ${version}${
deployedVersion
Expand Down
49 changes: 39 additions & 10 deletions packages/cli/src/util/autoVersion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ vi.mock("node:child_process");
describe("autoVersion", () => {
const execSyncMock = vi.mocked(execSync);

it("should return a valid SemVer version from git describe", async () => {
it("should return a valid SemVer version from git describe", () => {
const validGitVersion = "1.2.3";
execSyncMock.mockReturnValue(validGitVersion);

const version = await autoVersion();
const version = autoVersion();
expect(version).toBe("1.2.3");

expect(execSyncMock).toHaveBeenCalledWith(
Expand All @@ -36,10 +36,10 @@ describe("autoVersion", () => {
);
});

it("should replace default prefix v from git describe output", async () => {
it("should replace default prefix v from git describe output", () => {
const validGitVersion = "v1.2.3";
execSyncMock.mockReturnValue(validGitVersion);
const version = await autoVersion();
const version = autoVersion();

expect(version).toBe("1.2.3");
expect(execSyncMock).toHaveBeenCalledWith(
Expand All @@ -48,11 +48,11 @@ describe("autoVersion", () => {
);
});

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

const version = await autoVersion("@package@");
const version = autoVersion("@package@");

expect(version).toBe("1.2.3");
expect(execSyncMock).toHaveBeenCalledWith(
Expand All @@ -61,11 +61,11 @@ describe("autoVersion", () => {
);
});

it("should only replace the prefix if found at the start of the tag only", async () => {
it("should only replace the prefix if found at the start of the tag only", () => {
const validGitVersion = "1.2.3-package";
execSyncMock.mockReturnValue(validGitVersion);

const version = await autoVersion("-package");
const version = autoVersion("-package");

expect(version).toBe("1.2.3-package");
expect(execSyncMock).toHaveBeenCalledWith(
Expand All @@ -74,10 +74,39 @@ describe("autoVersion", () => {
);
});

it("should throw an error if git describe returns a non-SemVer string", async () => {
it("should throw an error if git describe returns a non-SemVer string", () => {
const nonSemVerGitVersion = "not-semver";
execSyncMock.mockReturnValue(nonSemVerGitVersion);
expect(() => autoVersion()).toThrowError();
});

it("should throw an error if git isn't found", () => {
execSyncMock.mockImplementation(() => {
throw new Error("Command not found");
});

expect(() => autoVersion()).toThrowError(
"Unable to determine the version using git-describe as git is not installed",
);
});

it("should throw an error if git isn't found", () => {
execSyncMock.mockImplementation(() => {
throw new Error("fatal: not a git repository");
});

await expect(autoVersion()).rejects.toThrow();
expect(() => autoVersion()).toThrowError(
"Unable to determine the version using git-describe as the current directory is not a git repository",
);
});

it("should throw an error if git isn't found", () => {
execSyncMock.mockImplementation(() => {
throw new Error("fatal: no names found, cannot describe anything.");
});

expect(() => autoVersion()).toThrowError(
"Unable to determine the version using git-describe as no tags were found.",
);
});
});
64 changes: 53 additions & 11 deletions packages/cli/src/util/autoVersion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,30 +25,72 @@ import { isValidSemver } from "./isValidSemver.js";
* @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 function autoVersion(tagPrefix: string = ""): string {
const [matchPrefix, prefixRegex] = tagPrefix !== ""
? [tagPrefix, new RegExp(`^${tagPrefix}`)]
: [undefined, new RegExp(`^v?`)];

const gitVersion = gitDescribe(matchPrefix);
const version = gitVersion.trim().replace(prefixRegex, "");
if (!isValidSemver(version)) {
throw new ExitProcessError(
2,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From earlier comments we don't actually do anything with this errorCode information on the error and it's inconsistent on what it means where some of the net code uses it as a HTTP status code and here it seems like we're using it as an exit code. What should we do with it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually think it doesn't add any value as well. So I could remove the passing of errorCode in a FLUP. Unless you foresee a usecase where we could benefit from it consistently?

`The version string ${version} is not SemVer compliant.`,
);
}

return version;
}

function gitDescribe(matchPrefix: string | undefined): string {
zeyadkhaled marked this conversation as resolved.
Show resolved Hide resolved
let gitVersion;
try {
const gitVersion = execSync(
gitVersion = execSync(
`git describe --tags --first-parent --dirty${
matchPrefix != null ? ` --match="${matchPrefix}*"` : ""
}`,
{ encoding: "utf8" },
);
const version = gitVersion.trim().replace(prefixRegex, "");
if (!isValidSemver(version)) {
throw new ExitProcessError(
2,
`The version string ${version} is not SemVer compliant.`,
);
} catch (error: any) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, have you tested for this new execAsync change that it throws? If it does not throw on non-zero exit code is the error message on stderr instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested and throws 👍

if (error instanceof Error) {
const errorMessage: string = error.message.toLowerCase();

if (
errorMessage.includes("not recognized")
|| errorMessage.includes("command not found")
|| errorMessage.includes("no such file or directory")
) {
throw new ExitProcessError(
2,
`Unable to determine the version using git-describe as git is not installed or found in the PATH.\nPlease correctly configure git or supply a --version option.`,
);
}

if (
errorMessage.includes("fatal: not a git repository")
) {
throw new ExitProcessError(
2,
`Unable to determine the version using git-describe as the current directory is not a git repository.\nPlease run the command in a git respository or supply a --version option.`,
);
}

if (
errorMessage.includes(
"fatal: no names found, cannot describe anything.",
)
) {
throw new ExitProcessError(
2,
`Unable to determine the version using git-describe as no tags were found.\nPlease tag a version or supply a --version option.`,
zeyadkhaled marked this conversation as resolved.
Show resolved Hide resolved
);
}
}

return version;
} catch (error) {
throw new ExitProcessError(
2,
`Unable to determine the version automatically. Please supply a --version argument. ${error}`,
`Unable to determine the version automatically: ${error}.\nPlease supply a --version argument.`,
zeyadkhaled marked this conversation as resolved.
Show resolved Hide resolved
);
}
return gitVersion;
}
16 changes: 14 additions & 2 deletions packages/cli/src/util/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,19 @@

import { findUp } from "find-up";
import { promises as fsPromises } from "node:fs";
import { describe, expect, it, vi } from "vitest";
import { extname } from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { loadFoundryConfig } from "./config.js";

vi.mock("find-up");
vi.mock("node:fs");
vi.mock("node:path");

describe("loadFoundryConfig", () => {
vi.mocked(findUp).mockResolvedValue("/path/foundry.config.json");
beforeEach(() => {
vi.mocked(findUp).mockResolvedValue("/path/foundry.config.json");
vi.mocked(extname).mockReturnValue(".json");
});

it("should load and parse the configuration file correctly", async () => {
const correctConfig = {
Expand Down Expand Up @@ -149,4 +154,11 @@ describe("loadFoundryConfig", () => {
vi.mocked(findUp).mockResolvedValue(undefined);
await expect(loadFoundryConfig()).resolves.toBeUndefined();
});

it("should throw if config file extension isn't supported", async () => {
vi.mocked(extname).mockResolvedValue(".yaml");
await expect(loadFoundryConfig()).rejects.toThrow(
"Unsupported file extension:",
);
});
});
24 changes: 23 additions & 1 deletion packages/cli/src/util/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import type { JSONSchemaType } from "ajv";
import { promises as fsPromises } from "node:fs";
import { extname } from "node:path";
import { ExitProcessError } from "../ExitProcessError.js";

export interface GitDescribeAutoVersionConfig {
Expand Down Expand Up @@ -44,6 +45,10 @@ const CONFIG_FILE_NAMES: string[] = [
"foundry.config.json",
];

const SUPPORTED_EXTENSIONS: string[] = [
".json",
];

const CONFIG_FILE_SCHEMA: JSONSchemaType<FoundryConfig> = {
type: "object",
properties: {
Expand Down Expand Up @@ -91,10 +96,17 @@ export async function loadFoundryConfig(): Promise<
const configFilePath = await findUp(CONFIG_FILE_NAMES);

if (configFilePath) {
const extension = extname(configFilePath);
if (!SUPPORTED_EXTENSIONS.includes(extension)) {
zeyadkhaled marked this conversation as resolved.
Show resolved Hide resolved
throw new ExitProcessError(
2,
`Unsupported file extension: ${extension} for config file. Only the following extensions are allowed ${SUPPORTED_EXTENSIONS}`,
);
}
let foundryConfig: FoundryConfig;
try {
const fileContent = await fsPromises.readFile(configFilePath, "utf-8");
foundryConfig = JSON.parse(fileContent);
foundryConfig = parseConfigFile(fileContent, extension);
} catch {
throw new ExitProcessError(
2,
Expand All @@ -116,3 +128,13 @@ export async function loadFoundryConfig(): Promise<

return undefined;
}

function parseConfigFile(
zeyadkhaled marked this conversation as resolved.
Show resolved Hide resolved
fileContent: string,
extension: string,
): FoundryConfig {
if (extension === ".json") {
return JSON.parse(fileContent);
}
throw Error();
}
6 changes: 2 additions & 4 deletions packages/cli/src/util/isValidSemver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,8 @@
* limitations under the License.
*/

// https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
const semver =
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
import { valid } from "semver";

export function isValidSemver(semverString: string): boolean {
return semver.test(semverString);
return valid(semverString) != null;
}
Loading
Loading