@backstage-community/plugin-code-coverage-backend 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,97 @@
1
+ 'use strict';
2
+
3
+ class Lcov {
4
+ constructor(logger) {
5
+ this.logger = logger;
6
+ this.logger = logger;
7
+ }
8
+ /**
9
+ * convert lcov into shared json coverage format
10
+ *
11
+ * @param raw - raw lcov report
12
+ * @param scmFiles - list of files that are committed to SCM
13
+ */
14
+ convert(raw, scmFiles) {
15
+ const lines = raw.split(/\r?\n/);
16
+ const jscov = [];
17
+ let currentFile = null;
18
+ lines.forEach((line) => {
19
+ const [section, value] = line.split(":");
20
+ switch (section) {
21
+ // If the line starts with SF, it's a new file
22
+ case "SF":
23
+ currentFile = this.processNewFile(value, scmFiles);
24
+ break;
25
+ // If the line starts with DA, it's a line hit
26
+ case "DA":
27
+ this.processLineHit(currentFile, value);
28
+ break;
29
+ // If the line starts with BRDA, it's a branch line
30
+ case "BRDA":
31
+ this.processBranchHit(currentFile, value);
32
+ break;
33
+ // If the line starts with end_of_record, it's the end of current file
34
+ case "end_of_record":
35
+ if (currentFile) {
36
+ jscov.push(currentFile);
37
+ currentFile = null;
38
+ }
39
+ break;
40
+ }
41
+ });
42
+ return jscov;
43
+ }
44
+ /**
45
+ * Parses a new file entry
46
+ *
47
+ * @param file - file name from coverage report
48
+ * @param scmFiles - list of files that are committed to SCM
49
+ */
50
+ processNewFile(file, scmFiles) {
51
+ const filename = scmFiles.map((f) => f.trimEnd()).find((f) => file.endsWith(f));
52
+ this.logger.debug(`matched ${file} to ${filename}`);
53
+ if (scmFiles.length === 0 || filename) {
54
+ return {
55
+ filename: filename || file,
56
+ lineHits: {},
57
+ branchHits: {}
58
+ };
59
+ }
60
+ return null;
61
+ }
62
+ /**
63
+ * Parses line coverage information
64
+ *
65
+ * @param currentFile - current file entry
66
+ * @param value - line coverage information
67
+ */
68
+ processLineHit(currentFile, value) {
69
+ if (!currentFile) return;
70
+ const [lineNumber, hits] = value.split(",");
71
+ currentFile.lineHits[Number(lineNumber)] = Number(hits);
72
+ }
73
+ /**
74
+ * Parses branch coverage information
75
+ *
76
+ * @param currentFile - current file entry
77
+ * @param value - branch coverage information
78
+ */
79
+ processBranchHit(currentFile, value) {
80
+ if (!currentFile) return;
81
+ const [lineNumber, , , hits] = value.split(",");
82
+ const lineNumberNum = Number(lineNumber);
83
+ const isHit = Number(hits) > 0;
84
+ const branch = currentFile.branchHits[lineNumberNum] || {
85
+ covered: 0,
86
+ available: 0,
87
+ missed: 0
88
+ };
89
+ branch.available++;
90
+ branch.covered += isHit ? 1 : 0;
91
+ branch.missed += isHit ? 0 : 1;
92
+ currentFile.branchHits[lineNumberNum] = branch;
93
+ }
94
+ }
95
+
96
+ exports.Lcov = Lcov;
97
+ //# sourceMappingURL=lcov.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lcov.cjs.js","sources":["../../../src/service/converter/lcov.ts"],"sourcesContent":["/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { FileEntry } from '../types';\nimport { Converter } from './Converter';\nimport { LoggerService } from '@backstage/backend-plugin-api';\n\nexport class Lcov implements Converter {\n constructor(readonly logger: LoggerService) {\n this.logger = logger;\n }\n\n /**\n * convert lcov into shared json coverage format\n *\n * @param raw - raw lcov report\n * @param scmFiles - list of files that are committed to SCM\n */\n convert(raw: string, scmFiles: string[]): FileEntry[] {\n const lines = raw.split(/\\r?\\n/);\n const jscov: Array<FileEntry> = [];\n let currentFile: FileEntry | null = null;\n\n lines.forEach(line => {\n const [section, value] = line.split(':');\n\n switch (section) {\n // If the line starts with SF, it's a new file\n case 'SF':\n currentFile = this.processNewFile(value, scmFiles);\n break;\n // If the line starts with DA, it's a line hit\n case 'DA':\n this.processLineHit(currentFile, value);\n break;\n // If the line starts with BRDA, it's a branch line\n case 'BRDA':\n this.processBranchHit(currentFile, value);\n break;\n // If the line starts with end_of_record, it's the end of current file\n case 'end_of_record':\n if (currentFile) {\n jscov.push(currentFile);\n currentFile = null;\n }\n break;\n default:\n break;\n }\n });\n\n return jscov;\n }\n\n /**\n * Parses a new file entry\n *\n * @param file - file name from coverage report\n * @param scmFiles - list of files that are committed to SCM\n */\n private processNewFile(file: string, scmFiles: string[]): FileEntry | null {\n const filename = scmFiles.map(f => f.trimEnd()).find(f => file.endsWith(f));\n\n this.logger.debug(`matched ${file} to ${filename}`);\n\n if (scmFiles.length === 0 || filename) {\n return {\n filename: filename || file,\n lineHits: {},\n branchHits: {},\n };\n }\n\n return null;\n }\n\n /**\n * Parses line coverage information\n *\n * @param currentFile - current file entry\n * @param value - line coverage information\n */\n private processLineHit(currentFile: FileEntry | null, value: string): void {\n if (!currentFile) return;\n\n const [lineNumber, hits] = value.split(',');\n currentFile.lineHits[Number(lineNumber)] = Number(hits);\n }\n\n /**\n * Parses branch coverage information\n *\n * @param currentFile - current file entry\n * @param value - branch coverage information\n */\n private processBranchHit(currentFile: FileEntry | null, value: string): void {\n if (!currentFile) return;\n\n const [lineNumber, , , hits] = value.split(',');\n const lineNumberNum = Number(lineNumber);\n const isHit = Number(hits) > 0;\n const branch = currentFile.branchHits[lineNumberNum] || {\n covered: 0,\n available: 0,\n missed: 0,\n };\n\n branch.available++;\n branch.covered += isHit ? 1 : 0;\n branch.missed += isHit ? 0 : 1;\n\n currentFile.branchHits[lineNumberNum] = branch;\n }\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,196 @@
1
+ 'use strict';
2
+
3
+ var express = require('express');
4
+ var Router = require('express-promise-router');
5
+ var BodyParser = require('body-parser');
6
+ var bodyParserXml = require('body-parser-xml');
7
+ var catalogClient = require('@backstage/catalog-client');
8
+ var backendCommon = require('@backstage/backend-common');
9
+ var errors = require('@backstage/errors');
10
+ var integration = require('@backstage/integration');
11
+ var CodeCoverageDatabase = require('./CodeCoverageDatabase.cjs.js');
12
+ var CoverageUtils = require('./CoverageUtils.cjs.js');
13
+ var cobertura = require('./converter/cobertura.cjs.js');
14
+ var jacoco = require('./converter/jacoco.cjs.js');
15
+ var lcov = require('./converter/lcov.cjs.js');
16
+ var catalogModel = require('@backstage/catalog-model');
17
+ var rootHttpRouter = require('@backstage/backend-defaults/rootHttpRouter');
18
+
19
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
20
+
21
+ var express__default = /*#__PURE__*/_interopDefaultCompat(express);
22
+ var Router__default = /*#__PURE__*/_interopDefaultCompat(Router);
23
+ var BodyParser__default = /*#__PURE__*/_interopDefaultCompat(BodyParser);
24
+ var bodyParserXml__default = /*#__PURE__*/_interopDefaultCompat(bodyParserXml);
25
+
26
+ const makeRouter = async (options) => {
27
+ const { config, logger, discovery, database, urlReader } = options;
28
+ const codeCoverageDatabase = await CodeCoverageDatabase.CodeCoverageDatabase.create(database);
29
+ const codecovUrl = await discovery.getExternalBaseUrl("code-coverage");
30
+ const catalogApi = options.catalogApi ?? new catalogClient.CatalogClient({ discoveryApi: discovery });
31
+ const scm = integration.ScmIntegrations.fromConfig(config);
32
+ const { auth, httpAuth } = backendCommon.createLegacyAuthAdapters(options);
33
+ const bodySizeLimit = config.getOptionalString("codeCoverage.bodySizeLimit") ?? "100kb";
34
+ bodyParserXml__default.default(BodyParser__default.default);
35
+ const router = Router__default.default();
36
+ router.use(
37
+ BodyParser__default.default.xml({
38
+ limit: bodySizeLimit
39
+ })
40
+ );
41
+ router.use(
42
+ BodyParser__default.default.text({
43
+ limit: bodySizeLimit
44
+ })
45
+ );
46
+ router.use(express__default.default.json());
47
+ const utils = new CoverageUtils.CoverageUtils(scm, urlReader);
48
+ router.get("/health", async (_req, res) => {
49
+ res.status(200).json({ status: "ok" });
50
+ });
51
+ router.get("/report", async (req, res) => {
52
+ const { entity } = req.query;
53
+ const entityLookup = await catalogApi.getEntityByRef(
54
+ entity,
55
+ await auth.getPluginRequestToken({
56
+ onBehalfOf: await httpAuth.credentials(req),
57
+ targetPluginId: "catalog"
58
+ })
59
+ );
60
+ if (!entityLookup) {
61
+ throw new errors.NotFoundError(`No entity found matching ${entity}`);
62
+ }
63
+ const stored = await codeCoverageDatabase.getCodeCoverage(entity);
64
+ const aggregate = CoverageUtils.aggregateCoverage(stored);
65
+ res.status(200).json({
66
+ ...stored,
67
+ aggregate: {
68
+ line: aggregate.line,
69
+ branch: aggregate.branch
70
+ }
71
+ });
72
+ });
73
+ router.get("/history", async (req, res) => {
74
+ const { entity } = req.query;
75
+ const entityLookup = await catalogApi.getEntityByRef(
76
+ entity,
77
+ await auth.getPluginRequestToken({
78
+ onBehalfOf: await httpAuth.credentials(req),
79
+ targetPluginId: "catalog"
80
+ })
81
+ );
82
+ if (!entityLookup) {
83
+ throw new errors.NotFoundError(`No entity found matching ${entity}`);
84
+ }
85
+ const { limit } = req.query;
86
+ const history = await codeCoverageDatabase.getHistory(
87
+ entity,
88
+ parseInt(limit?.toString() || "10", 10)
89
+ );
90
+ res.status(200).json(history);
91
+ });
92
+ router.get("/file-content", async (req, res) => {
93
+ const { entity, path } = req.query;
94
+ const entityLookup = await catalogApi.getEntityByRef(
95
+ entity,
96
+ await auth.getPluginRequestToken({
97
+ onBehalfOf: await httpAuth.credentials(req),
98
+ targetPluginId: "catalog"
99
+ })
100
+ );
101
+ if (!entityLookup) {
102
+ throw new errors.NotFoundError(`No entity found matching ${entity}`);
103
+ }
104
+ if (!path) {
105
+ throw new errors.InputError("Need path query parameter");
106
+ }
107
+ const sourceLocation = catalogModel.getEntitySourceLocation(entityLookup);
108
+ if (!sourceLocation) {
109
+ throw new errors.InputError(
110
+ `No "backstage.io/source-location" annotation on entity ${entity}`
111
+ );
112
+ }
113
+ const vcs = scm.byUrl(sourceLocation.target);
114
+ if (!vcs) {
115
+ throw new errors.InputError(`Unable to determine SCM from ${sourceLocation}`);
116
+ }
117
+ const scmTree = await urlReader.readTree(sourceLocation.target);
118
+ const scmFile = (await scmTree.files()).find((f) => f.path === path);
119
+ if (!scmFile) {
120
+ res.status(404).json({
121
+ message: "Couldn't find file in SCM",
122
+ file: path,
123
+ scm: vcs.title
124
+ });
125
+ return;
126
+ }
127
+ const content = await scmFile?.content();
128
+ if (!content) {
129
+ res.status(400).json({
130
+ message: "Couldn't process content of file in SCM",
131
+ file: path,
132
+ scm: vcs.title
133
+ });
134
+ return;
135
+ }
136
+ const data = content.toString();
137
+ res.status(200).contentType("text/plain").send(data);
138
+ });
139
+ router.post("/report", async (req, res) => {
140
+ const { entity: entityRef, coverageType } = req.query;
141
+ const entity = await catalogApi.getEntityByRef(
142
+ entityRef,
143
+ await auth.getPluginRequestToken({
144
+ onBehalfOf: await httpAuth.credentials(req),
145
+ targetPluginId: "catalog"
146
+ })
147
+ );
148
+ if (!entity) {
149
+ throw new errors.NotFoundError(`No entity found matching ${entityRef}`);
150
+ }
151
+ let converter;
152
+ if (!coverageType) {
153
+ throw new errors.InputError("Need coverageType query parameter");
154
+ } else if (coverageType === "jacoco") {
155
+ converter = new jacoco.Jacoco(logger);
156
+ } else if (coverageType === "cobertura") {
157
+ converter = new cobertura.Cobertura(logger);
158
+ } else if (coverageType === "lcov") {
159
+ converter = new lcov.Lcov(logger);
160
+ } else {
161
+ throw new errors.InputError(`Unsupported coverage type '${coverageType}`);
162
+ }
163
+ const { sourceLocation, vcs, scmFiles, body } = await utils.processCoveragePayload(entity, req);
164
+ const files = converter.convert(body, scmFiles);
165
+ if (!files || files.length === 0) {
166
+ throw new errors.InputError(`Unable to parse body as ${coverageType}`);
167
+ }
168
+ const coverage = await utils.buildCoverage(
169
+ entity,
170
+ sourceLocation,
171
+ vcs,
172
+ files
173
+ );
174
+ await codeCoverageDatabase.insertCodeCoverage(coverage);
175
+ res.status(201).json({
176
+ links: [
177
+ {
178
+ rel: "coverage",
179
+ href: `${codecovUrl}/report?entity=${entityRef}`
180
+ }
181
+ ]
182
+ });
183
+ });
184
+ const middleware = rootHttpRouter.MiddlewareFactory.create({ logger, config });
185
+ router.use(middleware.error());
186
+ return router;
187
+ };
188
+ async function createRouter(options) {
189
+ const logger = options.logger;
190
+ logger.info("Initializing Code Coverage backend");
191
+ return makeRouter(options);
192
+ }
193
+
194
+ exports.createRouter = createRouter;
195
+ exports.makeRouter = makeRouter;
196
+ //# sourceMappingURL=router.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router.cjs.js","sources":["../../src/service/router.ts"],"sourcesContent":["/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport express from 'express';\nimport Router from 'express-promise-router';\nimport BodyParser from 'body-parser';\nimport bodyParserXml from 'body-parser-xml';\nimport { CatalogApi, CatalogClient } from '@backstage/catalog-client';\nimport { createLegacyAuthAdapters } from '@backstage/backend-common';\nimport { InputError, NotFoundError } from '@backstage/errors';\nimport { Config } from '@backstage/config';\nimport { ScmIntegrations } from '@backstage/integration';\nimport { CodeCoverageDatabase } from './CodeCoverageDatabase';\nimport { aggregateCoverage, CoverageUtils } from './CoverageUtils';\nimport { Cobertura, Converter, Jacoco, Lcov } from './converter';\nimport { getEntitySourceLocation } from '@backstage/catalog-model';\nimport {\n AuthService,\n DatabaseService,\n DiscoveryService,\n HttpAuthService,\n LoggerService,\n UrlReaderService,\n} from '@backstage/backend-plugin-api';\nimport { MiddlewareFactory } from '@backstage/backend-defaults/rootHttpRouter';\n\n/**\n * Options for {@link createRouter}.\n *\n * @public\n */\nexport interface RouterOptions {\n config: Config;\n discovery: DiscoveryService;\n database: DatabaseService;\n urlReader: UrlReaderService;\n logger: LoggerService;\n catalogApi?: CatalogApi;\n auth?: AuthService;\n httpAuth?: HttpAuthService;\n}\n\nexport interface CodeCoverageApi {\n name: string;\n}\n\nexport const makeRouter = async (\n options: RouterOptions,\n): Promise<express.Router> => {\n const { config, logger, discovery, database, urlReader } = options;\n\n const codeCoverageDatabase = await CodeCoverageDatabase.create(database);\n const codecovUrl = await discovery.getExternalBaseUrl('code-coverage');\n const catalogApi =\n options.catalogApi ?? new CatalogClient({ discoveryApi: discovery });\n const scm = ScmIntegrations.fromConfig(config);\n const { auth, httpAuth } = createLegacyAuthAdapters(options);\n\n const bodySizeLimit =\n config.getOptionalString('codeCoverage.bodySizeLimit') ?? '100kb';\n\n bodyParserXml(BodyParser);\n const router = Router();\n router.use(\n BodyParser.xml({\n limit: bodySizeLimit,\n }),\n );\n router.use(\n BodyParser.text({\n limit: bodySizeLimit,\n }),\n );\n router.use(express.json());\n\n const utils = new CoverageUtils(scm, urlReader);\n\n router.get('/health', async (_req, res) => {\n res.status(200).json({ status: 'ok' });\n });\n\n /**\n * /report?entity=component:default/mycomponent\n */\n router.get('/report', async (req, res) => {\n const { entity } = req.query;\n const entityLookup = await catalogApi.getEntityByRef(\n entity as string,\n await auth.getPluginRequestToken({\n onBehalfOf: await httpAuth.credentials(req),\n targetPluginId: 'catalog',\n }),\n );\n if (!entityLookup) {\n throw new NotFoundError(`No entity found matching ${entity}`);\n }\n const stored = await codeCoverageDatabase.getCodeCoverage(entity as string);\n\n const aggregate = aggregateCoverage(stored);\n\n res.status(200).json({\n ...stored,\n aggregate: {\n line: aggregate.line,\n branch: aggregate.branch,\n },\n });\n });\n\n /**\n * /history?entity=component:default/mycomponent\n */\n router.get('/history', async (req, res) => {\n const { entity } = req.query;\n const entityLookup = await catalogApi.getEntityByRef(\n entity as string,\n await auth.getPluginRequestToken({\n onBehalfOf: await httpAuth.credentials(req),\n targetPluginId: 'catalog',\n }),\n );\n if (!entityLookup) {\n throw new NotFoundError(`No entity found matching ${entity}`);\n }\n const { limit } = req.query;\n const history = await codeCoverageDatabase.getHistory(\n entity as string,\n parseInt(limit?.toString() || '10', 10),\n );\n\n res.status(200).json(history);\n });\n\n /**\n * /file-content?entity=component:default/mycomponent&path=src/some-file.go\n */\n router.get('/file-content', async (req, res) => {\n const { entity, path } = req.query;\n const entityLookup = await catalogApi.getEntityByRef(\n entity as string,\n await auth.getPluginRequestToken({\n onBehalfOf: await httpAuth.credentials(req),\n targetPluginId: 'catalog',\n }),\n );\n if (!entityLookup) {\n throw new NotFoundError(`No entity found matching ${entity}`);\n }\n\n if (!path) {\n throw new InputError('Need path query parameter');\n }\n\n const sourceLocation = getEntitySourceLocation(entityLookup);\n\n if (!sourceLocation) {\n throw new InputError(\n `No \"backstage.io/source-location\" annotation on entity ${entity}`,\n );\n }\n\n const vcs = scm.byUrl(sourceLocation.target);\n if (!vcs) {\n throw new InputError(`Unable to determine SCM from ${sourceLocation}`);\n }\n\n const scmTree = await urlReader.readTree(sourceLocation.target);\n const scmFile = (await scmTree.files()).find(f => f.path === path);\n if (!scmFile) {\n res.status(404).json({\n message: \"Couldn't find file in SCM\",\n file: path,\n scm: vcs.title,\n });\n return;\n }\n const content = await scmFile?.content();\n if (!content) {\n res.status(400).json({\n message: \"Couldn't process content of file in SCM\",\n file: path,\n scm: vcs.title,\n });\n return;\n }\n\n const data = content.toString();\n res.status(200).contentType('text/plain').send(data);\n });\n\n /**\n * /report?entity=component:default/mycomponent&coverageType=cobertura\n */\n router.post('/report', async (req, res) => {\n const { entity: entityRef, coverageType } = req.query;\n const entity = await catalogApi.getEntityByRef(\n entityRef as string,\n await auth.getPluginRequestToken({\n onBehalfOf: await httpAuth.credentials(req),\n targetPluginId: 'catalog',\n }),\n );\n if (!entity) {\n throw new NotFoundError(`No entity found matching ${entityRef}`);\n }\n\n let converter: Converter;\n if (!coverageType) {\n throw new InputError('Need coverageType query parameter');\n } else if (coverageType === 'jacoco') {\n converter = new Jacoco(logger);\n } else if (coverageType === 'cobertura') {\n converter = new Cobertura(logger);\n } else if (coverageType === 'lcov') {\n converter = new Lcov(logger);\n } else {\n throw new InputError(`Unsupported coverage type '${coverageType}`);\n }\n\n const { sourceLocation, vcs, scmFiles, body } =\n await utils.processCoveragePayload(entity, req);\n\n const files = converter.convert(body, scmFiles);\n if (!files || files.length === 0) {\n throw new InputError(`Unable to parse body as ${coverageType}`);\n }\n\n const coverage = await utils.buildCoverage(\n entity,\n sourceLocation,\n vcs,\n files,\n );\n await codeCoverageDatabase.insertCodeCoverage(coverage);\n\n res.status(201).json({\n links: [\n {\n rel: 'coverage',\n href: `${codecovUrl}/report?entity=${entityRef}`,\n },\n ],\n });\n });\n\n const middleware = MiddlewareFactory.create({ logger, config });\n\n router.use(middleware.error());\n return router;\n};\n\n/**\n * Creates a code-coverage plugin backend router.\n *\n * @public\n */\nexport async function createRouter(\n options: RouterOptions,\n): Promise<express.Router> {\n const logger = options.logger;\n\n logger.info('Initializing Code Coverage backend');\n\n return makeRouter(options);\n}\n"],"names":["CodeCoverageDatabase","CatalogClient","ScmIntegrations","createLegacyAuthAdapters","bodyParserXml","BodyParser","Router","express","CoverageUtils","NotFoundError","aggregateCoverage","InputError","getEntitySourceLocation","Jacoco","Cobertura","Lcov","MiddlewareFactory"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backstage-community/plugin-code-coverage-backend",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "A Backstage backend plugin that helps you keep track of your code coverage",
5
5
  "backstage": {
6
6
  "role": "backend-plugin",
@@ -38,14 +38,14 @@
38
38
  "test": "backstage-cli package test"
39
39
  },
40
40
  "dependencies": {
41
- "@backstage/backend-common": "^0.24.0",
42
- "@backstage/backend-defaults": "^0.4.3",
43
- "@backstage/backend-plugin-api": "^0.8.0",
44
- "@backstage/catalog-client": "^1.6.6",
45
- "@backstage/catalog-model": "^1.6.0",
41
+ "@backstage/backend-common": "^0.25.0",
42
+ "@backstage/backend-defaults": "^0.5.2",
43
+ "@backstage/backend-plugin-api": "^1.0.1",
44
+ "@backstage/catalog-client": "^1.7.1",
45
+ "@backstage/catalog-model": "^1.7.0",
46
46
  "@backstage/config": "^1.2.0",
47
47
  "@backstage/errors": "^1.2.4",
48
- "@backstage/integration": "^1.14.0",
48
+ "@backstage/integration": "^1.15.1",
49
49
  "@types/express": "^4.17.6",
50
50
  "body-parser": "^1.20.0",
51
51
  "body-parser-xml": "^2.0.5",
@@ -56,8 +56,8 @@
56
56
  "yn": "^4.0.0"
57
57
  },
58
58
  "devDependencies": {
59
- "@backstage/backend-test-utils": "^0.5.0",
60
- "@backstage/cli": "^0.27.0",
59
+ "@backstage/backend-test-utils": "^1.0.2",
60
+ "@backstage/cli": "^0.28.0",
61
61
  "@types/body-parser-xml": "^2.0.2",
62
62
  "@types/supertest": "^6.0.0",
63
63
  "@types/uuid": "^9.0.0",