Generate schema for extension pack metadata
After the upgrade to the correct types for js-yaml, the return type of `load` is correctly typed as `unknown`. This means that we can't use the return value directly, but need to validate it first. This adds such validation by generating a JSON schema for a newly created typed. The JSON schema generation is very similar to how we do it in https://github.com/github/codeql-variant-analysis-action.
This commit is contained in:
36
.github/workflows/main.yml
vendored
36
.github/workflows/main.yml
vendored
@@ -99,6 +99,42 @@ jobs:
|
||||
run: |
|
||||
npm run find-deadcode
|
||||
|
||||
generated:
|
||||
name: Check generated code
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16.17.1'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: extensions/ql-vscode/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: extensions/ql-vscode
|
||||
run: |
|
||||
npm ci
|
||||
shell: bash
|
||||
|
||||
- name: Check that repo is clean
|
||||
run: |
|
||||
git diff --exit-code
|
||||
git diff --exit-code --cached
|
||||
|
||||
- name: Generate code
|
||||
working-directory: extensions/ql-vscode
|
||||
run: |
|
||||
npm run generate
|
||||
|
||||
- name: Check for changes
|
||||
run: |
|
||||
git diff --exit-code
|
||||
git diff --exit-code --cached
|
||||
|
||||
unit-test:
|
||||
name: Unit Test
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
@@ -1842,6 +1842,8 @@
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build",
|
||||
"lint:scenarios": "ts-node scripts/lint-scenarios.ts",
|
||||
"generate": "npm-run-all -p generate:*",
|
||||
"generate:schemas": "ts-node scripts/generate-schemas.ts",
|
||||
"check-types": "find . -type f -name \"tsconfig.json\" -not -path \"./node_modules/*\" | sed -r 's|/[^/]+$||' | sort | uniq | xargs -I {} sh -c \"echo Checking types in {} && cd {} && npx tsc --noEmit\"",
|
||||
"postinstall": "patch-package",
|
||||
"prepare": "cd ../.. && husky install"
|
||||
|
||||
45
extensions/ql-vscode/scripts/generate-schemas.ts
Normal file
45
extensions/ql-vscode/scripts/generate-schemas.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { createGenerator } from "ts-json-schema-generator";
|
||||
import { join, resolve } from "path";
|
||||
import { outputJSON } from "fs-extra";
|
||||
|
||||
const extensionDirectory = resolve(__dirname, "..");
|
||||
|
||||
const schemas = [
|
||||
{
|
||||
path: join(
|
||||
extensionDirectory,
|
||||
"src",
|
||||
"model-editor",
|
||||
"extension-pack-metadata.ts",
|
||||
),
|
||||
type: "ExtensionPackMetadata",
|
||||
schemaPath: join(
|
||||
extensionDirectory,
|
||||
"src",
|
||||
"model-editor",
|
||||
"extension-pack-metadata.schema.json",
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
async function generateSchemas() {
|
||||
for (const schemaDefinition of schemas) {
|
||||
const schema = createGenerator({
|
||||
path: schemaDefinition.path,
|
||||
tsconfig: resolve(extensionDirectory, "tsconfig.json"),
|
||||
type: schemaDefinition.type,
|
||||
skipTypeCheck: true,
|
||||
topRef: true,
|
||||
additionalProperties: true,
|
||||
}).createSchema(schemaDefinition.type);
|
||||
|
||||
await outputJSON(schemaDefinition.schemaPath, schema, {
|
||||
spaces: 2,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
generateSchemas().catch((e: unknown) => {
|
||||
console.error(e);
|
||||
process.exit(2);
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$ref": "#/definitions/ExtensionPackMetadata",
|
||||
"definitions": {
|
||||
"ExtensionPackMetadata": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
"dataExtensions": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"extensionTargets": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"version",
|
||||
"dataExtensions",
|
||||
"extensionTargets"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export type ExtensionPackMetadata = {
|
||||
name: string;
|
||||
version: string;
|
||||
dataExtensions: string | string[];
|
||||
extensionTargets: Record<string, string>;
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import { join } from "path";
|
||||
import { outputFile, pathExists, readFile } from "fs-extra";
|
||||
import { dump as dumpYaml, load as loadYaml } from "js-yaml";
|
||||
import { Uri } from "vscode";
|
||||
import Ajv from "ajv";
|
||||
import { CodeQLCliServer } from "../codeql-cli/cli";
|
||||
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
|
||||
import { ProgressCallback } from "../common/vscode/progress";
|
||||
@@ -18,6 +19,12 @@ import {
|
||||
} from "./extension-pack-name";
|
||||
import { autoPickExtensionsDirectory } from "./extensions-workspace-folder";
|
||||
|
||||
import { ExtensionPackMetadata } from "./extension-pack-metadata";
|
||||
import * as extensionPackMetadataSchemaJson from "./extension-pack-metadata.schema.json";
|
||||
|
||||
const ajv = new Ajv({ allErrors: true });
|
||||
const extensionPackValidate = ajv.compile(extensionPackMetadataSchemaJson);
|
||||
|
||||
export async function pickExtensionPack(
|
||||
cliServer: Pick<CodeQLCliServer, "resolveQlpacks">,
|
||||
databaseItem: Pick<DatabaseItem, "name" | "language">,
|
||||
@@ -170,6 +177,22 @@ async function writeExtensionPack(
|
||||
return extensionPack;
|
||||
}
|
||||
|
||||
function validateExtensionPack(
|
||||
extensionPack: unknown,
|
||||
): extensionPack is ExtensionPackMetadata {
|
||||
extensionPackValidate(extensionPack);
|
||||
|
||||
if (extensionPackValidate.errors) {
|
||||
throw new Error(
|
||||
`Invalid extension pack YAML: ${extensionPackValidate.errors
|
||||
.map((error) => `${error.instancePath} ${error.message}`)
|
||||
.join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function readExtensionPack(
|
||||
path: string,
|
||||
language: string,
|
||||
@@ -188,6 +211,10 @@ async function readExtensionPack(
|
||||
throw new Error(`Could not parse ${qlpackPath}`);
|
||||
}
|
||||
|
||||
if (!validateExtensionPack(qlpack)) {
|
||||
throw new Error(`Could not validate ${qlpackPath}`);
|
||||
}
|
||||
|
||||
const dataExtensionValue = qlpack.dataExtensions;
|
||||
if (
|
||||
!(
|
||||
|
||||
@@ -363,6 +363,43 @@ describe("pickExtensionPack", () => {
|
||||
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows an error when the pack YAML does not contain name", async () => {
|
||||
const tmpDir = await dir({
|
||||
unsafeCleanup: true,
|
||||
});
|
||||
|
||||
const cliServer = mockCliServer({
|
||||
"github/vscode-codeql-java": [tmpDir.path],
|
||||
});
|
||||
|
||||
await outputFile(
|
||||
join(tmpDir.path, "codeql-pack.yml"),
|
||||
dumpYaml({
|
||||
version: "0.0.0",
|
||||
library: true,
|
||||
extensionTargets: {
|
||||
"codeql/java-all": "*",
|
||||
},
|
||||
dataExtensions: ["models/**/*.yml"],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
await pickExtensionPack(
|
||||
cliServer,
|
||||
databaseItem,
|
||||
logger,
|
||||
progress,
|
||||
maxStep,
|
||||
),
|
||||
).toEqual(undefined);
|
||||
expect(logger.showErrorMessage).toHaveBeenCalledTimes(1);
|
||||
expect(logger.showErrorMessage).toHaveBeenCalledWith(
|
||||
"Could not read extension pack github/vscode-codeql-java",
|
||||
);
|
||||
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows an error when the pack YAML does not contain dataExtensions", async () => {
|
||||
const tmpDir = await dir({
|
||||
unsafeCleanup: true,
|
||||
|
||||
Reference in New Issue
Block a user