Merge remote-tracking branch 'upstream/master' into gh-3811-repo-url-file-registration

This commit is contained in:
Jussi Hallila
2021-01-18 19:30:22 +01:00
36 changed files with 425 additions and 197 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-org': patch
---
Fixed - normalizing strings for comparison when ignoring when one is in low case.
+11
View File
@@ -0,0 +1,11 @@
---
'@backstage/cli': patch
'@backstage/create-app': patch
'@backstage/plugin-catalog-backend': patch
'@backstage/plugin-catalog-import': patch
'@backstage/plugin-cloudbuild': patch
'@backstage/plugin-github-actions': patch
'@backstage/plugin-scaffolder-backend': patch
---
Get rid of all usages of @octokit/types, and bump the rest of the octokit dependencies to the latest version
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-graphiql': patch
---
Updated README
+12
View File
@@ -0,0 +1,12 @@
---
'example-backend': patch
'@backstage/create-app': patch
'@backstage/plugin-scaffolder-backend': patch
---
Bump the gitbeaker dependencies to 28.x.
To update your own installation, go through the `package.json` files of all of
your packages, and ensure that all dependencies on `@gitbeaker/node` or
`@gitbeaker/core` are at version `^28.0.2`. Then run `yarn install` at the root
of your repo.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Handle no npm info
@@ -0,0 +1,5 @@
---
'@backstage/plugin-techdocs': patch
---
Use `history.pushState` for hash link navigation.
+1
View File
@@ -13,3 +13,4 @@
/plugins/techdocs-backend @backstage/techdocs-core
/packages/techdocs-common @backstage/techdocs-core
/.changeset/cost-insights-* @backstage/silver-lining
/.changeset/techdocs-* @backstage/techdocs-core
+2
View File
@@ -77,6 +77,7 @@ Firekube
Fiverr
freben
Fredrik
gitbeaker
GitHub
GitLab
Grafana
@@ -142,6 +143,7 @@ npm
nvarchar
nvm
OAuth
octokit
oidc
Okta
Oldsberg
+1 -1
View File
@@ -43,7 +43,7 @@
"version": "1.0.0",
"devDependencies": {
"@changesets/cli": "^2.11.0",
"@octokit/openapi-types": "^2.0.0",
"@octokit/openapi-types": "^2.2.0",
"@spotify/eslint-config-oss": "^1.0.1",
"@spotify/prettier-config": "^9.0.0",
"command-exists": "^1.2.9",
+1 -1
View File
@@ -36,7 +36,7 @@
"@backstage/theme": "^0.2.2",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@octokit/rest": "^18.0.0",
"@octokit/rest": "^18.0.12",
"@roadiehq/backstage-plugin-buildkite": "^0.1.3",
"@roadiehq/backstage-plugin-github-insights": "^0.2.16",
"@roadiehq/backstage-plugin-github-pull-requests": "^0.6.3",
+2 -2
View File
@@ -39,8 +39,8 @@
"@backstage/plugin-rollbar-backend": "^0.1.5",
"@backstage/plugin-scaffolder-backend": "^0.4.0",
"@backstage/plugin-techdocs-backend": "^0.5.0",
"@gitbeaker/node": "^25.2.0",
"@octokit/rest": "^18.0.0",
"@gitbeaker/node": "^28.0.2",
"@octokit/rest": "^18.0.12",
"azure-devops-node-api": "^10.1.1",
"dockerode": "^3.2.1",
"example-app": "^0.2.8",
+1 -1
View File
@@ -34,7 +34,7 @@
"@hot-loader/react-dom": "^16.13.0",
"@lerna/package-graph": "^3.18.5",
"@lerna/project": "^3.18.0",
"@octokit/request": "^5.2.0",
"@octokit/request": "^5.4.12",
"@rollup/plugin-commonjs": "^16.0.0",
"@rollup/plugin-json": "^4.0.2",
"@rollup/plugin-node-resolve": "^9.0.0",
@@ -177,4 +177,71 @@ describe('bump', () => {
},
});
});
it('should ignore not found packages', async () => {
// Make sure all modules involved in package discovery are in the module cache before we mock fs
await mapDependencies(paths.targetDir);
mockFs({
'/yarn.lock': lockfileMockResult,
'/lerna.json': JSON.stringify({
packages: ['packages/*'],
}),
'/packages/a/package.json': JSON.stringify({
name: 'a',
dependencies: {
'@backstage/core': '^1.0.5',
},
}),
'/packages/b/package.json': JSON.stringify({
name: 'b',
dependencies: {
'@backstage/core': '^1.0.3',
'@backstage/theme': '^2.0.0',
},
}),
});
paths.targetDir = '/';
jest
.spyOn(paths, 'resolveTargetRoot')
.mockImplementation((...paths) => resolvePath('/', ...paths));
jest.spyOn(runObj, 'runPlain').mockImplementation(async () => '');
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
const { log: logs } = await withLogCollector(['log'], async () => {
await bump();
});
expect(logs.filter(Boolean)).toEqual([
'Checking for updates of @backstage/theme',
'Checking for updates of @backstage/core',
'Package info not found, ignoring package @backstage/theme',
'Package info not found, ignoring package @backstage/core',
'Checking for updates of @backstage/theme',
'Checking for updates of @backstage/core',
'Package info not found, ignoring package @backstage/theme',
'Package info not found, ignoring package @backstage/core',
'All Backstage packages are up to date!',
]);
expect(runObj.run).toHaveBeenCalledTimes(0);
const lockfileContents = await fs.readFile('/yarn.lock', 'utf8');
expect(lockfileContents).toBe(lockfileMockResult);
const packageA = await fs.readJson('/packages/a/package.json');
expect(packageA).toEqual({
name: 'a',
dependencies: {
'@backstage/core': '^1.0.5', // not bumped
},
});
const packageB = await fs.readJson('/packages/b/package.json');
expect(packageB).toEqual({
name: 'b',
dependencies: {
'@backstage/core': '^1.0.3', // not bumped
'@backstage/theme': '^2.0.0', // not bumped
},
});
});
});
+20 -2
View File
@@ -55,7 +55,16 @@ export default async () => {
// Track package versions that we want to remove from yarn.lock in order to trigger a bump
const unlocked = Array<{ name: string; range: string; target: string }>();
await workerThreads(16, dependencyMap.entries(), async ([name, pkgs]) => {
const target = await findTargetVersion(name);
let target: string;
try {
target = await findTargetVersion(name);
} catch (error) {
if (error.name === 'NotFoundError') {
console.log(`Package info not found, ignoring package ${name}`);
return;
}
throw error;
}
for (const pkg of pkgs) {
if (semver.satisfies(target, pkg.range)) {
@@ -84,7 +93,16 @@ export default async () => {
return;
}
const target = await findTargetVersion(name);
let target: string;
try {
target = await findTargetVersion(name);
} catch (error) {
if (error.name === 'NotFoundError') {
console.log(`Package info not found, ignoring package ${name}`);
return;
}
throw error;
}
for (const entry of lockfile.get(name) ?? []) {
// Ignore lockfile entries that don't satisfy the version range, since
+2
View File
@@ -44,3 +44,5 @@ export function exitWithError(error: Error): never {
process.exit(1);
}
}
export class NotFoundError extends CustomError {}
@@ -19,6 +19,7 @@ import path from 'path';
import * as runObj from '../run';
import { paths } from '../paths';
import { fetchPackageInfo, mapDependencies } from './packages';
import { NotFoundError } from '../errors';
describe('fetchPackageInfo', () => {
afterEach(() => {
@@ -40,6 +41,14 @@ describe('fetchPackageInfo', () => {
'my-package',
);
});
it('should throw if no info', async () => {
jest.spyOn(runObj, 'runPlain').mockResolvedValue('');
await expect(fetchPackageInfo('my-package')).rejects.toThrow(
new NotFoundError(`No package information found for package my-package`),
);
});
});
describe('mapDependencies', () => {
@@ -15,6 +15,7 @@
*/
import { runPlain } from '../../lib/run';
import { NotFoundError } from '../errors';
const PREFIX = '@backstage';
@@ -49,6 +50,11 @@ export async function fetchPackageInfo(
name: string,
): Promise<YarnInfoInspectData> {
const output = await runPlain('yarn', 'info', '--json', name);
if (!output) {
throw new NotFoundError(`No package information found for package ${name}`);
}
const info = JSON.parse(output) as YarnInfo;
if (info.type !== 'inspect') {
throw new Error(`Received unknown yarn info for ${name}, ${output}`);
@@ -27,8 +27,8 @@
"@backstage/plugin-proxy-backend": "^{{version '@backstage/plugin-proxy-backend'}}",
"@backstage/plugin-scaffolder-backend": "^{{version '@backstage/plugin-scaffolder-backend'}}",
"@backstage/plugin-techdocs-backend": "^{{version '@backstage/plugin-techdocs-backend'}}",
"@octokit/rest": "^18.0.0",
"@gitbeaker/node": "^25.2.0",
"@gitbeaker/node": "^28.0.2",
"@octokit/rest": "^18.0.12",
"dockerode": "^3.2.1",
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
+1 -1
View File
@@ -51,7 +51,7 @@ export interface Config {
cert?: string;
privateKey?: string;
decryptionPvk?: string;
signatureAlgorithm?: 'sha1' | 'sha256' | 'sha512';
signatureAlgorithm?: 'sha256' | 'sha512';
digestAlgorithm?: string;
};
okta?: {
+3 -1
View File
@@ -58,7 +58,7 @@
"passport-oauth2": "^1.5.0",
"passport-okta-oauth": "^0.0.1",
"passport-onelogin-oauth": "^0.0.1",
"passport-saml": "^1.3.5, <1.4.0",
"passport-saml": "^2.0.0",
"uuid": "^8.0.0",
"winston": "^3.2.1",
"yn": "^4.0.0"
@@ -74,6 +74,8 @@
"@types/passport-google-oauth20": "^2.0.3",
"@types/passport-microsoft": "^0.0.0",
"@types/passport-saml": "^1.1.2",
"@types/passport-strategy": "^0.2.35",
"@types/xml2js": "^0.4.7",
"msw": "^0.21.2",
"nock": "^13.0.5"
},
@@ -15,8 +15,8 @@
*/
import express from 'express';
import { SamlConfig } from 'passport-saml/lib/passport-saml/types';
import {
SamlConfig,
Strategy as SamlStrategy,
Profile as SamlProfile,
VerifyWithoutRequest,
+1 -1
View File
@@ -34,7 +34,7 @@
"@backstage/backend-common": "^0.4.2",
"@backstage/catalog-model": "^0.6.0",
"@backstage/config": "^0.1.2",
"@octokit/graphql": "^4.5.6",
"@octokit/graphql": "^4.5.8",
"@types/express": "^4.17.6",
"@types/ldapjs": "^1.0.9",
"codeowners-utils": "^1.0.2",
+1 -1
View File
@@ -38,7 +38,7 @@
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
"@octokit/rest": "^18.0.6",
"@octokit/rest": "^18.0.12",
"git-url-parse": "^11.4.3",
"react": "^16.13.1",
"react-dom": "^16.13.1",
-2
View File
@@ -37,8 +37,6 @@
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
"@octokit/rest": "^18.0.0",
"@octokit/types": "^5.4.1",
"moment": "^2.27.0",
"qs": "^6.9.4",
"react": "^16.13.1",
+1 -2
View File
@@ -40,8 +40,7 @@
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
"@octokit/rest": "^18.0.0",
"@octokit/types": "^5.4.1",
"@octokit/rest": "^18.0.12",
"moment": "^2.27.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
@@ -15,12 +15,7 @@
*/
import { createApiRef } from '@backstage/core';
import {
ActionsListWorkflowRunsForRepoResponseData,
ActionsGetWorkflowResponseData,
ActionsGetWorkflowRunResponseData,
EndpointInterface,
} from '@octokit/types';
import { RestEndpointMethodTypes } from '@octokit/rest';
export const githubActionsApiRef = createApiRef<GithubActionsApi>({
id: 'plugin.githubactions.service',
@@ -42,7 +37,9 @@ export type GithubActionsApi = {
pageSize?: number;
page?: number;
branch?: string;
}) => Promise<ActionsListWorkflowRunsForRepoResponseData>;
}) => Promise<
RestEndpointMethodTypes['actions']['listWorkflowRuns']['response']['data']
>;
getWorkflow: ({
token,
owner,
@@ -53,7 +50,9 @@ export type GithubActionsApi = {
owner: string;
repo: string;
id: number;
}) => Promise<ActionsGetWorkflowResponseData>;
}) => Promise<
RestEndpointMethodTypes['actions']['getWorkflow']['response']['data']
>;
getWorkflowRun: ({
token,
owner,
@@ -64,7 +63,9 @@ export type GithubActionsApi = {
owner: string;
repo: string;
id: number;
}) => Promise<ActionsGetWorkflowRunResponseData>;
}) => Promise<
RestEndpointMethodTypes['actions']['getWorkflowRun']['response']['data']
>;
reRunWorkflow: ({
token,
owner,
@@ -86,5 +87,7 @@ export type GithubActionsApi = {
owner: string;
repo: string;
runId: number;
}) => Promise<EndpointInterface>;
}) => Promise<
RestEndpointMethodTypes['actions']['downloadJobLogsForWorkflowRun']['response']['data']
>;
};
@@ -15,13 +15,7 @@
*/
import { GithubActionsApi } from './GithubActionsApi';
import { Octokit } from '@octokit/rest';
import {
ActionsListWorkflowRunsForRepoResponseData,
ActionsGetWorkflowResponseData,
ActionsGetWorkflowRunResponseData,
EndpointInterface,
} from '@octokit/types';
import { Octokit, RestEndpointMethodTypes } from '@octokit/rest';
export class GithubActionsClient implements GithubActionsApi {
async reRunWorkflow({
@@ -55,7 +49,9 @@ export class GithubActionsClient implements GithubActionsApi {
pageSize?: number;
page?: number;
branch?: string;
}): Promise<ActionsListWorkflowRunsForRepoResponseData> {
}): Promise<
RestEndpointMethodTypes['actions']['listWorkflowRuns']['response']['data']
> {
const workflowRuns = await new Octokit({
auth: token,
}).actions.listWorkflowRunsForRepo({
@@ -77,7 +73,9 @@ export class GithubActionsClient implements GithubActionsApi {
owner: string;
repo: string;
id: number;
}): Promise<ActionsGetWorkflowResponseData> {
}): Promise<
RestEndpointMethodTypes['actions']['getWorkflow']['response']['data']
> {
const workflow = await new Octokit({ auth: token }).actions.getWorkflow({
owner,
repo,
@@ -95,7 +93,9 @@ export class GithubActionsClient implements GithubActionsApi {
owner: string;
repo: string;
id: number;
}): Promise<ActionsGetWorkflowRunResponseData> {
}): Promise<
RestEndpointMethodTypes['actions']['getWorkflowRun']['response']['data']
> {
const run = await new Octokit({ auth: token }).actions.getWorkflowRun({
owner,
repo,
@@ -113,7 +113,9 @@ export class GithubActionsClient implements GithubActionsApi {
owner: string;
repo: string;
runId: number;
}): Promise<EndpointInterface> {
}): Promise<
RestEndpointMethodTypes['actions']['downloadJobLogsForWorkflowRun']['response']['data']
> {
const workflow = await new Octokit({
auth: token,
}).actions.downloadJobLogsForWorkflowRun({
@@ -209,8 +209,8 @@ export const WorkflowRunDetails = ({ entity }: { entity: Entity }) => {
</TableCell>
<TableCell>
<WorkflowRunStatus
status={details.value?.status}
conclusion={details.value?.conclusion}
status={details.value?.status || undefined}
conclusion={details.value?.conclusion || undefined}
/>
</TableCell>
</TableRow>
@@ -218,7 +218,7 @@ export const WorkflowRunDetails = ({ entity }: { entity: Entity }) => {
<TableCell>
<Typography noWrap>Author</Typography>
</TableCell>
<TableCell>{`${details.value?.head_commit.author.name} (${details.value?.head_commit.author.email})`}</TableCell>
<TableCell>{`${details.value?.head_commit.author?.name} (${details.value?.head_commit.author?.email})`}</TableCell>
</TableRow>
<TableRow>
<TableCell>
@@ -18,7 +18,6 @@ import { useAsyncRetry } from 'react-use';
import { WorkflowRun } from './WorkflowRunsTable/WorkflowRunsTable';
import { githubActionsApiRef } from '../api/GithubActionsApi';
import { useApi, githubAuthApiRef, errorApiRef } from '@backstage/core';
import { ActionsListWorkflowRunsForRepoResponseData } from '@octokit/types';
export function useWorkflowRuns({
owner,
@@ -55,44 +54,40 @@ export function useWorkflowRuns({
page: page + 1,
branch,
})
.then(
(
workflowRunsData: ActionsListWorkflowRunsForRepoResponseData,
): WorkflowRun[] => {
setTotal(workflowRunsData.total_count);
// Transformation here
return workflowRunsData.workflow_runs.map(run => ({
message: run.head_commit.message,
id: `${run.id}`,
onReRunClick: async () => {
try {
await api.reRunWorkflow({
token,
owner,
repo,
runId: run.id,
});
} catch (e) {
errorApi.post(e);
}
.then((workflowRunsData): WorkflowRun[] => {
setTotal(workflowRunsData.total_count);
// Transformation here
return workflowRunsData.workflow_runs.map(run => ({
message: run.head_commit.message,
id: `${run.id}`,
onReRunClick: async () => {
try {
await api.reRunWorkflow({
token,
owner,
repo,
runId: run.id,
});
} catch (e) {
errorApi.post(e);
}
},
source: {
branchName: run.head_branch,
commit: {
hash: run.head_commit.id,
url: run.head_repository.branches_url.replace(
'{/branch}',
run.head_branch,
),
},
source: {
branchName: run.head_branch,
commit: {
hash: run.head_commit.id,
url: run.head_repository.branches_url.replace(
'{/branch}',
run.head_branch,
),
},
},
status: run.status,
conclusion: run.conclusion,
url: run.url,
githubUrl: run.html_url,
}));
},
)
},
status: run.status,
conclusion: run.conclusion,
url: run.url,
githubUrl: run.html_url,
}));
})
);
}, [page, pageSize, repo, owner]);
+50 -39
View File
@@ -12,53 +12,64 @@ By exposing GraphiQL as a plugin instead of a standalone app, it's possible to p
Start out by installing the plugin in your Backstage app:
```bash
cd packages/app
yarn add @backstage/plugin-graphiql
```
Then add an entry to your App's `plugins.ts` to import the plugin.
```diff
# in packages/app/src/plugins.ts
+export { plugin as GraphiQL } from '@backstage/plugin-graphiql';
```
The plugin registers a `/graphiql` route, which you can link to from the Sidebar if desired.
```diff
# in packages/app/src/App.tsx
+import { Router as GraphiQLRouter } from '@backstage/plugin-graphiql';
const AppRoutes = () => (
<Routes>
+ <Route path="/graphiql" element={<GraphiQLRouter />} />
```
### Adding GraphQL endpoints
For the plugin to function, you need to supply GraphQL endpoints through the GraphQLBrowse API, which is done by implementing the `GraphQLBrowseApi` exported by this plugin.
For the plugin to function, you need to supply GraphQL endpoints through the GraphQLBrowse API. This is done by implementing the `graphQlBrowseApiRef` exported by this plugin.
If all you need is a static list of endpoints, the plugin exports a `GraphQLEndpoints` class that implements the `GraphQLBrowseApi` for you. Here's and example of how you could expose two GraphQL endpoints in your App:
If all you need is a static list of endpoints, the plugin exports a `GraphQLEndpoints` class that implements the `GraphQLBrowseApi` for you. Here's an example of how you could expose two GraphQL endpoints in your App:
```tsx
import {
graphQlBrowseApiRef,
GraphQLEndpoints,
} from '@backstage/plugin-graphiql';
```diff
# in packages/app/src/apis.ts
+import {
+ graphQlBrowseApiRef,
+ GraphQLEndpoints,
+} from '@backstage/plugin-graphiql';
// Implement the Graph QL browse API using a static list of endpoints
const graphQlBrowseApi = GraphQLEndpoints.from([
// Use the .create function if all you need is a static URL and headers.
GraphQLEndpoints.create({
id: 'gitlab',
title: 'GitLab',
url: 'https://gitlab.com/api/graphql',
// Optional extra headers
headers: { Extra: 'Header' },
}),
{
id: 'hooli-search',
title: 'Hooli Search',
// Custom fetch function, this one is equivalent to using GraphQLEndpoints.create()
// with url set to https://internal.hooli.com/search
fetcher: async (params: any) => {
return fetch('https://internal.hooli.com/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
}).then(res => res.json());
},
},
]);
// ApiRegistry builder created somewhere in your App
const builder = ApiRegistry.builder();
// Add the instance to the API registry
builder.add(graphQlBrowseApiRef, graphQlBrowseApi);
export const apis = [
+ createApiFactory({
+ api: graphQlBrowseApiRef,
+ deps: { errorApi: errorApiRef, githubAuthApi: githubAuthApiRef },
+ factory: ({ errorApi, githubAuthApi }) =>
+ GraphQLEndpoints.from([
+ // Use the .create function if all you need is a static URL and headers.
+ GraphQLEndpoints.create({
+ id: 'gitlab',
+ title: 'GitLab',
+ url: 'https://gitlab.com/api/graphql',
+ // Optional extra headers
+ headers: { Extra: 'Header' },
+ }),
+ {
+ id: 'hooli-search',
+ title: 'Hooli Search',
+ // Custom fetch function, this one is equivalent to using GraphQLEndpoints.create()
+ // with url set to https://internal.hooli.com/search
+ fetcher: async (params: any) => {
+ return fetch('https://internal.hooli.com/search', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(params),
+ }).then(res => res.json());
+ },
+ }
+ ]),
+ }),
```
+2 -1
View File
@@ -25,7 +25,8 @@ import { graphiQLRouteRef } from './route-refs';
export const plugin = createPlugin({
id: 'graphiql',
apis: [
// GitLab is used as an example endpoint, but most plug
// GitLab is used as an example endpoint, but most will want to plug in
// their own instead.
createApiFactory(
graphQlBrowseApiRef,
GraphQLEndpoints.from([
@@ -125,7 +125,9 @@ export const MembersListCard = ({
UserEntity
>).filter(member =>
member?.relations?.some(
r => r.type === RELATION_MEMBER_OF && r.target.name === groupName,
r =>
r.type === RELATION_MEMBER_OF &&
r.target.name.toLowerCase() === groupName.toLowerCase(),
),
);
return groupMembersList;
+3 -4
View File
@@ -33,9 +33,9 @@
"@backstage/catalog-model": "^0.6.0",
"@backstage/config": "^0.1.2",
"@backstage/integration": "^0.1.5",
"@gitbeaker/core": "^25.2.0",
"@gitbeaker/node": "^25.2.0",
"@octokit/rest": "^18.0.0",
"@gitbeaker/core": "^28.0.2",
"@gitbeaker/node": "^28.0.2",
"@octokit/rest": "^18.0.12",
"@types/dockerode": "^3.2.1",
"@types/express": "^4.17.6",
"azure-devops-node-api": "^10.1.1",
@@ -60,7 +60,6 @@
"devDependencies": {
"@backstage/cli": "^0.4.5",
"@backstage/test-utils": "^0.1.5",
"@octokit/types": "^5.4.1",
"@types/fs-extra": "^9.0.1",
"@types/git-url-parse": "^9.0.0",
"@types/supertest": "^2.0.8",
@@ -17,15 +17,10 @@
jest.mock('@octokit/rest');
jest.mock('./helpers');
import { Octokit } from '@octokit/rest';
import {
OctokitResponse,
ReposCreateInOrgResponseData,
UsersGetByUsernameResponseData,
} from '@octokit/types';
import { getVoidLogger } from '@backstage/backend-common';
import { Octokit, RestEndpointMethodTypes } from '@octokit/rest';
import { GithubPublisher } from './github';
import { initRepoAndPush } from './helpers';
import { getVoidLogger } from '@backstage/backend-common';
const { mockGithubClient } = require('@octokit/rest') as {
mockGithubClient: {
@@ -54,12 +49,12 @@ describe('GitHub Publisher', () => {
data: {
clone_url: 'https://github.com/backstage/backstage.git',
},
} as OctokitResponse<ReposCreateInOrgResponseData>);
} as RestEndpointMethodTypes['repos']['createInOrg']['response']);
mockGithubClient.users.getByUsername.mockResolvedValue({
data: {
type: 'Organization',
},
} as OctokitResponse<UsersGetByUsernameResponseData>);
} as RestEndpointMethodTypes['users']['getByUsername']['response']);
const result = await publisher.publish({
values: {
@@ -104,12 +99,12 @@ describe('GitHub Publisher', () => {
data: {
clone_url: 'https://github.com/backstage/backstage.git',
},
} as OctokitResponse<ReposCreateInOrgResponseData>);
} as RestEndpointMethodTypes['repos']['createForAuthenticatedUser']['response']);
mockGithubClient.users.getByUsername.mockResolvedValue({
data: {
type: 'User',
},
} as OctokitResponse<UsersGetByUsernameResponseData>);
} as RestEndpointMethodTypes['users']['getByUsername']['response']);
const result = await publisher.publish({
values: {
@@ -148,12 +143,12 @@ describe('GitHub Publisher', () => {
data: {
clone_url: 'https://github.com/backstage/backstage.git',
},
} as OctokitResponse<ReposCreateInOrgResponseData>);
} as RestEndpointMethodTypes['repos']['createForAuthenticatedUser']['response']);
mockGithubClient.users.getByUsername.mockResolvedValue({
data: {
type: 'User',
},
} as OctokitResponse<UsersGetByUsernameResponseData>);
} as RestEndpointMethodTypes['users']['getByUsername']['response']);
const result = await publisher.publish({
values: {
@@ -205,12 +200,12 @@ describe('GitHub Publisher', () => {
data: {
clone_url: 'https://github.com/backstage/backstage.git',
},
} as OctokitResponse<ReposCreateInOrgResponseData>);
} as RestEndpointMethodTypes['repos']['createInOrg']['response']);
mockGithubClient.users.getByUsername.mockResolvedValue({
data: {
type: 'Organization',
},
} as OctokitResponse<UsersGetByUsernameResponseData>);
} as RestEndpointMethodTypes['users']['getByUsername']['response']);
const result = await publisher.publish({
values: {
@@ -254,12 +249,12 @@ describe('GitHub Publisher', () => {
data: {
clone_url: 'https://github.com/backstage/backstage.git',
},
} as OctokitResponse<ReposCreateInOrgResponseData>);
} as RestEndpointMethodTypes['repos']['createForAuthenticatedUser']['response']);
mockGithubClient.users.getByUsername.mockResolvedValue({
data: {
type: 'User',
},
} as OctokitResponse<UsersGetByUsernameResponseData>);
} as RestEndpointMethodTypes['users']['getByUsername']['response']);
const result = await publisher.publish({
values: {
@@ -115,7 +115,15 @@ export const Reader = ({ entityId, onReady }: Props) => {
baseUrl: window.location.origin,
onClick: (_: MouseEvent, url: string) => {
const parsedUrl = new URL(url);
navigate(`${parsedUrl.pathname}${parsedUrl.hash}`);
if (parsedUrl.hash) {
history.pushState(
null,
'',
`${parsedUrl.pathname}${parsedUrl.hash}`,
);
} else {
navigate(parsedUrl.pathname);
}
shadowRoot?.querySelector(parsedUrl.hash)?.scrollIntoView();
},
+119 -54
View File
@@ -2963,30 +2963,30 @@
dependencies:
yaml-ast-parser "0.0.43"
"@gitbeaker/core@^25.2.0":
version "25.6.0"
resolved "https://registry.npmjs.org/@gitbeaker/core/-/core-25.6.0.tgz#97d5ccc5d61bab6b678bec280036d594d275931e"
integrity sha512-+CohJNsbZiPl7jPgw7PHt5t0JIIV9NngObOskY1Ww8jef7SqaKpz0NsbSDawuWFBdmXApMpK81AEfASKtVI+cw==
"@gitbeaker/core@^28.0.2":
version "28.0.2"
resolved "https://registry.npmjs.org/@gitbeaker/core/-/core-28.0.2.tgz#5c2cbe27a20426a96d9511642153ead18ff88877"
integrity sha512-H+0ChYSxLh+EVNzmOguHaJMR7xB9FlVZGi89JQ7BJ2meq0h1TQl+QaGqTyTNM4GYhn7cYrl7ebGoU0yrnYUWSg==
dependencies:
"@gitbeaker/requester-utils" "^25.6.0"
"@gitbeaker/requester-utils" "^28.0.2"
form-data "^3.0.0"
li "^1.3.0"
xcase "^2.0.1"
"@gitbeaker/node@^25.2.0":
version "25.2.0"
resolved "https://registry.npmjs.org/@gitbeaker/node/-/node-25.2.0.tgz#cc91e83328ec32de0b1a0dac23accd2385734a66"
integrity sha512-FWchXYJ5agn0ptAQxtkkSKSg1ObbP2xfMzHLECxINFRBHYhg0ms8Fp8Qb+71pxJz7IMlvajyEtZaPfHBmyuh9Q==
"@gitbeaker/node@^28.0.2":
version "28.0.2"
resolved "https://registry.npmjs.org/@gitbeaker/node/-/node-28.0.2.tgz#ead4eb9e1b4400d3a6fe9769b50e43e84fc99747"
integrity sha512-FCILxXiRep3PUpe/P2+Ak1KdwzIz3azxf1OY8Jb72gKjNNTgCi4ByFCIpHkvbFZlr7kDKTtcPyEN6oA6qnrCdQ==
dependencies:
"@gitbeaker/core" "^25.2.0"
"@gitbeaker/requester-utils" "^25.2.0"
"@gitbeaker/core" "^28.0.2"
"@gitbeaker/requester-utils" "^28.0.2"
got "^11.7.0"
xcase "^2.0.1"
"@gitbeaker/requester-utils@^25.2.0", "@gitbeaker/requester-utils@^25.6.0":
version "25.6.0"
resolved "https://registry.npmjs.org/@gitbeaker/requester-utils/-/requester-utils-25.6.0.tgz#001a432a48460bb5196a02ed71763eb707a1a01e"
integrity sha512-jD8cHbAZPR6+cB3HiukQxcqIKF5VkHtqg2m+Ns6ROE1pb0oRn10D/a9J1lZOXC9Jz2tQOBMWfHlplbmFFdB6Og==
"@gitbeaker/requester-utils@^28.0.2":
version "28.0.2"
resolved "https://registry.npmjs.org/@gitbeaker/requester-utils/-/requester-utils-28.0.2.tgz#3c1bf97e6b73053ef3c38a2871cbc8e1a2e7501a"
integrity sha512-S851vQi0x9Sg5Tx0BcwzkG6OTIG0X4qYTBaA9kPM2sEKg2OOsd8X0HRhj/TbFGr/YVz+3Ubu7oueeWSyr5jvfQ==
dependencies:
form-data "^3.0.0"
query-string "^6.13.3"
@@ -4668,6 +4668,13 @@
dependencies:
"@octokit/types" "^2.0.0"
"@octokit/auth-token@^2.4.4":
version "2.4.4"
resolved "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.4.4.tgz#ee31c69b01d0378c12fd3ffe406030f3d94d3b56"
integrity sha512-LNfGu3Ro9uFAYh10MUZVaT7X2CnNm2C8IDQmabx+3DygYIQjs9FwzFAHN/0t6mu5HEPhxcb1XOuxdpY82vCg2Q==
dependencies:
"@octokit/types" "^6.0.0"
"@octokit/core@^3.0.0":
version "3.1.0"
resolved "https://registry.npmjs.org/@octokit/core/-/core-3.1.0.tgz#9c3c9b23f7504668cfa057f143ccbf0c645a0ac9"
@@ -4680,6 +4687,18 @@
before-after-hook "^2.1.0"
universal-user-agent "^5.0.0"
"@octokit/core@^3.2.3":
version "3.2.4"
resolved "https://registry.npmjs.org/@octokit/core/-/core-3.2.4.tgz#5791256057a962eca972e31818f02454897fd106"
integrity sha512-d9dTsqdePBqOn7aGkyRFe7pQpCXdibSJ5SFnrTr0axevObZrpz3qkWm7t/NjYv5a66z6vhfteriaq4FRz3e0Qg==
dependencies:
"@octokit/auth-token" "^2.4.4"
"@octokit/graphql" "^4.5.8"
"@octokit/request" "^5.4.12"
"@octokit/types" "^6.0.3"
before-after-hook "^2.1.0"
universal-user-agent "^6.0.0"
"@octokit/endpoint@^6.0.1":
version "6.0.3"
resolved "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.3.tgz#dd09b599662d7e1b66374a177ab620d8cdf73487"
@@ -4698,19 +4717,19 @@
"@octokit/types" "^5.0.0"
universal-user-agent "^5.0.0"
"@octokit/graphql@^4.5.6":
version "4.5.6"
resolved "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.5.6.tgz#708143ba15cf7c1879ed6188266e7f270be805d4"
integrity sha512-Rry+unqKTa3svswT2ZAuqenpLrzJd+JTv89LTeVa5UM/5OX8o4KTkPL7/1ABq4f/ZkELb0XEK/2IEoYwykcLXg==
"@octokit/graphql@^4.5.8":
version "4.5.8"
resolved "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.5.8.tgz#d42373633c3015d0eafce64a8ce196be167fdd9b"
integrity sha512-WnCtNXWOrupfPJgXe+vSmprZJUr0VIu14G58PMlkWGj3cH+KLZEfKMmbUQ6C3Wwx6fdhzVW1CD5RTnBdUHxhhA==
dependencies:
"@octokit/request" "^5.3.0"
"@octokit/types" "^5.0.0"
"@octokit/types" "^6.0.0"
universal-user-agent "^6.0.0"
"@octokit/openapi-types@^2.0.0":
version "2.0.1"
resolved "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-2.0.1.tgz#7453d8281ce66b8ed1607f7ac7d751c3baffd2cc"
integrity sha512-9AuC04PUnZrjoLiw3uPtwGh9FE4Q3rTqs51oNlQ0rkwgE8ftYsOC+lsrQyvCvWm85smBbSc0FNRKKumvGyb44Q==
"@octokit/openapi-types@^2.2.0":
version "2.2.0"
resolved "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-2.2.0.tgz#123e0438a0bc718ccdac3b5a2e69b3dd00daa85b"
integrity sha512-274lNUDonw10kT8wHg8fCcUc1ZjZHbWv0/TbAwb0ojhBQqZYc1cQ/4yqTVTtPMDeZ//g7xVEYe/s3vURkRghPg==
"@octokit/plugin-enterprise-rest@^6.0.1":
version "6.0.1"
@@ -4731,11 +4750,23 @@
dependencies:
"@octokit/types" "^5.0.0"
"@octokit/plugin-paginate-rest@^2.6.2":
version "2.7.0"
resolved "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.7.0.tgz#6bb7b043c246e0654119a6ec4e72a172c9e2c7f3"
integrity sha512-+zARyncLjt9b0FjqPAbJo4ss7HOlBi1nprq+cPlw5vu2+qjy7WvlXhtXFdRHQbSL1Pt+bfAKaLADEkkvg8sP8w==
dependencies:
"@octokit/types" "^6.0.1"
"@octokit/plugin-request-log@^1.0.0":
version "1.0.0"
resolved "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.0.tgz#eef87a431300f6148c39a7f75f8cfeb218b2547e"
integrity sha512-ywoxP68aOT3zHCLgWZgwUJatiENeHE7xJzYjfz8WI0goynp96wETBF+d95b8g/uL4QmS6owPVlaxiz3wyMAzcw==
"@octokit/plugin-request-log@^1.0.2":
version "1.0.2"
resolved "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.2.tgz#394d59ec734cd2f122431fbaf05099861ece3c44"
integrity sha512-oTJSNAmBqyDR41uSMunLQKMX0jmEXbwD1fpz8FG27lScV3RhtGfBa1/BBLym+PxcC16IBlF7KH9vP1BUYxA+Eg==
"@octokit/plugin-rest-endpoint-methods@2.4.0":
version "2.4.0"
resolved "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-2.4.0.tgz#3288ecf5481f68c494dd0602fc15407a59faf61e"
@@ -4752,12 +4783,12 @@
"@octokit/types" "^5.4.1"
deprecation "^2.3.1"
"@octokit/plugin-rest-endpoint-methods@4.2.0":
version "4.2.0"
resolved "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.2.0.tgz#c5a0691b3aba5d8b4ef5dffd6af3649608f167ba"
integrity sha512-1/qn1q1C1hGz6W/iEDm9DoyNoG/xdFDt78E3eZ5hHeUfJTLJgyAMdj9chL/cNBHjcjd+FH5aO1x0VCqR2RE0mw==
"@octokit/plugin-rest-endpoint-methods@4.4.1":
version "4.4.1"
resolved "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.4.1.tgz#105cf93255432155de078c9efc33bd4e14d1cd63"
integrity sha512-+v5PcvrUcDeFXf8hv1gnNvNLdm4C0+2EiuWt9EatjjUmfriM1pTMM+r4j1lLHxeBQ9bVDmbywb11e3KjuavieA==
dependencies:
"@octokit/types" "^5.5.0"
"@octokit/types" "^6.1.0"
deprecation "^2.3.1"
"@octokit/request-error@^1.0.2":
@@ -4792,6 +4823,20 @@
once "^1.4.0"
universal-user-agent "^5.0.0"
"@octokit/request@^5.4.12":
version "5.4.12"
resolved "https://registry.npmjs.org/@octokit/request/-/request-5.4.12.tgz#b04826fa934670c56b135a81447be2c1723a2ffc"
integrity sha512-MvWYdxengUWTGFpfpefBBpVmmEYfkwMoxonIB3sUGp5rhdgwjXL1ejo6JbgzG/QD9B/NYt/9cJX1pxXeSIUCkg==
dependencies:
"@octokit/endpoint" "^6.0.1"
"@octokit/request-error" "^2.0.0"
"@octokit/types" "^6.0.3"
deprecation "^2.0.0"
is-plain-object "^5.0.0"
node-fetch "^2.6.1"
once "^1.4.0"
universal-user-agent "^6.0.0"
"@octokit/rest@^16.28.4":
version "16.43.1"
resolved "https://registry.npmjs.org/@octokit/rest/-/rest-16.43.1.tgz#3b11e7d1b1ac2bbeeb23b08a17df0b20947eda6b"
@@ -4824,15 +4869,15 @@
"@octokit/plugin-request-log" "^1.0.0"
"@octokit/plugin-rest-endpoint-methods" "4.1.4"
"@octokit/rest@^18.0.6":
version "18.0.6"
resolved "https://registry.npmjs.org/@octokit/rest/-/rest-18.0.6.tgz#76c274f1a68f40741a131768ef483f041e7b98b6"
integrity sha512-ES4lZBKPJMX/yUoQjAZiyFjei9pJ4lTTfb9k7OtYoUzKPDLl/M8jiHqt6qeSauyU4eZGLw0sgP1WiQl9FYeM5w==
"@octokit/rest@^18.0.12":
version "18.0.12"
resolved "https://registry.npmjs.org/@octokit/rest/-/rest-18.0.12.tgz#278bd41358c56d87c201e787e8adc0cac132503a"
integrity sha512-hNRCZfKPpeaIjOVuNJzkEL6zacfZlBPV8vw8ReNeyUkVvbuCvvrrx8K8Gw2eyHHsmd4dPlAxIXIZ9oHhJfkJpw==
dependencies:
"@octokit/core" "^3.0.0"
"@octokit/plugin-paginate-rest" "^2.2.0"
"@octokit/plugin-request-log" "^1.0.0"
"@octokit/plugin-rest-endpoint-methods" "4.2.0"
"@octokit/core" "^3.2.3"
"@octokit/plugin-paginate-rest" "^2.6.2"
"@octokit/plugin-request-log" "^1.0.2"
"@octokit/plugin-rest-endpoint-methods" "4.4.1"
"@octokit/types@^2.0.0", "@octokit/types@^2.0.1":
version "2.5.0"
@@ -4855,11 +4900,12 @@
dependencies:
"@types/node" ">= 8"
"@octokit/types@^5.5.0":
version "5.5.0"
resolved "https://registry.npmjs.org/@octokit/types/-/types-5.5.0.tgz#e5f06e8db21246ca102aa28444cdb13ae17a139b"
integrity sha512-UZ1pErDue6bZNjYOotCNveTXArOMZQFG6hKJfOnGnulVCMcVVi7YIIuuR4WfBhjo7zgpmzn/BkPDnUXtNx+PcQ==
"@octokit/types@^6.0.0", "@octokit/types@^6.0.1", "@octokit/types@^6.0.3", "@octokit/types@^6.1.0":
version "6.2.1"
resolved "https://registry.npmjs.org/@octokit/types/-/types-6.2.1.tgz#7f881fe44475ab1825776a4a59ca1ae082ed1043"
integrity sha512-jHs9OECOiZxuEzxMZcXmqrEO8GYraHF+UzNVH2ACYh8e/Y7YoT+hUf9ldvVd6zIvWv4p3NdxbQ0xx3ku5BnSiA==
dependencies:
"@octokit/openapi-types" "^2.2.0"
"@types/node" ">= 8"
"@open-draft/until@^1.0.3":
@@ -6821,6 +6867,14 @@
"@types/express" "*"
"@types/passport" "*"
"@types/passport-strategy@^0.2.35":
version "0.2.35"
resolved "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.35.tgz#e52f5212279ea73f02d9b06af67efe9cefce2d0c"
integrity sha512-o5D19Jy2XPFoX2rKApykY15et3Apgax00RRLf0RUotPDUsYrQa7x4howLYr9El2mlUApHmCMv5CZ1IXqKFQ2+g==
dependencies:
"@types/express" "*"
"@types/passport" "*"
"@types/passport@*", "@types/passport@^1.0.3":
version "1.0.4"
resolved "https://registry.npmjs.org/@types/passport/-/passport-1.0.4.tgz#1b35c4e197560d3974fa5f71711b6e9cce0711f0"
@@ -7275,6 +7329,13 @@
dependencies:
"@types/node" "*"
"@types/xml2js@^0.4.7":
version "0.4.7"
resolved "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.7.tgz#cd5b6c67bbec741ac625718a76e6cb99bc34365e"
integrity sha512-f5VOKSMEE0O+/L54FHwA/a7vcx9mHeSDM71844yHCOhh8Cin2xQa0UFw0b7Vc5hoZ3Ih6ZHaDobjfLih4tWPNw==
dependencies:
"@types/node" "*"
"@types/yargs-parser@*":
version "15.0.0"
resolved "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d"
@@ -15724,6 +15785,11 @@ is-plain-object@^3.0.0:
dependencies:
isobject "^4.0.0"
is-plain-object@^5.0.0:
version "5.0.0"
resolved "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344"
integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==
is-potential-custom-element-name@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz#0c52e54bcca391bb2c494b21e8626d7336c6e397"
@@ -19879,17 +19945,16 @@ passport-onelogin-oauth@^0.0.1:
pkginfo "0.2.x"
uid2 "0.0.3"
"passport-saml@^1.3.5, <1.4.0":
version "1.3.5"
resolved "https://registry.npmjs.org/passport-saml/-/passport-saml-1.3.5.tgz#747f2c8bb8b9fed41e8cd14586df5aa83e8a8996"
integrity sha512-HFamiqgGiMRCbUBm3wx02WYWKb6ojke0WJHrg4QXI8tx35HrTmDiY8MksUXhouJtEkfcRwXjBL2cSEpRQ6+PgQ==
passport-saml@^2.0.0:
version "2.0.2"
resolved "https://registry.npmjs.org/passport-saml/-/passport-saml-2.0.2.tgz#41c459f0f4c4cf0374b161bf3f9ab008fcacb8fe"
integrity sha512-e8rT/nWEY3YalktzZlGXvnj6mLDQD4R+atfStCF4nHCbqK5IUpip+Bj2Wzbm9v5F/KI497k57kNXkHCLfDSVjA==
dependencies:
debug "^3.1.0"
passport-strategy "*"
q "^1.5.0"
xml-crypto "^1.4.0"
xml-crypto "^2.0.0"
xml-encryption "1.2.1"
xml2js "0.4.x"
xml2js "^0.4.23"
xmlbuilder "^11.0.0"
xmldom "0.1.x"
@@ -21043,7 +21108,7 @@ pupa@^2.0.1:
dependencies:
escape-goat "^2.0.0"
q@^1.1.2, q@^1.5.0, q@^1.5.1:
q@^1.1.2, q@^1.5.1:
version "1.5.1"
resolved "https://registry.npmjs.org/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
@@ -26074,10 +26139,10 @@ xml-but-prettier@^1.0.1:
dependencies:
repeat-string "^1.5.2"
xml-crypto@^1.4.0:
version "1.5.3"
resolved "https://registry.npmjs.org/xml-crypto/-/xml-crypto-1.5.3.tgz#a8f500b90f0dfaf0efa3331c345ecb0fff993c34"
integrity sha512-uHkmpUtX15xExe5iimPmakAZN+6CqIvjmaJTy4FwqGzaTjrKRBNeqMh8zGEzVNgW0dk6beFYpyQSgqV/J6C5xA==
xml-crypto@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/xml-crypto/-/xml-crypto-2.0.0.tgz#54cd268ad9d31930afcf7092cbb664258ca9e826"
integrity sha512-/a04qr7RpONRZHOxROZ6iIHItdsQQjN3sj8lJkYDDss8tAkEaAs0VrFjb3tlhmS5snQru5lTs9/5ISSMdPDHlg==
dependencies:
xmldom "0.1.27"
xpath "0.0.27"
@@ -26097,7 +26162,7 @@ xml-name-validator@^3.0.0:
resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==
xml2js@0.4.x:
xml2js@^0.4.23:
version "0.4.23"
resolved "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66"
integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==