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

Creating wonder-stuff-ci package! #628

Merged
merged 9 commits into from
Apr 12, 2023
Merged
Show file tree
Hide file tree
Changes from 8 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/clean-houses-press.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/wonder-stuff-ci": patch
---

Adding a new wonder-stuff-ci package that contains functions that are useful for the mobile release.
3 changes: 3 additions & 0 deletions packages/wonder-stuff-ci/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# wonder-stuff-ci

This Wonder Stuff package contains functions for automation and scripts.
23 changes: 23 additions & 0 deletions packages/wonder-stuff-ci/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"publishConfig": {
"access": "public"
},
"engines": {
"node": ">=16"
},
"name": "@khanacademy/wonder-stuff-ci",
"version": "0.0.1",
"description": "Functions for automation and scripts.",
Copy link
Member

Choose a reason for hiding this comment

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

praise: Thanks!

"module": "dist/es/index.js",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
Copy link
Member

Choose a reason for hiding this comment

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

I think you're missing a critical index.ts file with all the things that you've created exported. Looking at wonder-stuff-core you'll probably want to add these lines here:

    "module": "dist/es/index.js",
    "main": "dist/index.js",
    "types": "dist/index.d.ts",

And then make a src/index.ts file that imports your files and re-exports them again.

"test": "bash -c 'yarn --silent --cwd \"../..\" test ${@:0} $($([[ ${@: -1} = -* ]] || [[ ${@: -1} = bash ]]) && echo $PWD)'"
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 was trying to get the test to run with yarn jest, but that didn't work because of typing. But using the yarn command it did work!

Copy link
Member

Choose a reason for hiding this comment

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

Ah yeah - I think this handles some of the trickiness with being in a monorepo, if I remember correctly.

Copy link
Member

Choose a reason for hiding this comment

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

Typically, you have to run yarn jest and yarn test from the root directory. This confusing command at the package level basically does that for you, so you can do yarn test in one of the package folders, and it will do the right thing.

},
"devDependencies": {
"@types/node": "^18.15.11",
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 needed to add this to resolve some types in the functions i need.

"ws-dev-build-settings": "^1.1.0"
Copy link
Member Author

Choose a reason for hiding this comment

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

What is the ws-dev-build-settings? Should I be adding the @types/node into this?

Copy link
Member

Choose a reason for hiding this comment

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

This is internal to wonder-stuff itself, these are the shared tools that we use to help with test running, builds, etc. I'm not sure if it makes sense to have the node types, or not! I'd say "no" only because not everything in wonder-stuff is designed to run in Node.

},
"author": "",
"license": "MIT"
}
17 changes: 17 additions & 0 deletions packages/wonder-stuff-ci/src/__tests__/buffer-to-string.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {bufferToString} from "../buffer-to-string";

describe("#bufferToString", () => {
it.each(["testing", Buffer.from("testing")])(
"the buffer to string function returns the correct value",
(testCase: string | Buffer) => {
// Arrange
const input = testCase;

// Act
const result = bufferToString(input);

// Assert
expect(result).toBe("testing");
},
);
});
45 changes: 45 additions & 0 deletions packages/wonder-stuff-ci/src/__tests__/compare-versions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {compareVersions} from "../compare-versions";

describe("#compareVersions", () => {
it.each(["7.8.1", "7.9.0", "7.10.0", "8.0.0"])(
"return 1 if version 1 is greater than version 2",
(testCase: string) => {
// Arrange
const version1 = testCase;
const version2 = "7.8.0";

// Act
const result = compareVersions(version1, version2);

// Assert
expect(result).toBe(1);
},
);

it.each(["7.7.1", "7.6.0", "6.10.0"])(
"return -1 if version 1 is less than version 2",
(testCase: string) => {
// Arrange
const version1 = testCase;
const version2 = "7.8.0";

// Act
const result = compareVersions(version1, version2);

// Assert
expect(result).toBe(-1);
},
);

it("return 0 if version 1 is equal to version 2", () => {
// Arrange
const version1 = "7.8.0";
const version2 = "7.8.0";

// Act
const result = compareVersions(version1, version2);

// Assert
expect(result).toBe(0);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {extractMobileReleaseInfoFromBranchName} from "../extract-mobile-release-info-from-branch-name";

describe("#extractMobileReleaseInfoFromBranchName", () => {
it.each([
"release/unified/7.8.0",
"release/android/7.8.0",
"release/ios/7.8.0",
])("get the version from the release branch", (testCase: string | null) => {
// Arrange
const releaseBranch = testCase;

// Act
const result = extractMobileReleaseInfoFromBranchName(releaseBranch);

// Assert
expect(result?.version).toBe("7.8.0");
});

it.each(["release/testing", "android/7.8.0", "ios/7.8.0", null])(
"return null if the branch is not a release branch",
(testCase: string | null) => {
// Arrange
const releaseBranch = testCase;

// Act
const result =
extractMobileReleaseInfoFromBranchName(releaseBranch);

// Assert
expect(result).toBe(null);
},
);

it("get the correct prefix string", () => {
// Arrange
const branchName = "release/unified/7.8.0";

// Act
const result = extractMobileReleaseInfoFromBranchName(branchName);

// Assert
expect(result?.prefix).toBe("release/unified/");
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {getMobileReleaseTags} from "../get-mobile-release-tags";

jest.mock("../get-tags-from-git");

describe("#getMobileReleaseTags", () => {
it("get the tags in sorted order by ascending version value", async () => {
// Arrange
jest.spyOn(
require("../get-tags-from-git"),
"getTagsFromGit",
).mockReturnValue(["android-7.10.0", "unified-7.8.0", "unified-7.9.0"]);

// Act
const result = await getMobileReleaseTags();

// Assert
expect(result).toStrictEqual([
"unified-7.8.0",
"unified-7.9.0",
"android-7.10.0",
]);
});
});
24 changes: 24 additions & 0 deletions packages/wonder-stuff-ci/src/__tests__/get-tags-from-git.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {getTagsFromGit} from "../get-tags-from-git";

import {execAsync} from "../exec-async";

jest.mock("../exec-async");

describe("#getTagsFromGit", () => {
it("return an array of release tags split by new line", async () => {
// Arrange
jest.spyOn(require("../exec-async"), "execAsync").mockReturnValue({
stdout: "android-7.10.0\nunified-7.8.0\nunified-7.9.0",
});

// Act
const result = await getTagsFromGit();

// Assert
expect(result).toStrictEqual([
"android-7.10.0",
"unified-7.8.0",
"unified-7.9.0",
]);
});
});
10 changes: 10 additions & 0 deletions packages/wonder-stuff-ci/src/buffer-to-string.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Coerce a buffer or string into a string.
*
* @param {Buffer | string} input The `Buffer` or `string`
* that should be coerced to a string.
* @returns {string} The `string` representation of the given parameter.
*/
export const bufferToString = (input: Buffer | string): string => {
return typeof input === "string" ? input : input.toString("utf8");
};
20 changes: 20 additions & 0 deletions packages/wonder-stuff-ci/src/compare-versions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Compare two versions or tags
* @param {string} v1 A version of the form `<num>.<num>.<num>` or tag of the form `<tag>-<num>.<num>.<num>`.
* @param {string} v2 A version of the form `<num>.<num>.<num>` or tag of the form `<tag>-<num>.<num>.<num>`.
* @returns {number} 1 if v1 > v2, -1 if v1 < v2, 0 if v1 == v2
*/
export const compareVersions = (v1: string, v2: string) => {
const v1v = v1.includes("-") ? v1.split("-")[1] : v1;
const v2v = v2.includes("-") ? v2.split("-")[1] : v2;
const v1p = v1v.replace(/^v/g, "").split(".");
const v2p = v2v.replace(/^v/g, "").split(".");
for (let i = 0; i < v1p.length || i < v2p.length; i++) {
const p1 = +v1p[i] || 0;
const p2 = +v2p[i] || 0;
if (+p1 !== +p2) {
return p1 > p2 ? 1 : -1;
}
}
return 0;
};
7 changes: 7 additions & 0 deletions packages/wonder-stuff-ci/src/exec-async.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {exec} from "child_process";
import util from "util";

/**
* A simple promisified version of child_process.exec, so we can `await` it
*/
export const execAsync = util.promisify(exec);
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Extract release version and prefix from mobile release branch.
*
* Example, given the branch `release/unified/v7.8.0`:
* {
* prefix: "release/unified/",
* version: "7.8.0"
* }
*
* @param {string} The release branch of the form `[release/[unified|ios|android]]/[v]<num>.<num>.<num>[-extra]`.
* @returns {Object} The release version and prefix, if found; otherwise, `null`.
Copy link
Member

Choose a reason for hiding this comment

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

suggestion: If you create the type as suggested below and then replace {Object} with {MobileReleaseInfo}, which gives folks a little nicer info in their IDEs.

*/
export const extractMobileReleaseInfoFromBranchName = (arg: string | null) => {
Copy link
Member

Choose a reason for hiding this comment

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

suggestion: Currently, the return type is inferred from the code and as such, there isn't a concrete way to refer to this type. It would be helpful for calling code if there was one as it can be used by them to type their own arguments and variables.

Something like:

type MobileReleaseInfo = {
    prefix: "release/unified/" | "release/ios" | "release/android",
    version: string,
};

And then:

Suggested change
export const extractMobileReleaseInfoFromBranchName = (arg: string | null) => {
export const extractMobileReleaseInfoFromBranchName =
(arg: string | null): MobileReleaseInfo => {

if (!arg) {
return null;
}

const match = arg.match(
/^(release\/(ios|android|unified)\/)?v?(\d+\.\d+\.\d+(-\w*)*)$/i,
);
return match && match.length >= 3 && match[3]
? {prefix: match[1] || "release/unified/", version: match[3]}
: null;
};
20 changes: 20 additions & 0 deletions packages/wonder-stuff-ci/src/get-mobile-release-tags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {compareVersions} from "./compare-versions";
import {getTagsFromGit} from "./get-tags-from-git";

/**
* Get mobile release tags.
*
* Tags are filtered to only include those matching our tag version format
* (`<tag>-<num>.<num>.<num>`), then they are sorted by the version information from
* earliest version to most recent.
*
* @returns {Promise<Array<string>>} all release tags sorted creation time ascending
*/
export const getMobileReleaseTags = async (): Promise<Array<string>> => {
const tags = await getTagsFromGit();
return tags
.filter((tag) =>
tag.match(/^(ios|android|unified)-(\d+\.\d+\.\d+(-\w*)*)$/i),
)
.sort(compareVersions);
};
13 changes: 13 additions & 0 deletions packages/wonder-stuff-ci/src/get-tags-from-git.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {execAsync} from "./exec-async";

/**
* Get all tags from git.
*
* @returns {Promise<Array<string>>} A promise of all git tags sorted by creation time ascending.
*/
export const getTagsFromGit = async (): Promise<Array<string>> => {
// Why not use simple-git here? Because for some reason it takes like 100x as long.
await execAsync("git fetch --tags");
const {stdout} = await execAsync("git tag");
return stdout.split("\n").filter(Boolean);
};
6 changes: 6 additions & 0 deletions packages/wonder-stuff-ci/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export {compareVersions} from "./compare-versions";
export {extractMobileReleaseInfoFromBranchName} from "./extract-mobile-release-info-from-branch-name";
export {getMobileReleaseTags} from "./get-mobile-release-tags";
export {getTagsFromGit} from "./get-tags-from-git";
export {execAsync} from "./exec-async";
export {bufferToString} from "./buffer-to-string";
10 changes: 10 additions & 0 deletions packages/wonder-stuff-ci/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"exclude": ["dist"],
"extends": "../tsconfig-shared.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
},
"references": [
]
}
1 change: 1 addition & 0 deletions tsconfig-build.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@
{"path": "./packages/wonder-stuff-sentry"},
{"path": "./packages/wonder-stuff-server"},
{"path": "./packages/wonder-stuff-testing"},
{"path": "./packages/wonder-stuff-ci"},
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 was curious how the tsconfig-build.json is used in wonderstuff?

What else is needed for me to get this new package published to npm?

Copy link
Member

Choose a reason for hiding this comment

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

All of it is managed by Changesets - once you changes are approved and landed then a Changesets PR will be generated, once that's approved and landed it'll publish to NPM!

],
}
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2289,6 +2289,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240"
integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==

"@types/node@^18.15.11":
version "18.15.11"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.11.tgz#b3b790f09cb1696cffcec605de025b088fa4225f"
integrity sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==

"@types/normalize-package-data@^2.4.0":
version "2.4.1"
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301"
Expand Down