diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index 079ac14..03b68e4 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -283,6 +283,24 @@ describe('action', () => { } }); + it('fails if schema missing $schema key', async () => { + mockGetBooleanInput({}); + mockGetInput({ schema }); + mockGetMultilineInput({ files }); + + jest + .mocked(fs.readFile) + .mockResolvedValueOnce(schemaContents.replace('$schema', '_schema')); + + await main.run(); + expect(runSpy).toHaveReturned(); + expect(process.exitCode).not.toBeDefined(); + + expect(core.setFailed).toHaveBeenLastCalledWith( + 'JSON schema missing $schema key' + ); + }); + it('fails if no files to validate', async () => { mockGetBooleanInput({}); mockGetInput({ schema }); @@ -377,4 +395,28 @@ describe('action', () => { expect(core.debug).toHaveBeenCalledWith(`𐄂 ${paths[0]} is not valid`); expect(core.debug).toHaveBeenCalledWith(`✓ ${paths[1]} is valid`); }); + + it('supports JSON Schema draft-04', async () => { + mockGetBooleanInput({}); + mockGetInput({ schema }); + mockGetMultilineInput({ files }); + + jest + .mocked(fs.readFile) + .mockResolvedValueOnce( + schemaContents.replace( + 'http://json-schema.org/draft-07/schema#', + 'http://json-schema.org/draft-04/schema#' + ) + ) + .mockResolvedValueOnce(instanceContents); + mockGlobGenerator(['/foo/bar/baz/config.yml']); + + await main.run(); + expect(runSpy).toHaveReturned(); + expect(process.exitCode).not.toBeDefined(); + + expect(core.setOutput).toHaveBeenCalledTimes(1); + expect(core.setOutput).toHaveBeenLastCalledWith('valid', true); + }); }); diff --git a/dist/index.js b/dist/index.js index 6f231e9..fff098f 100644 --- a/dist/index.js +++ b/dist/index.js @@ -45230,6 +45230,225 @@ exports.VERSION = '1.7.0'; /***/ }), +/***/ 7023: +/***/ ((module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.CodeGen = exports.Name = exports.nil = exports.stringify = exports.str = exports._ = exports.KeywordCxt = void 0; +const core_1 = __nccwpck_require__(2685); +const draft4_1 = __nccwpck_require__(2589); +const discriminator_1 = __nccwpck_require__(4025); +const draft4MetaSchema = __nccwpck_require__(8978); +const META_SUPPORT_DATA = ["/properties"]; +const META_SCHEMA_ID = "http://json-schema.org/draft-04/schema"; +class Ajv extends core_1.default { + constructor(opts = {}) { + super({ + ...opts, + schemaId: "id", + }); + } + _addVocabularies() { + super._addVocabularies(); + draft4_1.default.forEach((v) => this.addVocabulary(v)); + if (this.opts.discriminator) + this.addKeyword(discriminator_1.default); + } + _addDefaultMetaSchema() { + super._addDefaultMetaSchema(); + if (!this.opts.meta) + return; + const metaSchema = this.opts.$data + ? this.$dataMetaSchema(draft4MetaSchema, META_SUPPORT_DATA) + : draft4MetaSchema; + this.addMetaSchema(metaSchema, META_SCHEMA_ID, false); + this.refs["http://json-schema.org/schema"] = META_SCHEMA_ID; + } + defaultMeta() { + return (this.opts.defaultMeta = + super.defaultMeta() || (this.getSchema(META_SCHEMA_ID) ? META_SCHEMA_ID : undefined)); + } +} +module.exports = exports = Ajv; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports["default"] = Ajv; +var core_2 = __nccwpck_require__(2685); +Object.defineProperty(exports, "KeywordCxt", ({ enumerable: true, get: function () { return core_2.KeywordCxt; } })); +var core_3 = __nccwpck_require__(2685); +Object.defineProperty(exports, "_", ({ enumerable: true, get: function () { return core_3._; } })); +Object.defineProperty(exports, "str", ({ enumerable: true, get: function () { return core_3.str; } })); +Object.defineProperty(exports, "stringify", ({ enumerable: true, get: function () { return core_3.stringify; } })); +Object.defineProperty(exports, "nil", ({ enumerable: true, get: function () { return core_3.nil; } })); +Object.defineProperty(exports, "Name", ({ enumerable: true, get: function () { return core_3.Name; } })); +Object.defineProperty(exports, "CodeGen", ({ enumerable: true, get: function () { return core_3.CodeGen; } })); +//# sourceMappingURL=index.js.map + +/***/ }), + +/***/ 4935: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +const ref_1 = __nccwpck_require__(6532); +const core = [ + "$schema", + "id", + "$defs", + { keyword: "$comment" }, + "definitions", + ref_1.default, +]; +exports["default"] = core; +//# sourceMappingURL=core.js.map + +/***/ }), + +/***/ 2589: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +const core_1 = __nccwpck_require__(4935); +const validation_1 = __nccwpck_require__(3035); +const applicator_1 = __nccwpck_require__(3048); +const format_1 = __nccwpck_require__(9841); +const metadataVocabulary = ["title", "description", "default"]; +const draft4Vocabularies = [ + core_1.default, + validation_1.default, + applicator_1.default(), + format_1.default, + metadataVocabulary, +]; +exports["default"] = draft4Vocabularies; +//# sourceMappingURL=draft4.js.map + +/***/ }), + +/***/ 3035: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +const limitNumber_1 = __nccwpck_require__(45); +const limitNumberExclusive_1 = __nccwpck_require__(2674); +const multipleOf_1 = __nccwpck_require__(3201); +const limitLength_1 = __nccwpck_require__(7598); +const pattern_1 = __nccwpck_require__(4960); +const limitProperties_1 = __nccwpck_require__(3470); +const required_1 = __nccwpck_require__(3602); +const limitItems_1 = __nccwpck_require__(3924); +const uniqueItems_1 = __nccwpck_require__(9351); +const const_1 = __nccwpck_require__(3694); +const enum_1 = __nccwpck_require__(5529); +const validation = [ + // number + limitNumber_1.default, + limitNumberExclusive_1.default, + multipleOf_1.default, + // string + limitLength_1.default, + pattern_1.default, + // object + limitProperties_1.default, + required_1.default, + // array + limitItems_1.default, + uniqueItems_1.default, + // any + { keyword: "type", schemaType: ["string", "array"] }, + { keyword: "nullable", schemaType: "boolean" }, + const_1.default, + enum_1.default, +]; +exports["default"] = validation; +//# sourceMappingURL=index.js.map + +/***/ }), + +/***/ 45: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +const core_1 = __nccwpck_require__(2685); +const codegen_1 = __nccwpck_require__(9179); +const ops = codegen_1.operators; +const KWDs = { + maximum: { + exclusive: "exclusiveMaximum", + ops: [ + { okStr: "<=", ok: ops.LTE, fail: ops.GT }, + { okStr: "<", ok: ops.LT, fail: ops.GTE }, + ], + }, + minimum: { + exclusive: "exclusiveMinimum", + ops: [ + { okStr: ">=", ok: ops.GTE, fail: ops.LT }, + { okStr: ">", ok: ops.GT, fail: ops.LTE }, + ], + }, +}; +const error = { + message: (cxt) => core_1.str `must be ${kwdOp(cxt).okStr} ${cxt.schemaCode}`, + params: (cxt) => core_1._ `{comparison: ${kwdOp(cxt).okStr}, limit: ${cxt.schemaCode}}`, +}; +const def = { + keyword: Object.keys(KWDs), + type: "number", + schemaType: "number", + $data: true, + error, + code(cxt) { + const { data, schemaCode } = cxt; + cxt.fail$data(core_1._ `${data} ${kwdOp(cxt).fail} ${schemaCode} || isNaN(${data})`); + }, +}; +function kwdOp(cxt) { + var _a; + const keyword = cxt.keyword; + const opsIdx = ((_a = cxt.parentSchema) === null || _a === void 0 ? void 0 : _a[KWDs[keyword].exclusive]) ? 1 : 0; + return KWDs[keyword].ops[opsIdx]; +} +exports["default"] = def; +//# sourceMappingURL=limitNumber.js.map + +/***/ }), + +/***/ 2674: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +const KWDs = { + exclusiveMaximum: "maximum", + exclusiveMinimum: "minimum", +}; +const def = { + keyword: Object.keys(KWDs), + type: "number", + schemaType: "boolean", + code({ keyword, parentSchema }) { + const limitKwd = KWDs[keyword]; + if (parentSchema[limitKwd] === undefined) { + throw new Error(`${keyword} can only be used with ${limitKwd}`); + } + }, +}; +exports["default"] = def; +//# sourceMappingURL=limitNumberExclusive.js.map + +/***/ }), + /***/ 407: /***/ ((__unused_webpack_module, exports) => { @@ -89404,6 +89623,7 @@ const core = __importStar(__nccwpck_require__(2186)); const glob = __importStar(__nccwpck_require__(8090)); const http = __importStar(__nccwpck_require__(6255)); const _2019_1 = __importDefault(__nccwpck_require__(5988)); +const ajv_draft_04_1 = __importDefault(__nccwpck_require__(7023)); const ajv_formats_1 = __importDefault(__nccwpck_require__(567)); const yaml = __importStar(__nccwpck_require__(4083)); /** @@ -89416,6 +89636,7 @@ async function run() { const files = core.getMultilineInput('files', { required: true }); const cacheRemoteSchema = core.getBooleanInput('cache-remote-schema'); const failOnInvalid = core.getBooleanInput('fail-on-invalid'); + // TODO - Handle special case where schemaPath === 'json-schema', then use ajv.validateSchema // Fetch and cache remote schemas if (schemaPath.startsWith('http://') || schemaPath.startsWith('https://')) { const schemaUrl = schemaPath; @@ -89454,12 +89675,19 @@ async function run() { } } // Load and compile the schema - const schema = yaml.parse(await fs.readFile(schemaPath, 'utf-8')); - const ajv = new _2019_1.default(); - /* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */ - ajv.addMetaSchema(__nccwpck_require__(18)); - ajv.addMetaSchema(__nccwpck_require__(98)); - /* eslint-enable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */ + const schema = JSON.parse(await fs.readFile(schemaPath, 'utf-8')); + if (typeof schema.$schema !== 'string') { + core.setFailed('JSON schema missing $schema key'); + return; + } + const draft04Schema = schema.$schema === 'http://json-schema.org/draft-04/schema#'; + const ajv = draft04Schema ? new ajv_draft_04_1.default() : new _2019_1.default(); + if (!draft04Schema) { + /* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */ + ajv.addMetaSchema(__nccwpck_require__(18)); + ajv.addMetaSchema(__nccwpck_require__(98)); + /* eslint-enable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */ + } const validate = (0, ajv_formats_1.default)(ajv).compile(schema); let valid = true; let filesValidated = false; @@ -91412,7 +91640,7 @@ var YAMLMap = __nccwpck_require__(6011); var YAMLSeq = __nccwpck_require__(5161); var resolveBlockMap = __nccwpck_require__(2986); var resolveBlockSeq = __nccwpck_require__(2289); -var resolveFlowCollection = __nccwpck_require__(45); +var resolveFlowCollection = __nccwpck_require__(5488); function resolveCollection(CN, ctx, token, onError, tagName, tag) { const coll = token.type === 'block-map' @@ -92387,7 +92615,7 @@ exports.resolveEnd = resolveEnd; /***/ }), -/***/ 45: +/***/ 5488: /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { "use strict"; @@ -99821,6 +100049,14 @@ exports.visit = visit; exports.visitAsync = visitAsync; +/***/ }), + +/***/ 8978: +/***/ ((module) => { + +"use strict"; +module.exports = JSON.parse('{"id":"http://json-schema.org/draft-04/schema#","$schema":"http://json-schema.org/draft-04/schema#","description":"Core schema meta-schema","definitions":{"schemaArray":{"type":"array","minItems":1,"items":{"$ref":"#"}},"positiveInteger":{"type":"integer","minimum":0},"positiveIntegerDefault0":{"allOf":[{"$ref":"#/definitions/positiveInteger"},{"default":0}]},"simpleTypes":{"enum":["array","boolean","integer","null","number","object","string"]},"stringArray":{"type":"array","items":{"type":"string"},"minItems":1,"uniqueItems":true}},"type":"object","properties":{"id":{"type":"string","format":"uri"},"$schema":{"type":"string","format":"uri"},"title":{"type":"string"},"description":{"type":"string"},"default":{},"multipleOf":{"type":"number","minimum":0,"exclusiveMinimum":true},"maximum":{"type":"number"},"exclusiveMaximum":{"type":"boolean","default":false},"minimum":{"type":"number"},"exclusiveMinimum":{"type":"boolean","default":false},"maxLength":{"$ref":"#/definitions/positiveInteger"},"minLength":{"$ref":"#/definitions/positiveIntegerDefault0"},"pattern":{"type":"string","format":"regex"},"additionalItems":{"anyOf":[{"type":"boolean"},{"$ref":"#"}],"default":{}},"items":{"anyOf":[{"$ref":"#"},{"$ref":"#/definitions/schemaArray"}],"default":{}},"maxItems":{"$ref":"#/definitions/positiveInteger"},"minItems":{"$ref":"#/definitions/positiveIntegerDefault0"},"uniqueItems":{"type":"boolean","default":false},"maxProperties":{"$ref":"#/definitions/positiveInteger"},"minProperties":{"$ref":"#/definitions/positiveIntegerDefault0"},"required":{"$ref":"#/definitions/stringArray"},"additionalProperties":{"anyOf":[{"type":"boolean"},{"$ref":"#"}],"default":{}},"definitions":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"properties":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"patternProperties":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"dependencies":{"type":"object","additionalProperties":{"anyOf":[{"$ref":"#"},{"$ref":"#/definitions/stringArray"}]}},"enum":{"type":"array","minItems":1,"uniqueItems":true},"type":{"anyOf":[{"$ref":"#/definitions/simpleTypes"},{"type":"array","items":{"$ref":"#/definitions/simpleTypes"},"minItems":1,"uniqueItems":true}]},"allOf":{"$ref":"#/definitions/schemaArray"},"anyOf":{"$ref":"#/definitions/schemaArray"},"oneOf":{"$ref":"#/definitions/schemaArray"},"not":{"$ref":"#"}},"dependencies":{"exclusiveMaximum":["maximum"],"exclusiveMinimum":["minimum"]},"default":{}}'); + /***/ }), /***/ 4775: diff --git a/dist/licenses.txt b/dist/licenses.txt index 39fbe7a..71369f8 100644 --- a/dist/licenses.txt +++ b/dist/licenses.txt @@ -571,6 +571,31 @@ SOFTWARE. +ajv-draft-04 +MIT +MIT License + +Copyright (c) 2021 Evgeny Poberezkin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + ajv-formats MIT MIT License diff --git a/package-lock.json b/package-lock.json index 83fb35f..40a360f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@actions/glob": "^0.4.0", "@actions/http-client": "^2.2.0", "ajv": "^8.12.0", + "ajv-draft-04": "^1.0.0", "ajv-formats": "^2.1.1", "yaml": "^2.3.4" }, @@ -2048,6 +2049,19 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/ajv-formats": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", diff --git a/package.json b/package.json index 8f59ab5..9a90395 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "@actions/glob": "^0.4.0", "@actions/http-client": "^2.2.0", "ajv": "^8.12.0", + "ajv-draft-04": "^1.0.0", "ajv-formats": "^2.1.1", "yaml": "^2.3.4" }, diff --git a/src/main.ts b/src/main.ts index 982447b..61bdb33 100644 --- a/src/main.ts +++ b/src/main.ts @@ -8,6 +8,7 @@ import * as glob from '@actions/glob'; import * as http from '@actions/http-client'; import Ajv2019 from 'ajv/dist/2019'; +import AjvDraft04 from 'ajv-draft-04'; import AjvFormats from 'ajv-formats'; import * as yaml from 'yaml'; @@ -72,12 +73,27 @@ export async function run(): Promise { } // Load and compile the schema - const schema = yaml.parse(await fs.readFile(schemaPath, 'utf-8')); - const ajv = new Ajv2019(); - /* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */ - ajv.addMetaSchema(require('ajv/dist/refs/json-schema-draft-06.json')); - ajv.addMetaSchema(require('ajv/dist/refs/json-schema-draft-07.json')); - /* eslint-enable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */ + const schema: Record = JSON.parse( + await fs.readFile(schemaPath, 'utf-8') + ); + + if (typeof schema.$schema !== 'string') { + core.setFailed('JSON schema missing $schema key'); + return; + } + + const draft04Schema = + schema.$schema === 'http://json-schema.org/draft-04/schema#'; + + const ajv = draft04Schema ? new AjvDraft04() : new Ajv2019(); + + if (!draft04Schema) { + /* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */ + ajv.addMetaSchema(require('ajv/dist/refs/json-schema-draft-06.json')); + ajv.addMetaSchema(require('ajv/dist/refs/json-schema-draft-07.json')); + /* eslint-enable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */ + } + const validate = AjvFormats(ajv).compile(schema); let valid = true;