Skip to content
This repository has been archived by the owner on Jul 2, 2024. It is now read-only.

Commit

Permalink
EVG-20677 & EVG-20868: Make type.ts validation more robust (#2038)
Browse files Browse the repository at this point in the history
  • Loading branch information
SupaJoon authored Sep 18, 2023
1 parent f14f680 commit 7cb1622
Show file tree
Hide file tree
Showing 11 changed files with 206 additions and 325 deletions.
27 changes: 13 additions & 14 deletions codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,35 @@ import path from "path";

export const getConfig = ({
generatedFileName,
schema,
silent,
}: {
schema: string;
generatedFileName: string;
}): CodegenConfig => ({
schema,
} & Pick<CodegenConfig, "silent">): CodegenConfig => ({
documents: ["./src/**/*.ts", "./src/**/*.graphql", "./src/**/*.gql"].map(
(d) => path.resolve(__dirname, d)
),
hooks: {
afterAllFileWrite: [
`${path.resolve(__dirname, "./node_modules/.bin/prettier")} --write`,
],
},
overwrite: true,
generates: {
[generatedFileName]: {
plugins: ["typescript", "typescript-operations"],
config: {
preResolveTypes: true,
arrayInputCoercion: false,
preResolveTypes: true,
scalars: {
Duration: "number",
StringMap: "{ [key: string]: any }",
Time: "Date",
Duration: "number",
},
},
plugins: ["typescript", "typescript-operations"],
},
},
hooks: {
afterAllFileWrite: [
`${path.resolve(__dirname, "./node_modules/.bin/prettier")} --write`,
],
},
overwrite: true,
schema: "sdlschema/**/*.graphql",
silent,
});

export const generatedFileName = path.resolve(
Expand All @@ -40,6 +40,5 @@ export const generatedFileName = path.resolve(
);

export default getConfig({
schema: "sdlschema/**/*.graphql",
generatedFileName,
});
2 changes: 1 addition & 1 deletion lint-staged.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ module.exports = {
"yarn prettier --parser graphql",
], // For GraphQL files, run eslint and prettier
"*.{ts,tsx}": () => "tsc -p tsconfig.json --noEmit", // For TypeScript files, run tsc
"*": () => "yarn diff-schema",
"*": () => "yarn check-schema-and-codegen",
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"deploy:prod": "env-cmd -e production ts-node scripts/deploy/run-deploy",
"deploy:do-not-use": ". ./scripts/deploy/deploy.sh",
"dev": "env-cmd -e devLocal -r .env-cmdrc.local.json yarn start",
"diff-schema": "ts-node scripts/diff-local-schema-with-remote/index.ts",
"check-schema-and-codegen": "ts-node scripts/check-schema-and-codegen/script.ts",
"eslint:fix": "yarn eslint:strict --fix",
"eslint:staged": "STRICT=1 eslint",
"eslint:strict": "STRICT=1 eslint '*.{js,ts,tsx}' 'src/**/*.ts?(x)' 'scripts/**/*.js' 'cypress/**/*.ts' 'src/gql/**/*.graphql'",
Expand Down
70 changes: 70 additions & 0 deletions scripts/check-schema-and-codegen/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import fs from "fs";
import { checkSchemaAndCodegenCore } from ".";
import { checkIsAncestor, getLatestCommitFromRemote } from "./utils";

jest.mock("fs", () => ({
readFileSync: jest.fn().mockReturnValue(Buffer.from("file-contents")),
}));
jest.mock("path", () => ({
resolve: jest.fn().mockReturnValue("{path.resolve()}"),
}));
jest.mock("./utils.ts", () => ({
canResolveDNS: jest.fn(),
getLatestCommitFromRemote: jest.fn(),
checkIsAncestor: jest.fn(),
generateTypes: jest.fn(),
}));

describe("checkSchemaAndCodegen", () => {
let consoleErrorSpy;
beforeEach(() => {
consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
(checkIsAncestor as jest.Mock).mockResolvedValue(true);
(getLatestCommitFromRemote as jest.Mock).mockResolvedValue(
"{getLatestCommitFromRemote()}"
);
});

it("returns 0 when offline", async () => {
(getLatestCommitFromRemote as jest.Mock).mockRejectedValueOnce(
new Error("TypeError: fetch failed")
);
await expect(checkSchemaAndCodegenCore()).resolves.toBe(0);
expect(consoleErrorSpy).toHaveBeenCalledWith(
"An error occured during GQL types validation: Error: TypeError: fetch failed"
);
});

it("returns 1 when checkIsAncestor is false and the files are the same", async () => {
(checkIsAncestor as jest.Mock).mockResolvedValue(false);
await expect(checkSchemaAndCodegenCore()).resolves.toBe(1);
expect(consoleErrorSpy).toHaveBeenCalledWith(
"GQL types validation failed: Your local Evergreen code is missing commit {getLatestCommitFromRemote()}. Pull Evergreen and run 'yarn codegen'."
);
});

it("returns 1 when checkIsAncestor is false and the files are different", async () => {
(checkIsAncestor as jest.Mock).mockResolvedValue(false);
(fs.readFileSync as jest.Mock)
.mockReturnValueOnce(Buffer.from("content1"))
.mockReturnValueOnce(Buffer.from("content2"));
await expect(checkSchemaAndCodegenCore()).resolves.toBe(1);
expect(consoleErrorSpy).toHaveBeenCalledWith(
"GQL types validation failed: Your local Evergreen code is missing commit {getLatestCommitFromRemote()}. Pull Evergreen and run 'yarn codegen'."
);
});

it("returns 0 when checkIsAncestor is true and the files are the same", async () => {
await expect(checkSchemaAndCodegenCore()).resolves.toBe(0);
});

it("returns 1 when checkIsAncestor returns true and the files are different", async () => {
(fs.readFileSync as jest.Mock)
.mockReturnValueOnce(Buffer.from("content1"))
.mockReturnValueOnce(Buffer.from("content2"));
await expect(checkSchemaAndCodegenCore()).resolves.toBe(1);
expect(consoleErrorSpy).toHaveBeenCalledWith(
"GQL types validation failed: Your GQL types file ({path.resolve()}) is outdated. Run 'yarn codegen'."
);
});
});
45 changes: 45 additions & 0 deletions scripts/check-schema-and-codegen/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import fs from "fs";
import process from "process";
import { generatedFileName as existingTypesFileName } from "../../codegen";
import {
checkIsAncestor,
generateTypes,
getLatestCommitFromRemote,
} from "./utils";

const failCopy = "GQL types validation failed:";

/**
* An async function that returns 1 if the local types file is outdated and 0 otherwise.
* @returns Promise<number>
*/
export const checkSchemaAndCodegenCore = async (): Promise<number> => {
try {
const commit = await getLatestCommitFromRemote();
const hasLatestCommit = await checkIsAncestor(commit);
if (!hasLatestCommit) {
console.error(
`${failCopy} Your local Evergreen code is missing commit ${commit}. Pull Evergreen and run 'yarn codegen'.`
);
return 1;
}
// Finally check to see if 'yarn codegen' was ran.
const filenames = [await generateTypes(), existingTypesFileName];
const [file1, file2] = filenames.map((filename) =>
fs.readFileSync(filename)
);
if (!file1.equals(file2)) {
console.error(
`${failCopy} Your GQL types file (${existingTypesFileName}) is outdated. Run 'yarn codegen'.`
);
return 1;
}
} catch (error) {
console.error(`An error occured during GQL types validation: ${error}`);
}
return 0;
};

export const checkSchemaAndCodegen = async () => {
process.exit(await checkSchemaAndCodegenCore());
};
3 changes: 3 additions & 0 deletions scripts/check-schema-and-codegen/script.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { checkSchemaAndCodegen } from ".";

checkSchemaAndCodegen();
73 changes: 73 additions & 0 deletions scripts/check-schema-and-codegen/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import dns from "dns";
import fs from "fs";
import os from "os";
import { generate } from "@graphql-codegen/cli";
import { execSync } from "child_process";
import process from "process";
import { getConfig } from "../../codegen";

const GITHUB_API = "https://api.github.com";
const GQL_DIR = "graphql/schema";
const LOCAL_SCHEMA = "sdlschema";
const REPO = "/repos/evergreen-ci/evergreen";

/**
* Get the latest commit that was made to the GQL folder of the remote Evergreen repository.
* @returns A Promise that resolves to the SHA of the latest commit.
* @throws {Error} When failed to fetch commits.
*/
export const getLatestCommitFromRemote = async (): Promise<string> => {
const url = `${GITHUB_API}${REPO}/commits?path=${GQL_DIR}&sha=main`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch ${url}. Status: ${response.status}`);
}

const commits = await response.json();

if (commits.length > 0) {
return commits[0].sha;
}
throw new Error(`No commits found for this path: ${url}`);
};

/**
* Check if the local Evergreen repo contains a given commit.
* @param commit The commit string that will be checked to see if it exists in the Evergreen repo.
* @returns A Promise that resolves to true if local repo contains the given commit, and false otherwise.
* @throws {Error} When an error occurs while executing the command.
*/
export const checkIsAncestor = async (commit: string): Promise<boolean> => {
const localSchemaSymlink = fs.readlinkSync(LOCAL_SCHEMA);
const originalDir = process.cwd();
try {
process.chdir(localSchemaSymlink);
execSync(`git merge-base --is-ancestor ${commit} HEAD`);
process.chdir(originalDir);
return true;
} catch (error) {
process.chdir(originalDir);
// Error status 1 and 128 means that the commit is not an anecestor and the user must fetch.
// Error code docs: https://www.git-scm.com/docs/api-error-handling/
if (error.status === 1 || error.status === 128) {
return false;
}
throw new Error(`Error checking ancestor: ${error.message}`);
}
};

/**
* Generate types based on sdlschema.
* @returns A Promise that resolves to the path of the generated file.
*/
export const generateTypes = async (): Promise<string> => {
const generatedFileName = `${os.tmpdir()}/types.ts`;
await generate(
getConfig({
generatedFileName,
silent: true,
}),
true
);
return generatedFileName;
};
94 changes: 0 additions & 94 deletions scripts/diff-local-schema-with-remote/diffTypes.test.ts

This file was deleted.

Loading

0 comments on commit 7cb1622

Please sign in to comment.