diff --git a/README.md b/README.md index e37d02b5..a03a9c10 100644 --- a/README.md +++ b/README.md @@ -429,6 +429,7 @@ This is the current list: |   |:white_check_mark:| BN012 | unreferrenced Target Endpoints | Check that each TargetEndpoint can be reached. | |   |:white_check_mark:| BN013 | Unreferenced resources. | Warn for resources that not referenced in any policy. Unreferenced resources are dead code. | |   |:white_check_mark:| BN014 | Duplicate policies. | Warn if there are identically configured, if differently named, policies. | +|   |:white_check_mark:| BN015 | Manifest file exists. | The bundle root must contain the manifest .xml file (eg. apiproxy.xml or sharedflowbundle.xml); without it, the bundle cannot be imported into Apigee. | | Proxy Definition |   |   |   |   | |   |:white_check_mark:| PD001 | RouteRules to Targets | RouteRules should map to defined Targets. | |   |:white_check_mark:| PD002 | Unreachable Route Rules - defaults | Only one RouteRule should be present without a condition. | diff --git a/lib/package/Bundle.js b/lib/package/Bundle.js index 887a8d74..3cda7d59 100644 --- a/lib/package/Bundle.js +++ b/lib/package/Bundle.js @@ -168,7 +168,7 @@ function processFileSystem(config, bundle, cb) { ); } - bundle.filePath = bundle.root + "/" + files[0]; + bundle.filePath = files.length ? bundle.root + "/" + files[0] : undefined; bundle.proxyRoot = bundle.root; const bundleTypeName = config.source.bundleType; diff --git a/lib/package/plugins/BN015-checkManifestExists.js b/lib/package/plugins/BN015-checkManifestExists.js new file mode 100644 index 00000000..49049471 --- /dev/null +++ b/lib/package/plugins/BN015-checkManifestExists.js @@ -0,0 +1,52 @@ +/* + Copyright © 2026 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +const ruleId = require("../lintUtil.js").getRuleId(), + debug = require("debug")("apigeelint:" + ruleId); + +const plugin = { + ruleId, + name: "Manifest file exists", + message: + "The bundle root must contain exactly one manifest .xml file (eg. apiproxy.xml or sharedflowbundle.xml). Without it the bundle cannot be imported into Apigee.", + fatal: false, + severity: 2, //1 == warn 2 == error + nodeType: "Bundle", + enabled: true, +}; + +const onBundle = function (bundle, cb) { + let flagged = false; + // Bundle.js sets bundle.filePath only when exactly one .xml file is + // found at the bundle root. When the manifest is missing entirely, + // filePath is left undefined. + if (!bundle.filePath) { + debug(`No manifest file found in bundle root: ${bundle.root}`); + bundle.addMessage({ + plugin, + message: `Missing manifest file: no .xml file found at bundle root "${bundle.root}".`, + }); + flagged = true; + } + if (typeof cb == "function") { + cb(null, flagged); + } +}; + +module.exports = { + plugin, + onBundle, +}; diff --git a/test/fixtures/resources/missingManifest/apiproxy/proxies/default.xml b/test/fixtures/resources/missingManifest/apiproxy/proxies/default.xml new file mode 100644 index 00000000..30266f6c --- /dev/null +++ b/test/fixtures/resources/missingManifest/apiproxy/proxies/default.xml @@ -0,0 +1,20 @@ + + + / + + secure + + + + + + + + + + + + + + + diff --git a/test/specs/BN015-checkManifestExists.js b/test/specs/BN015-checkManifestExists.js new file mode 100644 index 00000000..3ce61355 --- /dev/null +++ b/test/specs/BN015-checkManifestExists.js @@ -0,0 +1,79 @@ +/* + Copyright © 2026 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +const assert = require("node:assert"), + path = require("node:path"), + testID = "BN015", + debug = require("debug")("apigeelint:" + testID), + bl = require("../../lib/package/bundleLinter.js"); + +describe("BN015 - Check manifest file exists", function () { + it("should flag a bundle that is missing the manifest .xml file", () => { + const configuration = { + debug: true, + source: { + type: "filesystem", + path: path.resolve( + __dirname, + "../fixtures/resources/missingManifest/apiproxy", + ), + bundleType: "apiproxy", + }, + excluded: {}, + setExitCode: false, + output: () => {}, // suppress output + }; + + bl.lint(configuration, (bundle) => { + const items = bundle.getReport(); + assert.ok(items); + assert.ok(items.length); + const bn015Messages = items[0].messages.filter( + (m) => m.ruleId == testID, + ); + assert.equal(bn015Messages.length, 1); + assert.equal(bn015Messages[0].severity, 2); + debug(`bn015Messages: ${JSON.stringify(bn015Messages)}`); + }); + }); + + it("should not flag a bundle that has a manifest .xml file", () => { + const configuration = { + debug: true, + source: { + type: "filesystem", + path: path.resolve( + __dirname, + "../fixtures/resources/sampleProxy/24Solver/apiproxy", + ), + bundleType: "apiproxy", + }, + excluded: {}, + setExitCode: false, + output: () => {}, // suppress output + }; + + bl.lint(configuration, (bundle) => { + const items = bundle.getReport(); + assert.ok(items); + assert.ok(items.length); + const bn015Messages = items[0].messages.filter( + (m) => m.ruleId == testID, + ); + assert.equal(bn015Messages.length, 0); + }); + }); +}); diff --git a/test/specs/testBundleEx.js b/test/specs/testBundleEx.js index 511aa2c5..da315f13 100644 --- a/test/specs/testBundleEx.js +++ b/test/specs/testBundleEx.js @@ -80,6 +80,24 @@ describe("BundleEx", function () { assert.ok(missingFolderMsg, "Should warn about missing apiproxy folder"); }); + it("Should leave filePath undefined when the manifest .xml file is missing", function () { + const missingManifestPath = path.resolve( + __dirname, + "../fixtures/resources/missingManifest/apiproxy", + ); + const configuration = { + source: { + type: "filesystem", + path: missingManifestPath, + bundleType: "apiproxy", + }, + }; + + const bundle = new Bundle(configuration); + assert.ok(bundle); + assert.equal(bundle.filePath, undefined); + }); + it("Should handle messages without a plugin object", function () { const configuration = { source: {