Merge remote-tracking branch 'origin/master' into scaffolder-result-content
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend-module-msgraph': patch
|
||||
---
|
||||
|
||||
`MicrosoftGraphOrgEntityProvider`: Add option to configure schedule via `app-config.yaml` instead of in code.
|
||||
|
||||
Please find how to configure the schedule at the config at
|
||||
https://github.com/backstage/backstage/tree/master/plugins/catalog-backend-module-msgraph#readme
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/integration': minor
|
||||
---
|
||||
|
||||
This patch brings Gitea as a valid integration: target, via the ScmIntegration interface. It adds gitea to the relevant static properties (get integration by name, get integration by type) for plugins to be able to reference the same Gitea server.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-common': patch
|
||||
---
|
||||
|
||||
This patch adds GiteaURLReader to the available classes. It currently only reads single files via gitea's public repos api
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-import': patch
|
||||
---
|
||||
|
||||
Get rid of `this-is-undefined-in-esm` warning
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-react': patch
|
||||
---
|
||||
|
||||
Removed forced capitalization for Entity types in the catalog sidebar.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-tasks': patch
|
||||
---
|
||||
|
||||
Deprecated the `HumanDuration` type, which should now instead be imported from `@backstage/types`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-common': patch
|
||||
---
|
||||
|
||||
Small update to fix compatibility with newer versions of the `keyv` library
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend-module-msgraph': patch
|
||||
---
|
||||
|
||||
Add `microsoftGraphOrgEntityProviderCatalogModule` (new backend-plugin-api, alpha).
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/types': patch
|
||||
---
|
||||
|
||||
Added the `HumanDuration` type, moved here from `@backstage/backend-tasks`. This type matches the `Duration.fromObject` form of `luxon`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/create-app': patch
|
||||
---
|
||||
|
||||
Updated the create-app command to no longer require Git to be installed and configured. A git repository will only be initialized if possible and if not already in an git repository.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-circleci': patch
|
||||
---
|
||||
|
||||
Update screenshots in documentation to match latest CircleCI plugin
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-tech-insights-backend': patch
|
||||
'@backstage/plugin-tech-insights-node': patch
|
||||
---
|
||||
|
||||
Add a default delay to the fact retrievers to prevent cold-start errors
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-tech-insights-backend': patch
|
||||
'@backstage/plugin-tech-insights-node': patch
|
||||
---
|
||||
|
||||
Use `HumanDuration` from `@backstage/types`
|
||||
@@ -121,6 +121,8 @@ github
|
||||
Gitiles
|
||||
gitlab
|
||||
GitLab
|
||||
gitea
|
||||
Gitea
|
||||
Gource
|
||||
Grafana
|
||||
graphql
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
name: Sync Snyk GitHub issues
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 */4 * * *'
|
||||
|
||||
@@ -14,7 +15,7 @@ jobs:
|
||||
node-version: [14.x]
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
NODE_OPTIONS: --max-old-space-size=7168
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
id: backend
|
||||
title: Backend
|
||||
description: About Backend
|
||||
---
|
||||
|
||||
## TODO
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
id: locations
|
||||
title: Gitea Locations
|
||||
sidebar_label: Locations
|
||||
description: Integrating source code stored in Gitea into the Backstage catalog
|
||||
---
|
||||
|
||||
The Gitea integration supports loading catalog entities from a hosted repository. Entities can be added to
|
||||
[static catalog configuration](../../features/software-catalog/configuration.md),
|
||||
registered with the
|
||||
[catalog-import](https://github.com/backstage/backstage/tree/master/plugins/catalog-import)
|
||||
plugin.
|
||||
|
||||
## Configuration
|
||||
|
||||
To use this integration, add configuration to your root `app-config.yaml`:
|
||||
|
||||
```yaml
|
||||
integrations:
|
||||
gitea:
|
||||
- host: gitea.example.com
|
||||
password: ${GITEA_TOKEN}
|
||||
- host: gitea.example.com
|
||||
username: ${GITEA_USERNAME}
|
||||
password: ${GITEA_PASSWORD}
|
||||
```
|
||||
|
||||
Directly under the `gitea` key is a list of provider configurations, where you
|
||||
can list the Gitea instances you want to be able to fetch
|
||||
data from. Each entry is a structure with up to four elements:
|
||||
|
||||
- `host`: The host of the gitea instance that you want to match on.
|
||||
- `baseUrl` (optional): Needed if the Gitea instance is not reachable at
|
||||
the base of the `host` option (e.g. `https://git.company.com/gitea`). This is the address that you would open in a browser.
|
||||
- `username` (optional): The gitea username to use in API requests.
|
||||
- `password` (optional): The password or api token to authenticate with.
|
||||
|
||||
You may supply only the `password` field, if authenticating via API access tokens (generated in Settings > Applications).
|
||||
@@ -11,7 +11,7 @@ For some use cases, you may want to define custom [rules](./concepts.md#resource
|
||||
Plugins should export a rule factory that provides type-safety that ensures compatibility with the plugin's backend. The catalog plugin exports `createCatalogPermissionRule` from `@backstage/plugin-catalog-backend/alpha` for this purpose. Note: the `/alpha` path segment is temporary until this API is marked as stable. For this example, we'll define the rule in `packages/backend/src/plugins/permission.ts`, but you can put it anywhere that's accessible by your `backend` package.
|
||||
|
||||
```typescript
|
||||
import type { Entity } from '@backstage/plugin-catalog-model';
|
||||
import type { Entity } from '@backstage/catalog-model';
|
||||
import { createCatalogPermissionRule } from '@backstage/plugin-catalog-backend/alpha';
|
||||
import { createConditionFactory } from '@backstage/plugin-permission-node';
|
||||
import { z } from 'zod';
|
||||
|
||||
@@ -53,7 +53,6 @@ $ yarn workspace backend add @backstage/plugin-permission-backend
|
||||
2. Add the following to a new file, `packages/backend/src/plugins/permission.ts`. This adds the permission-backend router, and configures it with a policy which allows everything.
|
||||
|
||||
```typescript
|
||||
import { IdentityClient } from '@backstage/plugin-auth-node';
|
||||
import { createRouter } from '@backstage/plugin-permission-backend';
|
||||
import {
|
||||
AuthorizeResult,
|
||||
@@ -119,7 +118,6 @@ permission:
|
||||
2. Update the PermissionPolicy in `packages/backend/src/plugins/permission.ts` to disable a permission that’s easy for us to test. This policy rejects any attempt to delete a catalog entity:
|
||||
|
||||
```diff
|
||||
import { IdentityClient } from '@backstage/plugin-auth-node';
|
||||
import { createRouter } from '@backstage/plugin-permission-backend';
|
||||
import {
|
||||
AuthorizeResult,
|
||||
|
||||
@@ -48,7 +48,9 @@ Edit `plugins/todo-list-backend/src/service/router.ts`:
|
||||
...
|
||||
|
||||
- import { InputError } from '@backstage/errors';
|
||||
- import { IdentityApi } from '@backstage/plugin-auth-node';
|
||||
+ import { InputError, NotAllowedError } from '@backstage/errors';
|
||||
+ import { getBearerTokenFromAuthorizationHeader, IdentityApi } from '@backstage/plugin-auth-node';
|
||||
+ import { PermissionEvaluator, AuthorizeResult } from '@backstage/plugin-permission-common';
|
||||
+ import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
|
||||
|
||||
@@ -56,7 +58,7 @@ Edit `plugins/todo-list-backend/src/service/router.ts`:
|
||||
|
||||
export interface RouterOptions {
|
||||
logger: Logger;
|
||||
identity: IdentityClient;
|
||||
identity: IdentityApi;
|
||||
+ permissions: PermissionEvaluator;
|
||||
}
|
||||
|
||||
@@ -69,11 +71,13 @@ Edit `plugins/todo-list-backend/src/service/router.ts`:
|
||||
...
|
||||
|
||||
router.post('/todos', async (req, res) => {
|
||||
const token = IdentityClient.getBearerToken(req.header('authorization'));
|
||||
let author: string | undefined = undefined;
|
||||
|
||||
const user = token ? await identity.authenticate(token) : undefined;
|
||||
const user = await identity.getIdentity({ request: req });
|
||||
author = user?.identity.userEntityRef;
|
||||
+ const token = getBearerTokenFromAuthorizationHeader(
|
||||
+ req.header('authorization'),
|
||||
+ );
|
||||
+ const decision = (
|
||||
+ await permissions.authorize([{ permission: todoListCreatePermission }], {
|
||||
+ token,
|
||||
@@ -128,10 +132,8 @@ In order to test the logic above, the integrators of your backstage instance nee
|
||||
```diff
|
||||
// packages/backend/src/plugins/permission.ts
|
||||
|
||||
- import { IdentityClient } from '@backstage/plugin-auth-node';
|
||||
+ import {
|
||||
+ BackstageIdentityResponse,
|
||||
+ IdentityClient
|
||||
+ } from '@backstage/plugin-auth-node';
|
||||
import {
|
||||
PermissionPolicy,
|
||||
@@ -170,3 +172,117 @@ Let's flip the result back to `ALLOW` before moving on.
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
At this point everything is working but if you run `yarn tsc` you'll get some errors, let's fix those up.
|
||||
|
||||
First we'll clean up the `plugins/todo-list-backend/src/service/router.test.ts`:
|
||||
|
||||
```diff
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { DefaultIdentityClient } from '@backstage/plugin-auth-node';
|
||||
+ import { PermissionEvaluator } from '@backstage/plugin-permission-common';
|
||||
import express from 'express';
|
||||
import request from 'supertest';
|
||||
|
||||
import { createRouter } from './router';
|
||||
|
||||
+ const mockedAuthorize: jest.MockedFunction<PermissionEvaluator['authorize']> =
|
||||
+ jest.fn();
|
||||
+ const mockedPermissionQuery: jest.MockedFunction<
|
||||
+ PermissionEvaluator['authorizeConditional']
|
||||
+ > = jest.fn();
|
||||
|
||||
+ const permissionEvaluator: PermissionEvaluator = {
|
||||
+ authorize: mockedAuthorize,
|
||||
+ authorizeConditional: mockedPermissionQuery,
|
||||
+ };
|
||||
|
||||
describe('createRouter', () => {
|
||||
let app: express.Express;
|
||||
|
||||
beforeAll(async () => {
|
||||
const router = await createRouter({
|
||||
logger: getVoidLogger(),
|
||||
identity: {} as DefaultIdentityClient,
|
||||
+ permissions: toPermissionEvaluator,
|
||||
});
|
||||
app = express().use(router);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('GET /health', () => {
|
||||
it('returns ok', async () => {
|
||||
const response = await request(app).get('/health');
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual({ status: 'ok' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
Then we want to update the `plugins/todo-list-backend/src/service/standaloneServer.ts`, first we need to add the `@backstage/plugin-permission-node` package to `plugins/todo-list-backend/package.json` and then we can make the following edits:
|
||||
|
||||
```diff
|
||||
import {
|
||||
createServiceBuilder,
|
||||
loadBackendConfig,
|
||||
SingleHostDiscovery,
|
||||
+ ServerTokenManager,
|
||||
} from '@backstage/backend-common';
|
||||
import { DefaultIdentityClient } from '@backstage/plugin-auth-node';
|
||||
import { ServerPermissionClient } from '@backstage/plugin-permission-node';
|
||||
import { Server } from 'http';
|
||||
import { Logger } from 'winston';
|
||||
import { createRouter } from './router';
|
||||
|
||||
export interface ServerOptions {
|
||||
port: number;
|
||||
enableCors: boolean;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
export async function startStandaloneServer(
|
||||
options: ServerOptions,
|
||||
): Promise<Server> {
|
||||
const logger = options.logger.child({ service: 'todo-list-backend' });
|
||||
logger.debug('Starting application server...');
|
||||
const config = await loadBackendConfig({ logger, argv: process.argv });
|
||||
const discovery = SingleHostDiscovery.fromConfig(config);
|
||||
+ const tokenManager = ServerTokenManager.fromConfig(config, {
|
||||
+ logger,
|
||||
+ });
|
||||
+ const permissions = ServerPermissionClient.fromConfig(config, {
|
||||
+ discovery,
|
||||
+ tokenManager,
|
||||
+ });
|
||||
const router = await createRouter({
|
||||
logger,
|
||||
identity: DefaultIdentityClient.create({
|
||||
discovery,
|
||||
issuer: await discovery.getExternalBaseUrl('auth'),
|
||||
}),
|
||||
+ permissions,
|
||||
});
|
||||
|
||||
let service = createServiceBuilder(module)
|
||||
.setPort(options.port)
|
||||
.addRouter('/todo-list', router);
|
||||
if (options.enableCors) {
|
||||
service = service.enableCors({ origin: 'http://localhost:3000' });
|
||||
}
|
||||
|
||||
return await service.start().catch(err => {
|
||||
logger.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.hot?.accept();
|
||||
```
|
||||
|
||||
Now when you run `yarn tsc` you should have no more errors.
|
||||
|
||||
@@ -118,7 +118,7 @@ Providing a disabled state can be a helpful signal to users, but there may be ca
|
||||
- <Grid item>
|
||||
- <AddTodo onAdd={handleAdd} />
|
||||
- </Grid>
|
||||
+ <RequirePermission permission={todoListCreatePermission}>
|
||||
+ <RequirePermission permission={todoListCreatePermission} errorPage={<></>}>
|
||||
+ <Grid item>
|
||||
+ <AddTodo onAdd={handleAdd} />
|
||||
+ </Grid>
|
||||
@@ -165,3 +165,25 @@ Providing a disabled state can be a helpful signal to users, but there may be ca
|
||||
```
|
||||
|
||||
Now you should find that the component for adding a todo list item does not render at all. Success!
|
||||
|
||||
You can also use `RequirePermission` to prevent access to routes as well. Here's how that would look in your `packages/app/src/App.tsx`:
|
||||
|
||||
```diff
|
||||
+ import { RequirePermission } from '@backstage/plugin-permission-react';
|
||||
+ import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
|
||||
|
||||
...
|
||||
|
||||
<Route path="/search" element={<SearchPage />}>
|
||||
{searchPage}
|
||||
</Route>
|
||||
<Route path="/settings" element={<UserSettingsPage />} />
|
||||
+ <Route path="/todo-list" element={
|
||||
// You might want to create a "read" permission for this, we are just using this one as an example
|
||||
+ <RequirePermission permission={todoListCreatePermission}>
|
||||
+ <TodoListPage />
|
||||
+ </RequirePermission>
|
||||
</FlatRoutes>
|
||||
```
|
||||
|
||||
Now if you try to navigate to `https://localhost:3000/todo-list` you'll get and error page if you do not have permission.
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
---
|
||||
id: new-backend-system
|
||||
title: New Backend System
|
||||
description: Details of the upcoming backend system
|
||||
---
|
||||
|
||||
> **DISCLAIMER: The new backend system is under active development and is not considered stable**
|
||||
|
||||
## Status
|
||||
|
||||
The new backend system is under active development, and only a small number of plugins and services have been migrated so far. It is possible to try it out, but it is not recommended to use this new system in production yet.
|
||||
|
||||
You can find an example backend setup at https://github.com/backstage/backstage/tree/master/packages/backend-next.
|
||||
|
||||
## Overview
|
||||
|
||||
The new Backstage backend system is being built to help make it simpler to install backend plugins and keep projects up to date. It also changes the foundation to one that makes it a lot easier to evolve plugins and the system itself. You can read more about the reasoning in the [original RFC](https://github.com/backstage/backstage/issues/11611).
|
||||
|
||||
One of the goals of the new system was to reduce the code needed for setting up a Backstage backend and installing plugins. This is an example of how you create, add features, and start up your backend in the new system:
|
||||
|
||||
```ts
|
||||
import { createBackend } from '@backstage/backend-defaults';
|
||||
import { catalogPlugin } from '@backstage/plugin-catalog-backend';
|
||||
|
||||
// Create your backend instance
|
||||
const backend = createBackend();
|
||||
|
||||
// Install all desired features
|
||||
backend.add(catalogPlugin());
|
||||
|
||||
// Start up the backend
|
||||
await backend.start();
|
||||
```
|
||||
|
||||
One notable change that helped achieve this much slimmer backend setup is the introduction of dependency injection, with a system that is very similar to the one in the Backstage frontend.
|
||||
|
||||
## Building Blocks
|
||||
|
||||
This section introduces the high-level building blocks upon which this new system is built. These are all concepts that exist in our current system in one way or another, but the have all been lifted up to be first class concerns in the new system.
|
||||
|
||||
### Backend
|
||||
|
||||
This is the backend instance itself, which you can think of as the unit of deployment. It does not have any functionality in itself, but is simply responsible for wiring things together.
|
||||
|
||||
It is up to you to decide how many different backends you want to deploy. You can have all features in a single one, or split things out into multiple smaller deployments. All depending on your need to scale and isolate individual features.
|
||||
|
||||
### Plugins
|
||||
|
||||
Plugins provide the actual features, just like in our existing system. They operate completely independently of each other. If plugins what to communicate with each other, they must do so over the wire. There can be no direct communication between plugins through code. Because of this constraints, each plugins can be considered to be its own microservice.
|
||||
|
||||
### Services
|
||||
|
||||
Services provide utilities to help make it simpler to implement plugins, so that each plugin doesn't need to implement everything from scratch. There are both many built-in services, like the ones for logging, database access, and reading configuration, but you can also import third-party services, or create your own.
|
||||
|
||||
Services are also a customization point for individual backend installations. You can both override services with your own implementations, as well as make smaller customizations to existing services.
|
||||
|
||||
### Extension Points
|
||||
|
||||
Many plugins have ways in which you can extend them, for example entity providers for the Catalog, or custom actions for the Scaffolder. These extension patterns are now encoded into Extension Points.
|
||||
|
||||
Extension Points look a little bit like services, since you depended on them just like you would a service. A key difference is that extension points are registered and provided by plugins themselves, based on what customizations each individual plugin wants to expose.
|
||||
|
||||
Extension Points are also exported separately from the plugin instance itself, and a single plugin can also expose multiple different extension points at once. This makes it easier to evolve and deprecated individual Extension Points over time, rather than dealing with a single large API surface.
|
||||
|
||||
### Modules
|
||||
|
||||
Modules use the plugin Extension Points to add new features for plugins. They might for example add an individual Catalog Entity Provider, or one or more Scaffolder Actions. Modules are basically plugins for plugins.
|
||||
|
||||
Each module may only extend a single plugin, and the module must be deployed together with that plugin in the same backend instance. Modules may however only communicate with their plugin through its registered extension points.
|
||||
|
||||
Just like plugins, modules also have access to services and can depend on their own service implementations. They will however share services with the plugin that they extend, there are no module-specific service implementations.
|
||||
|
||||
## Creating Plugins
|
||||
|
||||
Plugins are created using the `createBackendPlugin` function. All plugins must have an ID and a register method. Plugins may also accept an options object, which can be either optional or required. The options are passed to the second parameter of the register method, and the options type is inferred and forwarded to the returned plugin factory function.
|
||||
|
||||
```ts
|
||||
import {
|
||||
configServiceRef,
|
||||
createBackendPlugin,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
|
||||
// export type ExamplePluginOptions = { exampleOption: boolean };
|
||||
export const examplePlugin = createBackendPlugin({
|
||||
// unique id for the plugin
|
||||
id: 'example',
|
||||
// It's possible to provide options to the plugin
|
||||
// register(env, options: ExamplePluginOptions) {
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
logger: loggerServiceRef,
|
||||
},
|
||||
// logger is provided by the backend based on the dependency on loggerServiceRef above.
|
||||
async init({ logger }) {
|
||||
logger.info('Hello from example plugin');
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The plugin can then be installed in the backend using the returned plugin factory function:
|
||||
|
||||
```ts
|
||||
backend.add(examplePlugin());
|
||||
```
|
||||
|
||||
If we wanted our plugin to accept options as well, we'd accept the options as the second parameter of the register method:
|
||||
|
||||
```ts
|
||||
export const examplePlugin = createBackendPlugin({
|
||||
id: 'example',
|
||||
register(env, options?: { silent?: boolean }) {
|
||||
env.registerInit({
|
||||
deps: { logger: loggerServiceRef },
|
||||
async init({ logger }) {
|
||||
if (!options?.silent) {
|
||||
logger.info('Hello from example plugin');
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Passing the option to the plugin during installation looks like this:
|
||||
|
||||
```ts
|
||||
backend.add(examplePlugin({ silent: true }));
|
||||
```
|
||||
|
||||
## Creating Modules
|
||||
|
||||
Some facts about modules
|
||||
|
||||
- A Module is able to extend a plugin with additional functionality using the `ExtensionPoint`s registered by the plugin.
|
||||
- A module can only extend one plugin but can interact with multiple `ExtensionPoint`s registered by that plugin.
|
||||
- A module is always initialized before the plugin it extends.
|
||||
|
||||
A module depends on the `ExtensionPoint`s exported by the target plugin's library package, for example `@backstage/plugin-catalog-node`, and does not directly declare a dependency on the plugin package itself.
|
||||
|
||||
Here's an example on how to create a module that adds a new processor using the `catalogProcessingExtensionPoint`:
|
||||
|
||||
```ts
|
||||
import { createBackendModule } from '@backstage/backend-plugin-api';
|
||||
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node';
|
||||
import { MyCustomProcessor } from './processor';
|
||||
|
||||
export const exampleCustomProcessorCatalogModule = createBackendModule({
|
||||
moduleId: 'exampleCustomProcessor',
|
||||
pluginId: 'catalog',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
catalog: catalogProcessingExtensionPoint,
|
||||
},
|
||||
async init({ catalog }) {
|
||||
catalog.addProcessor(new MyCustomProcessor());
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Extension Points
|
||||
|
||||
Modules depend on extension points just as a regular dependency by specifying it in the `deps` section.
|
||||
|
||||
#### Defining an Extension Point
|
||||
|
||||
```ts
|
||||
import { createExtensionPoint } from '@backstage/backend-plugin-api';
|
||||
|
||||
export interface ScaffolderActionsExtensionPoint {
|
||||
addAction(action: ScaffolderAction): void;
|
||||
}
|
||||
|
||||
export const scaffolderActionsExtensionPoint =
|
||||
createExtensionPoint<ScaffolderActionsExtensionPoint>({
|
||||
id: 'scaffolder.actions',
|
||||
});
|
||||
```
|
||||
|
||||
#### Registering an Extension Point
|
||||
|
||||
Extension points are registered by a plugin and extended by modules.
|
||||
|
||||
## Backend Services
|
||||
|
||||
The default backend provides several _services_ out of the box which includes access to configuration, logging, databases and more.
|
||||
Service dependencies are declared using their `ServiceRef`s in the `deps` section of the plugin or module, and the implementations are then forwarded to the `init` method of the plugin or module.
|
||||
|
||||
### Service References
|
||||
|
||||
A `ServiceRef` is a named reference to an interface which are later used to resolve the concrete service implementation. Conceptually this is very similar to `ApiRef`s in the frontend.
|
||||
Services is what provides common utilities that previously resided in the `PluginEnvironment` such as Config, Logging and Database.
|
||||
|
||||
On startup the backend will make sure that the services are initialized before being passed to the plugin/module that depend on them.
|
||||
ServiceRefs contain a scope which is used to determine if the serviceFactory creating the service will create a new instance scoped per plugin/module or if it will be shared. `plugin` scoped services will be created once per plugin/module and `root` scoped services will be created once per backend instance.
|
||||
|
||||
#### Defining a Service
|
||||
|
||||
```ts
|
||||
import {
|
||||
createServiceFactory,
|
||||
pluginMetadataServiceRef,
|
||||
loggerServiceRef,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { ExampleImpl } from './ExampleImpl';
|
||||
|
||||
export interface ExampleApi {
|
||||
doSomething(): Promise<void>;
|
||||
}
|
||||
|
||||
export const exampleServiceRef = createServiceRef<ExampleApi>({
|
||||
id: 'example',
|
||||
scope: 'plugin', // can be 'root' or 'plugin'
|
||||
|
||||
// The defaultFactory is optional to implement but it will be used if no other factory is provided to the backend.
|
||||
// This is allows for the backend to provide a default implementation of the service without having to wire it beforehand.
|
||||
defaultFactory: async service =>
|
||||
createServiceFactory({
|
||||
service,
|
||||
deps: {
|
||||
logger: loggerServiceRef,
|
||||
plugin: pluginMetadataServiceRef,
|
||||
},
|
||||
// Logger is available directly in the factory as it's a root scoped service and will be created once per backend instance.
|
||||
async factory({ logger }) {
|
||||
// plugin is available as it's a plugin scoped service and will be created once per plugin.
|
||||
return async ({ plugin }) => {
|
||||
// This block will be executed once for every plugin that depends on this service
|
||||
logger.info('Initializing example service plugin instance');
|
||||
return new ExampleImpl({ logger, plugin });
|
||||
};
|
||||
},
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
### Overriding Services
|
||||
|
||||
In this example we replace the default root logger service implementation with a custom one that streams logs to GCP. The `rootLoggerServiceRef` has a `'root'` scope, meaning there are no plugin-specific instances of this service.
|
||||
|
||||
```ts
|
||||
import {
|
||||
createServiceFactory,
|
||||
rootLoggerServiceRef,
|
||||
LoggerService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
|
||||
// This custom implementation would typically live separately from
|
||||
// the backend setup code, either nearby such as in
|
||||
// packages/backend/src/services/logger/GoogleCloudLogger.ts
|
||||
// Or you can let it live in its own library package.
|
||||
class GoogleCloudLogger implements LoggerService {
|
||||
static factory = createServiceFactory({
|
||||
service: rootLoggerServiceRef,
|
||||
deps: {},
|
||||
async factory() {
|
||||
return new GoogleCloudLogger();
|
||||
},
|
||||
});
|
||||
// custom implementation here ...
|
||||
}
|
||||
|
||||
// packages/backend/src/index.ts
|
||||
const backend = createBackend({
|
||||
services: [
|
||||
// supplies additional or replacement services to the backend
|
||||
GoogleCloudLogger.factory(),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Utilities for testing backend plugins and modules are available in `@backstage/backend-test-utils`.
|
||||
|
||||
```ts
|
||||
import { startTestBackend } from '@backstage/backend-test-utils';
|
||||
|
||||
describe('Example', () => {
|
||||
it('should do something', async () => {
|
||||
await startTestBackend({
|
||||
// mock services can be provided to the backend
|
||||
services: [someServiceFactory],
|
||||
// plugins and modules for testing
|
||||
features: [testModule()],
|
||||
});
|
||||
|
||||
// assertions
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Package structure
|
||||
|
||||
A detailed explanation of the package architecture can be found in the [Backstage Architecture Overview](../overview/architecture-overview.md#package-architecture). The most important packages to consider for this system are `backend`, `plugin-<pluginId>-backend`, `plugin-<pluginId>-node`, and `plugin-<pluginId>-backend-module-<moduleId>`.
|
||||
|
||||
- `plugin-<pluginId>-backend` houses the implementation of the plugins themselves.
|
||||
- `plugin-<pluginId>-node` houses the extension points and any other utilities that modules or other plugins might need.
|
||||
- `plugin-<pluginId>-backend-module-<moduleId>` houses the modules that extend the plugins via the extension points.
|
||||
- `backend` is the backend itself that wires everything together to something that you can deploy.
|
||||
@@ -6,6 +6,7 @@ const redirects = {
|
||||
'bind-routes':
|
||||
'/docs/plugins/composability#binding-external-routes-in-the-app',
|
||||
'scm-auth': '/docs/auth/#scaffolder-configuration-software-templates',
|
||||
'backend-system': '/docs/plugins/new-backend-system',
|
||||
};
|
||||
const fallback = '/docs';
|
||||
|
||||
|
||||
@@ -200,6 +200,11 @@
|
||||
"integrations/gitlab/discovery"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "subcategory",
|
||||
"label": "Gitea",
|
||||
"ids": ["integrations/gitea/locations"]
|
||||
},
|
||||
{
|
||||
"type": "subcategory",
|
||||
"label": "Google GCS",
|
||||
@@ -230,7 +235,8 @@
|
||||
"plugins/proxying",
|
||||
"plugins/backend-plugin",
|
||||
"plugins/call-existing-api",
|
||||
"plugins/url-reader"
|
||||
"plugins/url-reader",
|
||||
"plugins/new-backend-system"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -137,6 +137,7 @@ nav:
|
||||
- Backend plugin: 'plugins/backend-plugin.md'
|
||||
- Call existing API: 'plugins/call-existing-api.md'
|
||||
- URL Reader: 'plugins/url-reader.md'
|
||||
- New Backend System: 'plugins/new-backend-system.md'
|
||||
- Testing:
|
||||
- Testing with Jest: 'plugins/testing.md'
|
||||
- Publishing:
|
||||
|
||||
@@ -21,6 +21,7 @@ import { Duration } from 'luxon';
|
||||
import { ErrorRequestHandler } from 'express';
|
||||
import express from 'express';
|
||||
import { GerritIntegration } from '@backstage/integration';
|
||||
import { GiteaIntegration } from '@backstage/integration';
|
||||
import { GithubCredentialsProvider } from '@backstage/integration';
|
||||
import { GithubIntegration } from '@backstage/integration';
|
||||
import { GitLabIntegration } from '@backstage/integration';
|
||||
@@ -406,6 +407,23 @@ export class Git {
|
||||
resolveRef(options: { dir: string; ref: string }): Promise<string>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export class GiteaUrlReader implements UrlReader {
|
||||
constructor(integration: GiteaIntegration);
|
||||
// (undocumented)
|
||||
static factory: ReaderFactory;
|
||||
// (undocumented)
|
||||
read(url: string): Promise<Buffer>;
|
||||
// (undocumented)
|
||||
readTree(): Promise<ReadTreeResponse>;
|
||||
// (undocumented)
|
||||
readUrl(url: string, options?: ReadUrlOptions): Promise<ReadUrlResponse>;
|
||||
// (undocumented)
|
||||
search(): Promise<SearchResponse>;
|
||||
// (undocumented)
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export class GithubUrlReader implements UrlReader {
|
||||
constructor(
|
||||
|
||||
+4
-2
@@ -14,11 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Store } from 'keyv';
|
||||
|
||||
/**
|
||||
* Storage class compatible with Keyv which always results in a no-op. This is
|
||||
* used when no cache store is configured in a Backstage backend instance.
|
||||
*/
|
||||
export class NoStore extends Map<string, any> {
|
||||
export class NoStore implements Store<string | undefined> {
|
||||
clear(): void {
|
||||
return;
|
||||
}
|
||||
@@ -28,7 +30,7 @@ export class NoStore extends Map<string, any> {
|
||||
}
|
||||
|
||||
get(_key: string) {
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
has(_key: string): boolean {
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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.
|
||||
*/
|
||||
|
||||
import { setupRequestMockHandlers } from '@backstage/backend-test-utils';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { GiteaIntegration, readGiteaConfig } from '@backstage/integration';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { getVoidLogger } from '../logging';
|
||||
import { UrlReaderPredicateTuple } from './types';
|
||||
import { DefaultReadTreeResponseFactory } from './tree';
|
||||
import getRawBody from 'raw-body';
|
||||
import { GiteaUrlReader } from './GiteaUrlReader';
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
|
||||
const treeResponseFactory = DefaultReadTreeResponseFactory.create({
|
||||
config: new ConfigReader({}),
|
||||
});
|
||||
|
||||
jest.mock('../scm', () => ({
|
||||
Git: {
|
||||
fromAuth: () => ({
|
||||
clone: jest.fn(() => Promise.resolve({})),
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
const giteaProcessor = new GiteaUrlReader(
|
||||
new GiteaIntegration(
|
||||
readGiteaConfig(
|
||||
new ConfigReader({
|
||||
host: 'gitea.com',
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const createReader = (config: JsonObject): UrlReaderPredicateTuple[] => {
|
||||
return GiteaUrlReader.factory({
|
||||
config: new ConfigReader(config),
|
||||
logger: getVoidLogger(),
|
||||
treeResponseFactory,
|
||||
});
|
||||
};
|
||||
|
||||
describe('GiteaUrlReader', () => {
|
||||
const worker = setupServer();
|
||||
setupRequestMockHandlers(worker);
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('reader factory', () => {
|
||||
it('creates a reader.', () => {
|
||||
const readers = createReader({
|
||||
integrations: {
|
||||
gitea: [{ host: 'gitea.com' }],
|
||||
},
|
||||
});
|
||||
expect(readers).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should not create a default entry.', () => {
|
||||
const readers = createReader({
|
||||
integrations: {},
|
||||
});
|
||||
expect(readers).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('predicates', () => {
|
||||
it('returns true for the configured host', () => {
|
||||
const readers = createReader({
|
||||
integrations: {
|
||||
gitea: [{ host: 'gitea.com' }],
|
||||
},
|
||||
});
|
||||
const predicate = readers[0].predicate;
|
||||
|
||||
expect(predicate(new URL('https://gitea.com/path'))).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for a different host.', () => {
|
||||
const readers = createReader({
|
||||
integrations: {
|
||||
gitea: [{ host: 'gitea.com' }],
|
||||
},
|
||||
});
|
||||
const predicate = readers[0].predicate;
|
||||
|
||||
expect(predicate(new URL('https://github.com/path'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('readUrl', () => {
|
||||
const responseBuffer = Buffer.from('Apache License');
|
||||
const giteaApiResponse = (content: any) => {
|
||||
return JSON.stringify({
|
||||
encoding: 'base64',
|
||||
content: Buffer.from(content).toString('base64'),
|
||||
});
|
||||
};
|
||||
|
||||
it('should be able to read file contents as buffer', async () => {
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://gitea.com/api/v1/repos/owner/project/contents/LICENSE',
|
||||
(req, res, ctx) => {
|
||||
// Test utils prefers matching URL directly but it is part of Gitea's API
|
||||
if (req.url.searchParams.get('ref') === 'branch2') {
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.body(giteaApiResponse(responseBuffer.toString())),
|
||||
);
|
||||
}
|
||||
|
||||
return res(ctx.status(500));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const result = await giteaProcessor.readUrl(
|
||||
'https://gitea.com/owner/project/src/branch/branch2/LICENSE',
|
||||
);
|
||||
const buffer = await result.buffer();
|
||||
expect(buffer.toString()).toBe(responseBuffer.toString());
|
||||
});
|
||||
|
||||
it('should be able to read file contents as stream', async () => {
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://gitea.com/api/v1/repos/owner/project/contents/LICENSE',
|
||||
(req, res, ctx) => {
|
||||
if (req.url.searchParams.get('ref') === 'branch2') {
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.body(giteaApiResponse(responseBuffer.toString())),
|
||||
);
|
||||
}
|
||||
|
||||
return res(ctx.status(500));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const result = await giteaProcessor.readUrl(
|
||||
'https://gitea.com/owner/project/src/branch/branch2/LICENSE',
|
||||
);
|
||||
const fromStream = await getRawBody(result.stream!());
|
||||
expect(fromStream.toString()).toBe(responseBuffer.toString());
|
||||
});
|
||||
|
||||
it('should raise NotFoundError on 404.', async () => {
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://gitea.com/api/v1/repos/owner/project/contents/LICENSE',
|
||||
(_, res, ctx) => {
|
||||
return res(ctx.status(404, 'File not found.'));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await expect(
|
||||
giteaProcessor.readUrl(
|
||||
'https://gitea.com/owner/project/src/branch/branch2/LICENSE',
|
||||
),
|
||||
).rejects.toThrow(NotFoundError);
|
||||
});
|
||||
|
||||
it('should throw an error on non 404 errors.', async () => {
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://gitea.com/api/v1/repos/owner/project/contents/LICENSE',
|
||||
(_, res, ctx) => {
|
||||
return res(ctx.status(500, 'Error!!!'));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await expect(
|
||||
giteaProcessor.readUrl(
|
||||
'https://gitea.com/owner/project/src/branch/branch2/LICENSE',
|
||||
),
|
||||
).rejects.toThrow(
|
||||
'https://gitea.com/owner/project/src/branch/branch2/LICENSE could not be read as https://gitea.com/api/v1/repos/owner/project/contents/LICENSE?ref=branch2, 500 Error!!!',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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.
|
||||
*/
|
||||
import {
|
||||
getGiteaRequestOptions,
|
||||
getGiteaFileContentsUrl,
|
||||
GiteaIntegration,
|
||||
ScmIntegrations,
|
||||
} from '@backstage/integration';
|
||||
import { ReadUrlOptions, ReadUrlResponse } from './types';
|
||||
import {
|
||||
ReaderFactory,
|
||||
ReadTreeResponse,
|
||||
SearchResponse,
|
||||
UrlReader,
|
||||
} from './types';
|
||||
import fetch, { Response } from 'node-fetch';
|
||||
import { ReadUrlResponseFactory } from './ReadUrlResponseFactory';
|
||||
import {
|
||||
AuthenticationError,
|
||||
NotFoundError,
|
||||
NotModifiedError,
|
||||
} from '@backstage/errors';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
/**
|
||||
* Implements a {@link UrlReader} for the Gitea v1 api.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class GiteaUrlReader implements UrlReader {
|
||||
static factory: ReaderFactory = ({ config }) => {
|
||||
return ScmIntegrations.fromConfig(config)
|
||||
.gitea.list()
|
||||
.map(integration => {
|
||||
const reader = new GiteaUrlReader(integration);
|
||||
const predicate = (url: URL) => {
|
||||
return url.host === integration.config.host;
|
||||
};
|
||||
return { reader, predicate };
|
||||
});
|
||||
};
|
||||
|
||||
constructor(private readonly integration: GiteaIntegration) {}
|
||||
|
||||
async read(url: string): Promise<Buffer> {
|
||||
const response = await this.readUrl(url);
|
||||
return response.buffer();
|
||||
}
|
||||
|
||||
async readUrl(
|
||||
url: string,
|
||||
options?: ReadUrlOptions,
|
||||
): Promise<ReadUrlResponse> {
|
||||
let response: Response;
|
||||
const blobUrl = getGiteaFileContentsUrl(this.integration.config, url);
|
||||
|
||||
try {
|
||||
response = await fetch(blobUrl, {
|
||||
method: 'GET',
|
||||
...getGiteaRequestOptions(this.integration.config),
|
||||
signal: options?.signal as any,
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error(`Unable to read ${blobUrl}, ${e}`);
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
// Gitea returns an object with the file contents encoded, not the file itself
|
||||
const { encoding, content } = await response.json();
|
||||
|
||||
if (encoding === 'base64') {
|
||||
return ReadUrlResponseFactory.fromReadable(
|
||||
Readable.from(Buffer.from(content, 'base64')),
|
||||
{
|
||||
etag: response.headers.get('ETag') ?? undefined,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(`Unknown encoding: ${encoding}`);
|
||||
}
|
||||
|
||||
const message = `${url} could not be read as ${blobUrl}, ${response.status} ${response.statusText}`;
|
||||
if (response.status === 404) {
|
||||
throw new NotFoundError(message);
|
||||
}
|
||||
|
||||
if (response.status === 304) {
|
||||
throw new NotModifiedError();
|
||||
}
|
||||
|
||||
if (response.status === 403) {
|
||||
throw new AuthenticationError();
|
||||
}
|
||||
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
readTree(): Promise<ReadTreeResponse> {
|
||||
throw new Error('GiteaUrlReader readTree not implemented.');
|
||||
}
|
||||
search(): Promise<SearchResponse> {
|
||||
throw new Error('GiteaUrlReader search not implemented.');
|
||||
}
|
||||
|
||||
toString() {
|
||||
const { host } = this.integration.config;
|
||||
return `gitea{host=${host},authed=${Boolean(
|
||||
this.integration.config.password,
|
||||
)}}`;
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import { DefaultReadTreeResponseFactory } from './tree';
|
||||
import { FetchUrlReader } from './FetchUrlReader';
|
||||
import { GoogleGcsUrlReader } from './GoogleGcsUrlReader';
|
||||
import { AwsS3UrlReader } from './AwsS3UrlReader';
|
||||
import { GiteaUrlReader } from './GiteaUrlReader';
|
||||
|
||||
/**
|
||||
* Creation options for {@link UrlReaders}.
|
||||
@@ -89,6 +90,7 @@ export class UrlReaders {
|
||||
BitbucketUrlReader.factory,
|
||||
GerritUrlReader.factory,
|
||||
GithubUrlReader.factory,
|
||||
GiteaUrlReader.factory,
|
||||
GitlabUrlReader.factory,
|
||||
GoogleGcsUrlReader.factory,
|
||||
AwsS3UrlReader.factory,
|
||||
|
||||
@@ -21,6 +21,7 @@ export { BitbucketServerUrlReader } from './BitbucketServerUrlReader';
|
||||
export { GerritUrlReader } from './GerritUrlReader';
|
||||
export { GithubUrlReader } from './GithubUrlReader';
|
||||
export { GitlabUrlReader } from './GitlabUrlReader';
|
||||
export { GiteaUrlReader } from './GiteaUrlReader';
|
||||
export { AwsS3UrlReader } from './AwsS3UrlReader';
|
||||
export { FetchUrlReader } from './FetchUrlReader';
|
||||
export { ReadUrlResponseFactory } from './ReadUrlResponseFactory';
|
||||
|
||||
@@ -7,19 +7,11 @@ import { AbortSignal as AbortSignal_2 } from 'node-abort-controller';
|
||||
import { Config } from '@backstage/config';
|
||||
import { DatabaseManager } from '@backstage/backend-common';
|
||||
import { Duration } from 'luxon';
|
||||
import { HumanDuration as HumanDuration_2 } from '@backstage/types';
|
||||
import { Logger } from 'winston';
|
||||
|
||||
// @public
|
||||
export type HumanDuration = {
|
||||
years?: number;
|
||||
months?: number;
|
||||
weeks?: number;
|
||||
days?: number;
|
||||
hours?: number;
|
||||
minutes?: number;
|
||||
seconds?: number;
|
||||
milliseconds?: number;
|
||||
};
|
||||
// @public @deprecated
|
||||
export type HumanDuration = HumanDuration_2;
|
||||
|
||||
// @public
|
||||
export interface PluginTaskScheduler {
|
||||
@@ -59,10 +51,10 @@ export interface TaskScheduleDefinition {
|
||||
cron: string;
|
||||
}
|
||||
| Duration
|
||||
| HumanDuration;
|
||||
initialDelay?: Duration | HumanDuration;
|
||||
| HumanDuration_2;
|
||||
initialDelay?: Duration | HumanDuration_2;
|
||||
scope?: 'global' | 'local';
|
||||
timeout: Duration | HumanDuration;
|
||||
timeout: Duration | HumanDuration_2;
|
||||
}
|
||||
|
||||
// @public
|
||||
@@ -72,10 +64,10 @@ export interface TaskScheduleDefinitionConfig {
|
||||
cron: string;
|
||||
}
|
||||
| string
|
||||
| HumanDuration;
|
||||
initialDelay?: string | HumanDuration;
|
||||
| HumanDuration_2;
|
||||
initialDelay?: string | HumanDuration_2;
|
||||
scope?: 'global' | 'local';
|
||||
timeout: string | HumanDuration;
|
||||
timeout: string | HumanDuration_2;
|
||||
}
|
||||
|
||||
// @public
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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.
|
||||
*/
|
||||
|
||||
import { HumanDuration as TypesHumanDuration } from '@backstage/types';
|
||||
|
||||
/**
|
||||
* Human friendly durations object.
|
||||
*
|
||||
* @public
|
||||
* @deprecated Import from `@backstage/types` instead
|
||||
*/
|
||||
export type HumanDuration = TypesHumanDuration;
|
||||
@@ -21,3 +21,4 @@
|
||||
*/
|
||||
|
||||
export * from './tasks';
|
||||
export * from './deprecated';
|
||||
|
||||
@@ -23,5 +23,4 @@ export type {
|
||||
TaskRunner,
|
||||
TaskScheduleDefinition,
|
||||
TaskScheduleDefinitionConfig,
|
||||
HumanDuration,
|
||||
} from './types';
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
*/
|
||||
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { HumanDuration } from '@backstage/types';
|
||||
import { Duration } from 'luxon';
|
||||
import { readTaskScheduleDefinitionFromConfig } from './readTaskScheduleDefinitionFromConfig';
|
||||
import { HumanDuration } from './types';
|
||||
|
||||
describe('readTaskScheduleDefinitionFromConfig', () => {
|
||||
it('all valid values', () => {
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
*/
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { HumanDuration, TaskScheduleDefinition } from './types';
|
||||
import { HumanDuration, JsonObject } from '@backstage/types';
|
||||
import { TaskScheduleDefinition } from './types';
|
||||
import { Duration } from 'luxon';
|
||||
|
||||
const propsOfHumanDuration = [
|
||||
|
||||
@@ -14,26 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { HumanDuration } from '@backstage/types';
|
||||
import { CronTime } from 'cron';
|
||||
import { Duration } from 'luxon';
|
||||
import { AbortSignal } from 'node-abort-controller';
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Human friendly durations object
|
||||
* @public
|
||||
*/
|
||||
export type HumanDuration = {
|
||||
years?: number;
|
||||
months?: number;
|
||||
weeks?: number;
|
||||
days?: number;
|
||||
hours?: number;
|
||||
minutes?: number;
|
||||
seconds?: number;
|
||||
milliseconds?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* A function that can be called as a scheduled task.
|
||||
*
|
||||
|
||||
@@ -34,7 +34,6 @@
|
||||
"dependencies": {
|
||||
"@backstage/cli-common": "workspace:^",
|
||||
"chalk": "^4.0.0",
|
||||
"command-exists": "^1.2.9",
|
||||
"commander": "^9.1.0",
|
||||
"fs-extra": "10.1.0",
|
||||
"handlebars": "^4.7.3",
|
||||
|
||||
@@ -32,7 +32,7 @@ const promptMock = jest.spyOn(inquirer, 'prompt');
|
||||
const checkPathExistsMock = jest.spyOn(tasks, 'checkPathExistsTask');
|
||||
const templatingMock = jest.spyOn(tasks, 'templatingTask');
|
||||
const checkAppExistsMock = jest.spyOn(tasks, 'checkAppExistsTask');
|
||||
const initGitRepositoryMock = jest.spyOn(tasks, 'initGitRepository');
|
||||
const tryInitGitRepositoryMock = jest.spyOn(tasks, 'tryInitGitRepository');
|
||||
const readGitConfig = jest.spyOn(tasks, 'readGitConfig');
|
||||
const createTemporaryAppFolderMock = jest.spyOn(
|
||||
tasks,
|
||||
@@ -59,8 +59,6 @@ describe('command entrypoint', () => {
|
||||
dbType: 'PostgreSQL',
|
||||
});
|
||||
readGitConfig.mockResolvedValue({
|
||||
name: 'git-user',
|
||||
email: 'git-email',
|
||||
defaultBranch: 'git-default-branch',
|
||||
});
|
||||
});
|
||||
@@ -74,7 +72,7 @@ describe('command entrypoint', () => {
|
||||
await createApp(cmd);
|
||||
expect(checkAppExistsMock).toHaveBeenCalled();
|
||||
expect(createTemporaryAppFolderMock).toHaveBeenCalled();
|
||||
expect(initGitRepositoryMock).toHaveBeenCalled();
|
||||
expect(tryInitGitRepositoryMock).toHaveBeenCalled();
|
||||
expect(templatingMock).toHaveBeenCalled();
|
||||
expect(moveAppMock).toHaveBeenCalled();
|
||||
expect(buildAppMock).toHaveBeenCalled();
|
||||
@@ -84,7 +82,7 @@ describe('command entrypoint', () => {
|
||||
const cmd = { path: 'myDirectory' } as unknown as Command;
|
||||
await createApp(cmd);
|
||||
expect(checkPathExistsMock).toHaveBeenCalled();
|
||||
expect(initGitRepositoryMock).toHaveBeenCalled();
|
||||
expect(tryInitGitRepositoryMock).toHaveBeenCalled();
|
||||
expect(templatingMock).toHaveBeenCalled();
|
||||
expect(buildAppMock).toHaveBeenCalled();
|
||||
});
|
||||
@@ -97,8 +95,8 @@ describe('command entrypoint', () => {
|
||||
|
||||
it('should not call `initGitRepository` when `gitConfig` is undefined', async () => {
|
||||
const cmd = {} as unknown as Command;
|
||||
readGitConfig.mockResolvedValue({});
|
||||
readGitConfig.mockResolvedValue(undefined);
|
||||
await createApp(cmd);
|
||||
expect(initGitRepositoryMock).not.toHaveBeenCalled();
|
||||
expect(tryInitGitRepositoryMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
createTemporaryAppFolderTask,
|
||||
moveAppTask,
|
||||
templatingTask,
|
||||
initGitRepository,
|
||||
tryInitGitRepository,
|
||||
readGitConfig,
|
||||
} from './lib/tasks';
|
||||
|
||||
@@ -109,13 +109,16 @@ export default async (opts: OptionValues): Promise<void> => {
|
||||
await moveAppTask(tempDir, appDir, answers.name);
|
||||
}
|
||||
|
||||
if (gitConfig?.name && gitConfig?.email) {
|
||||
Task.section('Initializing git repository');
|
||||
await initGitRepository(appDir);
|
||||
if (gitConfig) {
|
||||
if (await tryInitGitRepository(appDir)) {
|
||||
// Since we don't know whether we were able to init git before we
|
||||
// try, we can't track the actual task execution
|
||||
Task.forItem('init', 'git repository', async () => {});
|
||||
}
|
||||
}
|
||||
|
||||
if (!opts.skipInstall) {
|
||||
Task.section('Building the app');
|
||||
Task.section('Installing dependencies');
|
||||
await buildAppTask(appDir);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,12 +27,10 @@ import {
|
||||
createTemporaryAppFolderTask,
|
||||
moveAppTask,
|
||||
templatingTask,
|
||||
initGitRepository,
|
||||
tryInitGitRepository,
|
||||
readGitConfig,
|
||||
} from './tasks';
|
||||
|
||||
const commandExists = jest.fn();
|
||||
|
||||
jest.spyOn(Task, 'log').mockReturnValue(undefined);
|
||||
jest.spyOn(Task, 'error').mockReturnValue(undefined);
|
||||
jest.spyOn(Task, 'section').mockReturnValue(undefined);
|
||||
@@ -41,12 +39,6 @@ jest
|
||||
.mockImplementation((_a, _b, taskFunc) => taskFunc());
|
||||
|
||||
jest.mock('child_process');
|
||||
jest.mock(
|
||||
'command-exists',
|
||||
() =>
|
||||
(...args: any[]) =>
|
||||
commandExists(...args),
|
||||
);
|
||||
|
||||
// By mocking this the filesystem mocks won't mess with reading all of the package.jsons
|
||||
jest.mock('./versions', () => ({
|
||||
@@ -102,7 +94,10 @@ describe('tasks', () => {
|
||||
(
|
||||
command: string,
|
||||
options: any,
|
||||
callback: (error: null, stdout: any, stderr: any) => void,
|
||||
callback: (
|
||||
error: Error | null,
|
||||
result: { stdout: string; stderr: string },
|
||||
) => void,
|
||||
) => void
|
||||
>;
|
||||
|
||||
@@ -293,29 +288,15 @@ describe('tasks', () => {
|
||||
|
||||
it('should return git config if git package is installed and git credentials are set', async () => {
|
||||
mockExec.mockImplementation((_command, _options, callback) => {
|
||||
callback(null, { stdout: 'main' }, 'standard error');
|
||||
callback(null, { stdout: 'main', stderr: '' });
|
||||
});
|
||||
|
||||
commandExists.mockResolvedValue(true);
|
||||
|
||||
const gitConfig = await readGitConfig();
|
||||
|
||||
expect(gitConfig).toBeTruthy();
|
||||
expect(gitConfig).toEqual({
|
||||
name: 'main',
|
||||
email: 'main',
|
||||
defaultBranch: 'main',
|
||||
});
|
||||
expect(mockExec).toHaveBeenCalledWith(
|
||||
'git config user.name',
|
||||
{ cwd: tmpDir },
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(mockExec).toHaveBeenCalledWith(
|
||||
'git config user.email',
|
||||
{ cwd: tmpDir },
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(mockExec).toHaveBeenCalledTimes(3);
|
||||
expect(mockExec).toHaveBeenCalledWith(
|
||||
'git init',
|
||||
{ cwd: tmpDir },
|
||||
@@ -326,58 +307,82 @@ describe('tasks', () => {
|
||||
{ cwd: tmpDir },
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(mockExec).toHaveBeenCalledWith(
|
||||
'git branch --format="%(refname:short)"',
|
||||
{ cwd: tmpDir },
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return false if git package is not installed', async () => {
|
||||
commandExists.mockResolvedValue(false);
|
||||
|
||||
const gitConfig = await readGitConfig();
|
||||
|
||||
expect(gitConfig).toEqual({});
|
||||
});
|
||||
|
||||
it('should return false if git package is installed but git credentials are not set', async () => {
|
||||
it('should return false if git config is invalid', async () => {
|
||||
mockExec.mockImplementation((_command, _options, callback) => {
|
||||
callback(null, { stdout: null }, 'standard error');
|
||||
callback(null, { stdout: '', stderr: '' });
|
||||
});
|
||||
|
||||
commandExists.mockResolvedValue(true);
|
||||
|
||||
const gitConfig = await readGitConfig();
|
||||
|
||||
expect(gitConfig).toEqual({});
|
||||
expect(mockExec).toHaveBeenCalledWith(
|
||||
'git config user.name',
|
||||
{ cwd: tmpDir },
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(mockExec).toHaveBeenCalledWith(
|
||||
'git config user.email',
|
||||
{ cwd: tmpDir },
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(gitConfig).toEqual({
|
||||
defaultBranch: undefined,
|
||||
});
|
||||
expect(mockExec).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('initGitRepository', () => {
|
||||
describe('tryInitGitRepository', () => {
|
||||
it('should initialize a git repository at the given path', async () => {
|
||||
const destinationDir = 'tmp/mockApp/';
|
||||
const destinationDir = 'tmp/mockApp';
|
||||
|
||||
mockExec.mockImplementation((_command, callback) => {
|
||||
callback(null, { stdout: 'main' }, 'standard error');
|
||||
mockExec.mockImplementation((command, _opts, callback) => {
|
||||
if (command.startsWith('git rev-parse')) {
|
||||
callback(new Error('not a git repo'), { stdout: '', stderr: '' });
|
||||
} else {
|
||||
callback(null, { stdout: '', stderr: '' });
|
||||
}
|
||||
});
|
||||
|
||||
await initGitRepository(destinationDir);
|
||||
await expect(tryInitGitRepository(destinationDir)).resolves.toBe(true);
|
||||
|
||||
expect(mockExec).toHaveBeenCalledTimes(2);
|
||||
expect(mockExec).toHaveBeenCalledTimes(4);
|
||||
expect(mockExec).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'git init',
|
||||
'git rev-parse --is-inside-work-tree',
|
||||
{ cwd: destinationDir },
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(mockExec).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'git commit --allow-empty -m "Initial commit"',
|
||||
'git init',
|
||||
{ cwd: destinationDir },
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(mockExec).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
'git add .',
|
||||
{ cwd: destinationDir },
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(mockExec).toHaveBeenNthCalledWith(
|
||||
4,
|
||||
'git commit -m "Initial commit"',
|
||||
{ cwd: destinationDir },
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not initialize a git repository if in one already', async () => {
|
||||
const destinationDir = 'tmp/mockApp';
|
||||
|
||||
mockExec.mockImplementation((_command, _opts, callback) => {
|
||||
callback(null, { stdout: '', stderr: '' });
|
||||
});
|
||||
|
||||
await expect(tryInitGitRepository(destinationDir)).resolves.toBe(false);
|
||||
|
||||
expect(mockExec).toHaveBeenCalledTimes(1);
|
||||
expect(mockExec).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'git rev-parse --is-inside-work-tree',
|
||||
{ cwd: destinationDir },
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -28,15 +28,12 @@ import {
|
||||
import { exec as execCb } from 'child_process';
|
||||
import { packageVersions } from './versions';
|
||||
import { promisify } from 'util';
|
||||
import commandExists from 'command-exists';
|
||||
import os from 'os';
|
||||
|
||||
const TASK_NAME_MAX_LENGTH = 14;
|
||||
const exec = promisify(execCb);
|
||||
|
||||
export type GitConfig = {
|
||||
name?: string;
|
||||
email?: string;
|
||||
defaultBranch?: string;
|
||||
};
|
||||
|
||||
@@ -250,71 +247,59 @@ export async function moveAppTask(
|
||||
*
|
||||
* @throws if `exec` fails
|
||||
*/
|
||||
export async function readGitConfig(): Promise<GitConfig> {
|
||||
export async function readGitConfig(): Promise<GitConfig | undefined> {
|
||||
const tempDir = resolvePath(os.tmpdir(), 'git-temp-dir');
|
||||
|
||||
const runCmd = (cmd: string) =>
|
||||
exec(cmd, { cwd: tempDir }).catch(error => {
|
||||
process.stdout.write(error.stderr);
|
||||
process.stdout.write(error.stdout);
|
||||
throw new Error(`Could not execute command ${chalk.cyan(cmd)}`);
|
||||
});
|
||||
|
||||
const isGitAvailable = await commandExists('git').catch(() => false);
|
||||
|
||||
if (!isGitAvailable) return {};
|
||||
|
||||
try {
|
||||
await fs.mkdir(tempDir);
|
||||
|
||||
const [gitUsername, gitEmail] = await Promise.all([
|
||||
runCmd('git config user.name'),
|
||||
runCmd('git config user.email'),
|
||||
]);
|
||||
await exec('git init', { cwd: tempDir });
|
||||
await exec('git commit --allow-empty -m "Initial commit"', {
|
||||
cwd: tempDir,
|
||||
});
|
||||
|
||||
const gitCredentials = Boolean(
|
||||
gitUsername.stdout?.trim() && gitEmail.stdout?.trim(),
|
||||
);
|
||||
|
||||
if (!gitCredentials) return {};
|
||||
|
||||
await runCmd('git init');
|
||||
await runCmd('git commit --allow-empty -m "Initial commit"');
|
||||
|
||||
const gitDefaultBranch = await runCmd(
|
||||
const getDefaultBranch = await exec(
|
||||
'git branch --format="%(refname:short)"',
|
||||
{ cwd: tempDir },
|
||||
);
|
||||
|
||||
return {
|
||||
name: gitUsername.stdout?.trim(),
|
||||
email: gitEmail.stdout?.trim(),
|
||||
defaultBranch: gitDefaultBranch.stdout?.trim(),
|
||||
defaultBranch: getDefaultBranch.stdout?.trim() || undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to read git config, ${error}`);
|
||||
return undefined;
|
||||
} finally {
|
||||
await fs.rm(tempDir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a git repository in the destination folder
|
||||
* Initializes a git repository in the destination folder if possible
|
||||
*
|
||||
* @param dir - source path to initialize git repository in
|
||||
* @throws if `exec` fails
|
||||
* @returns true if git repository was initialized
|
||||
*/
|
||||
export async function initGitRepository(dir: string) {
|
||||
const runCmd = (cmd: string) =>
|
||||
exec(cmd).catch(error => {
|
||||
process.stdout.write(error.stderr);
|
||||
process.stdout.write(error.stdout);
|
||||
throw new Error(`Could not execute command ${chalk.cyan(cmd)}`);
|
||||
});
|
||||
export async function tryInitGitRepository(dir: string) {
|
||||
try {
|
||||
// Check if we're already in a git repo
|
||||
await exec('git rev-parse --is-inside-work-tree', { cwd: dir });
|
||||
return false;
|
||||
} catch {
|
||||
/* ignored */
|
||||
}
|
||||
|
||||
await Task.forItem('init', 'git repository', async () => {
|
||||
process.chdir(dir);
|
||||
try {
|
||||
await exec('git init', { cwd: dir });
|
||||
await exec('git add .', { cwd: dir });
|
||||
await exec('git commit -m "Initial commit"', { cwd: dir });
|
||||
return true;
|
||||
} catch (error) {
|
||||
try {
|
||||
await fs.rm(resolvePath(dir, '.git'), { recursive: true, force: true });
|
||||
} catch {
|
||||
throw new Error('Failed to remove .git folder');
|
||||
}
|
||||
|
||||
await runCmd('git init');
|
||||
await runCmd('git commit --allow-empty -m "Initial commit"');
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,6 +323,17 @@ export function getGerritRequestOptions(config: GerritIntegrationConfig): {
|
||||
headers?: Record<string, string>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export function getGiteaFileContentsUrl(
|
||||
config: GiteaIntegrationConfig,
|
||||
url: string,
|
||||
): string;
|
||||
|
||||
// @public
|
||||
export function getGiteaRequestOptions(config: GiteaIntegrationConfig): {
|
||||
headers?: Record<string, string>;
|
||||
};
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export const getGitHubFileFetchUrl: typeof getGithubFileFetchUrl;
|
||||
|
||||
@@ -357,6 +368,35 @@ export function getGitLabRequestOptions(config: GitLabIntegrationConfig): {
|
||||
headers: Record<string, string>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export class GiteaIntegration implements ScmIntegration {
|
||||
constructor(config: GiteaIntegrationConfig);
|
||||
// (undocumented)
|
||||
readonly config: GiteaIntegrationConfig;
|
||||
// (undocumented)
|
||||
static factory: ScmIntegrationsFactory<GiteaIntegration>;
|
||||
// (undocumented)
|
||||
resolveEditUrl(url: string): string;
|
||||
// (undocumented)
|
||||
resolveUrl(options: {
|
||||
url: string;
|
||||
base: string;
|
||||
lineNumber?: number | undefined;
|
||||
}): string;
|
||||
// (undocumented)
|
||||
get title(): string;
|
||||
// (undocumented)
|
||||
get type(): string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type GiteaIntegrationConfig = {
|
||||
host: string;
|
||||
baseUrl?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type GithubAppConfig = {
|
||||
appId: number;
|
||||
@@ -488,6 +528,8 @@ export interface IntegrationsByType {
|
||||
// (undocumented)
|
||||
gerrit: ScmIntegrationsGroup<GerritIntegration>;
|
||||
// (undocumented)
|
||||
gitea: ScmIntegrationsGroup<GiteaIntegration>;
|
||||
// (undocumented)
|
||||
github: ScmIntegrationsGroup<GithubIntegration>;
|
||||
// (undocumented)
|
||||
gitlab: ScmIntegrationsGroup<GitLabIntegration>;
|
||||
@@ -566,6 +608,9 @@ export function readGerritIntegrationConfigs(
|
||||
configs: Config[],
|
||||
): GerritIntegrationConfig[];
|
||||
|
||||
// @public
|
||||
export function readGiteaConfig(config: Config): GiteaIntegrationConfig;
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export const readGitHubIntegrationConfig: typeof readGithubIntegrationConfig;
|
||||
|
||||
@@ -640,6 +685,8 @@ export interface ScmIntegrationRegistry
|
||||
// (undocumented)
|
||||
gerrit: ScmIntegrationsGroup<GerritIntegration>;
|
||||
// (undocumented)
|
||||
gitea: ScmIntegrationsGroup<GiteaIntegration>;
|
||||
// (undocumented)
|
||||
github: ScmIntegrationsGroup<GithubIntegration>;
|
||||
// (undocumented)
|
||||
gitlab: ScmIntegrationsGroup<GitLabIntegration>;
|
||||
@@ -673,6 +720,8 @@ export class ScmIntegrations implements ScmIntegrationRegistry {
|
||||
// (undocumented)
|
||||
get gerrit(): ScmIntegrationsGroup<GerritIntegration>;
|
||||
// (undocumented)
|
||||
get gitea(): ScmIntegrationsGroup<GiteaIntegration>;
|
||||
// (undocumented)
|
||||
get github(): ScmIntegrationsGroup<GithubIntegration>;
|
||||
// (undocumented)
|
||||
get gitlab(): ScmIntegrationsGroup<GitLabIntegration>;
|
||||
|
||||
Vendored
+26
@@ -281,5 +281,31 @@ export interface Config {
|
||||
*/
|
||||
externalId?: string;
|
||||
}>;
|
||||
|
||||
/** Integration configuration for Gitea */
|
||||
gitea?: Array<{
|
||||
/**
|
||||
* The hostname of the given Gitea instance
|
||||
* @visibility frontend
|
||||
*/
|
||||
host: string;
|
||||
/**
|
||||
* The base url for the Gitea instance.
|
||||
* @visibility frontend
|
||||
*/
|
||||
baseUrl?: string;
|
||||
|
||||
/**
|
||||
* The username to use for authenticated requests.
|
||||
* @visibility secret
|
||||
*/
|
||||
username?: string;
|
||||
/**
|
||||
* Gitea password used to authenticate requests. This can be either a password
|
||||
* or a generated access token.
|
||||
* @visibility secret
|
||||
*/
|
||||
password?: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import { GitLabIntegrationConfig } from './gitlab';
|
||||
import { GitLabIntegration } from './gitlab/GitLabIntegration';
|
||||
import { basicIntegrations } from './helpers';
|
||||
import { ScmIntegrations } from './ScmIntegrations';
|
||||
import { GiteaIntegration, GiteaIntegrationConfig } from './gitea';
|
||||
|
||||
describe('ScmIntegrations', () => {
|
||||
const awsS3 = new AwsS3Integration({
|
||||
@@ -69,6 +70,10 @@ describe('ScmIntegrations', () => {
|
||||
host: 'gitlab.local',
|
||||
} as GitLabIntegrationConfig);
|
||||
|
||||
const gitea = new GiteaIntegration({
|
||||
host: 'gitea.local',
|
||||
} as GiteaIntegrationConfig);
|
||||
|
||||
const i = new ScmIntegrations({
|
||||
awsS3: basicIntegrations([awsS3], item => item.config.host),
|
||||
azure: basicIntegrations([azure], item => item.config.host),
|
||||
@@ -81,6 +86,7 @@ describe('ScmIntegrations', () => {
|
||||
gerrit: basicIntegrations([gerrit], item => item.config.host),
|
||||
github: basicIntegrations([github], item => item.config.host),
|
||||
gitlab: basicIntegrations([gitlab], item => item.config.host),
|
||||
gitea: basicIntegrations([gitea], item => item.config.host),
|
||||
});
|
||||
|
||||
it('can get the specifics', () => {
|
||||
@@ -96,6 +102,7 @@ describe('ScmIntegrations', () => {
|
||||
expect(i.gerrit.byUrl('https://gerrit.local')).toBe(gerrit);
|
||||
expect(i.github.byUrl('https://github.local')).toBe(github);
|
||||
expect(i.gitlab.byUrl('https://gitlab.local')).toBe(gitlab);
|
||||
expect(i.gitea.byUrl('https://gitea.local')).toBe(gitea);
|
||||
});
|
||||
|
||||
it('can list', () => {
|
||||
@@ -109,6 +116,7 @@ describe('ScmIntegrations', () => {
|
||||
gerrit,
|
||||
github,
|
||||
gitlab,
|
||||
gitea,
|
||||
]),
|
||||
);
|
||||
});
|
||||
@@ -122,6 +130,7 @@ describe('ScmIntegrations', () => {
|
||||
expect(i.byUrl('https://gerrit.local')).toBe(gerrit);
|
||||
expect(i.byUrl('https://github.local')).toBe(github);
|
||||
expect(i.byUrl('https://gitlab.local')).toBe(gitlab);
|
||||
expect(i.byUrl('https://gitea.local')).toBe(gitea);
|
||||
|
||||
expect(i.byHost('awss3.local')).toBe(awsS3);
|
||||
expect(i.byHost('azure.local')).toBe(azure);
|
||||
@@ -131,6 +140,7 @@ describe('ScmIntegrations', () => {
|
||||
expect(i.byHost('gerrit.local')).toBe(gerrit);
|
||||
expect(i.byHost('github.local')).toBe(github);
|
||||
expect(i.byHost('gitlab.local')).toBe(gitlab);
|
||||
expect(i.byHost('gitea.local')).toBe(gitea);
|
||||
});
|
||||
|
||||
it('can resolveUrl using fallback', () => {
|
||||
|
||||
@@ -26,6 +26,7 @@ import { GitLabIntegration } from './gitlab/GitLabIntegration';
|
||||
import { defaultScmResolveUrl } from './helpers';
|
||||
import { ScmIntegration, ScmIntegrationsGroup } from './types';
|
||||
import { ScmIntegrationRegistry } from './registry';
|
||||
import { GiteaIntegration } from './gitea';
|
||||
|
||||
/**
|
||||
* The set of supported integrations.
|
||||
@@ -44,6 +45,7 @@ export interface IntegrationsByType {
|
||||
gerrit: ScmIntegrationsGroup<GerritIntegration>;
|
||||
github: ScmIntegrationsGroup<GithubIntegration>;
|
||||
gitlab: ScmIntegrationsGroup<GitLabIntegration>;
|
||||
gitea: ScmIntegrationsGroup<GiteaIntegration>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,6 +66,7 @@ export class ScmIntegrations implements ScmIntegrationRegistry {
|
||||
gerrit: GerritIntegration.factory({ config }),
|
||||
github: GithubIntegration.factory({ config }),
|
||||
gitlab: GitLabIntegration.factory({ config }),
|
||||
gitea: GiteaIntegration.factory({ config }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -106,6 +109,10 @@ export class ScmIntegrations implements ScmIntegrationRegistry {
|
||||
return this.byType.gitlab;
|
||||
}
|
||||
|
||||
get gitea(): ScmIntegrationsGroup<GiteaIntegration> {
|
||||
return this.byType.gitea;
|
||||
}
|
||||
|
||||
list(): ScmIntegration[] {
|
||||
return Object.values(this.byType).flatMap(
|
||||
i => i.list() as ScmIntegration[],
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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.
|
||||
*/
|
||||
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { GiteaIntegration } from './GiteaIntegration';
|
||||
|
||||
describe('GiteaIntegration', () => {
|
||||
it('has a working factory', () => {
|
||||
const integrations = GiteaIntegration.factory({
|
||||
config: new ConfigReader({
|
||||
integrations: {
|
||||
gitea: [
|
||||
{
|
||||
host: 'gitea.example.com',
|
||||
username: 'git',
|
||||
baseUrl: 'https://gitea.example.com/route',
|
||||
password: '1234',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(integrations.list().length).toBe(1);
|
||||
expect(integrations.list()[0].config.host).toBe('gitea.example.com');
|
||||
expect(integrations.list()[0].config.baseUrl).toBe(
|
||||
'https://gitea.example.com/route',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the basics', () => {
|
||||
const integration = new GiteaIntegration({
|
||||
host: 'gitea.example.com',
|
||||
});
|
||||
expect(integration.type).toBe('gitea');
|
||||
expect(integration.title).toBe('gitea.example.com');
|
||||
});
|
||||
|
||||
describe('resolveUrl', () => {
|
||||
it('works for valid urls, ignoring line number', () => {
|
||||
const integration = new GiteaIntegration({
|
||||
host: 'gitea.example.com',
|
||||
});
|
||||
|
||||
expect(
|
||||
integration.resolveUrl({
|
||||
url: 'https://gitea.example.com/catalog-info.yaml',
|
||||
base: 'https://gitea.example.com/catalog-info.yaml',
|
||||
lineNumber: 9,
|
||||
}),
|
||||
).toBe('https://gitea.example.com/catalog-info.yaml');
|
||||
});
|
||||
|
||||
it('handles line numbers', () => {
|
||||
const integration = new GiteaIntegration({
|
||||
host: 'gitea.example.com',
|
||||
});
|
||||
|
||||
expect(
|
||||
integration.resolveUrl({
|
||||
url: '',
|
||||
base: 'https://gitea.example.com/catalog-info.yaml#4',
|
||||
lineNumber: 9,
|
||||
}),
|
||||
).toBe('https://gitea.example.com/catalog-info.yaml#L9');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolves with a relative url', () => {
|
||||
it('works for valid urls', () => {
|
||||
const integration = new GiteaIntegration({
|
||||
host: 'gitea.example.com',
|
||||
});
|
||||
|
||||
expect(
|
||||
integration.resolveUrl({
|
||||
url: './skeleton',
|
||||
base: 'https://gitea.example.com/git/plugins/repo/+/refs/heads/master/template.yaml',
|
||||
}),
|
||||
).toBe(
|
||||
'https://gitea.example.com/git/plugins/repo/+/refs/heads/master/skeleton',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolves with an absolute url', () => {
|
||||
it('works for valid urls', () => {
|
||||
const integration = new GiteaIntegration({
|
||||
host: 'gitea.example.com',
|
||||
});
|
||||
|
||||
expect(
|
||||
integration.resolveUrl({
|
||||
url: '/catalog-info.yaml',
|
||||
base: 'https://gitea.example.com/git/repo/+/refs/heads/master/',
|
||||
}),
|
||||
).toBe(
|
||||
'https://gitea.example.com/git/repo/+/refs/heads/master/catalog-info.yaml',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('resolve edit URL', () => {
|
||||
const integration = new GiteaIntegration({
|
||||
host: 'gitea.example.com',
|
||||
});
|
||||
|
||||
expect(
|
||||
integration.resolveEditUrl(
|
||||
'https://gitea.example.com/owner/repo/src/branch/branch_name/path/to/c.yaml',
|
||||
),
|
||||
).toBe(
|
||||
'https://gitea.example.com/owner/repo/_edit/branch_name/path/to/c.yaml',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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.
|
||||
*/
|
||||
import { basicIntegrations, defaultScmResolveUrl } from '../helpers';
|
||||
import { ScmIntegration, ScmIntegrationsFactory } from '../types';
|
||||
import { GiteaIntegrationConfig, readGiteaConfig } from './config';
|
||||
import { getGiteaEditContentsUrl } from './core';
|
||||
|
||||
/**
|
||||
* A Gitea based integration.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class GiteaIntegration implements ScmIntegration {
|
||||
static factory: ScmIntegrationsFactory<GiteaIntegration> = ({ config }) => {
|
||||
const configs = config.getOptionalConfigArray('integrations.gitea') ?? [];
|
||||
const giteaConfigs = configs.map(c => readGiteaConfig(c));
|
||||
|
||||
return basicIntegrations(
|
||||
giteaConfigs.map(c => new GiteaIntegration(c)),
|
||||
(gitea: GiteaIntegration) => gitea.config.host,
|
||||
);
|
||||
};
|
||||
|
||||
constructor(readonly config: GiteaIntegrationConfig) {}
|
||||
|
||||
get type(): string {
|
||||
return 'gitea';
|
||||
}
|
||||
|
||||
get title(): string {
|
||||
return this.config.host;
|
||||
}
|
||||
|
||||
resolveUrl(options: {
|
||||
url: string;
|
||||
base: string;
|
||||
lineNumber?: number | undefined;
|
||||
}): string {
|
||||
return defaultScmResolveUrl(options);
|
||||
}
|
||||
|
||||
resolveEditUrl(url: string): string {
|
||||
return getGiteaEditContentsUrl(this.config, url);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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.
|
||||
*/
|
||||
|
||||
import { Config, ConfigReader } from '@backstage/config';
|
||||
import { loadConfigSchema } from '@backstage/config-loader';
|
||||
import { GiteaIntegrationConfig, readGiteaConfig } from './config';
|
||||
|
||||
describe('readGiteaConfig', () => {
|
||||
function buildConfig(data: Partial<GiteaIntegrationConfig>): Config {
|
||||
return new ConfigReader(data);
|
||||
}
|
||||
|
||||
async function buildFrontendConfig(
|
||||
data: Partial<GiteaIntegrationConfig>,
|
||||
): Promise<Config> {
|
||||
const fullSchema = await loadConfigSchema({
|
||||
dependencies: ['@backstage/integration'],
|
||||
});
|
||||
const serializedSchema = fullSchema.serialize() as {
|
||||
schemas: { value: { properties?: { integrations?: object } } }[];
|
||||
};
|
||||
const schema = await loadConfigSchema({
|
||||
serialized: {
|
||||
...serializedSchema, // only include schemas that apply to integrations
|
||||
schemas: serializedSchema.schemas.filter(
|
||||
s => s.value?.properties?.integrations,
|
||||
),
|
||||
},
|
||||
});
|
||||
const processed = schema.process(
|
||||
[{ data: { integrations: { gitea: [data] } }, context: 'app' }],
|
||||
{ visibility: ['frontend'] },
|
||||
);
|
||||
return new ConfigReader((processed[0].data as any).integrations.gitea[0]);
|
||||
}
|
||||
|
||||
it('reads all values', () => {
|
||||
const output = readGiteaConfig(
|
||||
buildConfig({
|
||||
host: 'a.com',
|
||||
baseUrl: 'https://a.com/route/api',
|
||||
username: 'u',
|
||||
password: 'p',
|
||||
}),
|
||||
);
|
||||
expect(output).toEqual({
|
||||
host: 'a.com',
|
||||
baseUrl: 'https://a.com/route/api',
|
||||
username: 'u',
|
||||
password: 'p',
|
||||
});
|
||||
});
|
||||
|
||||
it('can create a default value if the API base URL is missing', () => {
|
||||
const output = readGiteaConfig(
|
||||
buildConfig({
|
||||
host: 'a.com',
|
||||
}),
|
||||
);
|
||||
expect(output).toEqual({
|
||||
host: 'a.com',
|
||||
baseUrl: 'https://a.com',
|
||||
username: undefined,
|
||||
password: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects funky configs', () => {
|
||||
const valid: any = {
|
||||
host: 'a.com',
|
||||
};
|
||||
expect(() => readGiteaConfig(buildConfig({ ...valid, host: 2 }))).toThrow(
|
||||
/host/,
|
||||
);
|
||||
expect(() =>
|
||||
readGiteaConfig(buildConfig({ ...valid, baseUrl: 2 })),
|
||||
).toThrow(/baseUrl/);
|
||||
});
|
||||
|
||||
it('works on the frontend', async () => {
|
||||
expect(
|
||||
readGiteaConfig(
|
||||
await buildFrontendConfig({
|
||||
host: 'a.com',
|
||||
baseUrl: 'https://a.com/route',
|
||||
username: 'u',
|
||||
password: 'p',
|
||||
}),
|
||||
),
|
||||
).toEqual({
|
||||
host: 'a.com',
|
||||
baseUrl: 'https://a.com/route',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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.
|
||||
*/
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import { trimEnd } from 'lodash';
|
||||
import { isValidHost } from '../helpers';
|
||||
|
||||
/**
|
||||
* The configuration for a single Gitea integration.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type GiteaIntegrationConfig = {
|
||||
/**
|
||||
* The host of the target that this matches on, e.g. "gitea.website.com"
|
||||
*/
|
||||
host: string;
|
||||
/**
|
||||
* The optional base URL of the Gitea instance. It is assumed that https
|
||||
* is used and that the base path is "/" on the host. If that is not the
|
||||
* case set the complete base url to the gitea instance, e.g.
|
||||
* "https://gitea.website.com/". This is the url that you would open
|
||||
* in a browser.
|
||||
*/
|
||||
baseUrl?: string;
|
||||
/**
|
||||
* The username to use for requests to gitea.
|
||||
*/
|
||||
username?: string;
|
||||
|
||||
/**
|
||||
* The password or http token to use for authentication.
|
||||
*/
|
||||
password?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses a location config block for use in GiteaIntegration
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function readGiteaConfig(config: Config): GiteaIntegrationConfig {
|
||||
const host = config.getString('host');
|
||||
let baseUrl = config.getOptionalString('baseUrl');
|
||||
const username = config.getOptionalString('username');
|
||||
const password = config.getOptionalString('password');
|
||||
|
||||
if (!isValidHost(host)) {
|
||||
throw new Error(
|
||||
`Invalid Gitea integration config, '${host}' is not a valid host`,
|
||||
);
|
||||
}
|
||||
|
||||
if (baseUrl) {
|
||||
baseUrl = trimEnd(baseUrl, '/');
|
||||
} else {
|
||||
baseUrl = `https://${host}`;
|
||||
}
|
||||
|
||||
return {
|
||||
host,
|
||||
baseUrl,
|
||||
username,
|
||||
password,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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.
|
||||
*/
|
||||
|
||||
import { setupServer } from 'msw/node';
|
||||
import { setupRequestMockHandlers } from '@backstage/test-utils';
|
||||
import { GiteaIntegrationConfig } from './config';
|
||||
import {
|
||||
getGiteaEditContentsUrl,
|
||||
getGiteaFileContentsUrl,
|
||||
getGiteaRequestOptions,
|
||||
} from './core';
|
||||
|
||||
describe('gitea core', () => {
|
||||
const worker = setupServer();
|
||||
setupRequestMockHandlers(worker);
|
||||
|
||||
describe('getGiteaFileContentsUrl', () => {
|
||||
it('can create an url from arguments', () => {
|
||||
const config: GiteaIntegrationConfig = {
|
||||
host: 'gitea.com',
|
||||
};
|
||||
expect(
|
||||
getGiteaFileContentsUrl(
|
||||
config,
|
||||
'https://gitea.com/a/b/src/branch/branch_name/path/to/c.yaml',
|
||||
),
|
||||
).toEqual(
|
||||
'https://gitea.com/api/v1/repos/a/b/contents/path/to/c.yaml?ref=branch_name',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGiteaEditContentsUrl', () => {
|
||||
it('can create an url from arguments', () => {
|
||||
const config: GiteaIntegrationConfig = {
|
||||
host: 'gitea.example.com',
|
||||
};
|
||||
expect(
|
||||
getGiteaEditContentsUrl(
|
||||
config,
|
||||
'https://gitea.example.com/owner/repo/src/branch/branch_name/path/to/c.yaml',
|
||||
),
|
||||
).toEqual(
|
||||
'https://gitea.example.com/owner/repo/_edit/branch_name/path/to/c.yaml',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGerritRequestOptions', () => {
|
||||
it('adds token header when only a password is specified', () => {
|
||||
const authRequest: GiteaIntegrationConfig = {
|
||||
host: 'gerrit.com',
|
||||
password: 'P',
|
||||
};
|
||||
const anonymousRequest: GiteaIntegrationConfig = {
|
||||
host: 'gerrit.com',
|
||||
};
|
||||
expect(
|
||||
(getGiteaRequestOptions(authRequest).headers as any).Authorization,
|
||||
).toEqual('token P');
|
||||
expect(getGiteaRequestOptions(anonymousRequest).headers).toBeUndefined();
|
||||
});
|
||||
|
||||
it('adds basic auth when username and password are specified', () => {
|
||||
const authRequest: GiteaIntegrationConfig = {
|
||||
host: 'gerrit.com',
|
||||
username: 'username',
|
||||
password: 'P',
|
||||
};
|
||||
|
||||
const basicAuthentication = `basic ${Buffer.from(
|
||||
`${authRequest.username}:${authRequest.password}`,
|
||||
).toString('base64')}`;
|
||||
|
||||
expect(
|
||||
(getGiteaRequestOptions(authRequest).headers as any).Authorization,
|
||||
).toEqual(basicAuthentication);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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.
|
||||
*/
|
||||
import { GiteaIntegrationConfig } from './config';
|
||||
|
||||
/**
|
||||
* Given a URL pointing to a file, returns a URL
|
||||
* for editing the contents of the data.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* Converts
|
||||
* from: https://gitea.com/a/b/src/branchname/path/to/c.yaml
|
||||
* or: https://gitea.com/a/b/_edit/branchname/path/to/c.yaml
|
||||
*
|
||||
* @param url - A URL pointing to a file
|
||||
* @param config - The relevant provider config
|
||||
* @public
|
||||
*/
|
||||
export function getGiteaEditContentsUrl(
|
||||
config: GiteaIntegrationConfig,
|
||||
url: string,
|
||||
) {
|
||||
try {
|
||||
const baseUrl = config.baseUrl ?? `https://${config.host}`;
|
||||
const [_blank, owner, name, _src, _branch, ref, ...path] = url
|
||||
.replace(baseUrl, '')
|
||||
.split('/');
|
||||
const pathWithoutSlash = path.join('/').replace(/^\//, '');
|
||||
return `${baseUrl}/${owner}/${name}/_edit/${ref}/${pathWithoutSlash}`;
|
||||
} catch (e) {
|
||||
throw new Error(`Incorrect URL: ${url}, ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a URL pointing to a file, returns an api URL
|
||||
* for fetching the contents of the data.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* Converts
|
||||
* from: https://gitea.com/a/b/src/branch/branchname/path/to/c.yaml
|
||||
* to: https://gitea.com/api/v1/repos/a/b/contents/path/to/c.yaml?ref=branchname
|
||||
*
|
||||
* @param url - A URL pointing to a file
|
||||
* @param config - The relevant provider config
|
||||
* @public
|
||||
*/
|
||||
export function getGiteaFileContentsUrl(
|
||||
config: GiteaIntegrationConfig,
|
||||
url: string,
|
||||
) {
|
||||
try {
|
||||
const baseUrl = config.baseUrl ?? `https://${config.host}`;
|
||||
const [_blank, owner, name, _src, _branch, ref, ...path] = url
|
||||
.replace(baseUrl, '')
|
||||
.split('/');
|
||||
const pathWithoutSlash = path.join('/').replace(/^\//, '');
|
||||
|
||||
return `${baseUrl}/api/v1/repos/${owner}/${name}/contents/${pathWithoutSlash}?ref=${ref}`;
|
||||
} catch (e) {
|
||||
throw new Error(`Incorrect URL: ${url}, ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return request headers for a Gitea provider.
|
||||
*
|
||||
* @param config - A Gitea provider config
|
||||
* @public
|
||||
*/
|
||||
export function getGiteaRequestOptions(config: GiteaIntegrationConfig): {
|
||||
headers?: Record<string, string>;
|
||||
} {
|
||||
const headers: Record<string, string> = {};
|
||||
const { username, password } = config;
|
||||
|
||||
if (!password) {
|
||||
return headers;
|
||||
}
|
||||
|
||||
if (username) {
|
||||
headers.Authorization = `basic ${Buffer.from(
|
||||
`${username}:${password}`,
|
||||
).toString('base64')}`;
|
||||
} else {
|
||||
headers.Authorization = `token ${password}`;
|
||||
}
|
||||
|
||||
return {
|
||||
headers,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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.
|
||||
*/
|
||||
export { GiteaIntegration } from './GiteaIntegration';
|
||||
export { getGiteaRequestOptions, getGiteaFileContentsUrl } from './core';
|
||||
export { readGiteaConfig } from './config';
|
||||
export type { GiteaIntegrationConfig } from './config';
|
||||
@@ -26,6 +26,7 @@ export * from './bitbucket';
|
||||
export * from './bitbucketCloud';
|
||||
export * from './bitbucketServer';
|
||||
export * from './gerrit';
|
||||
export * from './gitea';
|
||||
export * from './github';
|
||||
export * from './gitlab';
|
||||
export * from './googleGcs';
|
||||
|
||||
@@ -23,6 +23,7 @@ import { BitbucketServerIntegration } from './bitbucketServer/BitbucketServerInt
|
||||
import { GerritIntegration } from './gerrit/GerritIntegration';
|
||||
import { GithubIntegration } from './github/GithubIntegration';
|
||||
import { GitLabIntegration } from './gitlab/GitLabIntegration';
|
||||
import { GiteaIntegration } from './gitea/GiteaIntegration';
|
||||
|
||||
/**
|
||||
* Holds all registered SCM integrations, of all types.
|
||||
@@ -42,7 +43,7 @@ export interface ScmIntegrationRegistry
|
||||
gerrit: ScmIntegrationsGroup<GerritIntegration>;
|
||||
github: ScmIntegrationsGroup<GithubIntegration>;
|
||||
gitlab: ScmIntegrationsGroup<GitLabIntegration>;
|
||||
|
||||
gitea: ScmIntegrationsGroup<GiteaIntegration>;
|
||||
/**
|
||||
* Resolves an absolute or relative URL in relation to a base URL.
|
||||
*
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
// @public
|
||||
export type HumanDuration = {
|
||||
years?: number;
|
||||
months?: number;
|
||||
weeks?: number;
|
||||
days?: number;
|
||||
hours?: number;
|
||||
minutes?: number;
|
||||
seconds?: number;
|
||||
milliseconds?: number;
|
||||
};
|
||||
|
||||
// @public
|
||||
export interface JsonArray extends Array<JsonValue> {}
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@types/zen-observable": "^0.8.0",
|
||||
"luxon": "^3.0.0",
|
||||
"zen-observable": "^0.8.15"
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -22,3 +22,4 @@
|
||||
|
||||
export type { JsonArray, JsonObject, JsonPrimitive, JsonValue } from './json';
|
||||
export type { Observable, Observer, Subscription } from './observable';
|
||||
export type { HumanDuration } from './time';
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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.
|
||||
*/
|
||||
|
||||
import { HumanDuration } from './time';
|
||||
import { Duration } from 'luxon';
|
||||
|
||||
describe('time', () => {
|
||||
describe('HumanDuration', () => {
|
||||
const durations: HumanDuration[] = [
|
||||
{ years: 1 },
|
||||
{ months: 1 },
|
||||
{ weeks: 1 },
|
||||
{ days: 1 },
|
||||
{ hours: 1 },
|
||||
{ minutes: 1 },
|
||||
{ seconds: 1 },
|
||||
{ milliseconds: 1 },
|
||||
];
|
||||
it.each(durations)('successfully parsed by luxon, %p', d => {
|
||||
expect(Duration.fromObject(d).toObject()).toEqual(d);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Human friendly durations object.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type HumanDuration = {
|
||||
years?: number;
|
||||
months?: number;
|
||||
weeks?: number;
|
||||
days?: number;
|
||||
hours?: number;
|
||||
minutes?: number;
|
||||
seconds?: number;
|
||||
milliseconds?: number;
|
||||
};
|
||||
@@ -85,6 +85,13 @@ catalog:
|
||||
# in order to add extra information to your groups that can be used on your custom groupTransformers
|
||||
# See https://docs.microsoft.com/en-us/graph/api/resources/schemaextension?view=graph-rest-1.0
|
||||
select: ['id', 'displayName', 'description']
|
||||
schedule: # optional; same options as in TaskScheduleDefinition
|
||||
# supports cron, ISO duration, "human duration" as used in code
|
||||
frequency: { hours: 1 }
|
||||
# supports ISO duration, "human duration" as used in code
|
||||
timeout: { minutes: 50 }
|
||||
# supports ISO duration, "human duration" as used in code
|
||||
initialDelay: { seconds: 15},
|
||||
```
|
||||
|
||||
`user.filter` and `userGroupMember.filter` are mutually exclusive, only one can be provided. If both are provided, an error will be thrown.
|
||||
@@ -116,13 +123,21 @@ yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-msgraph
|
||||
+ builder.addEntityProvider(
|
||||
+ MicrosoftGraphOrgEntityProvider.fromConfig(env.config, {
|
||||
+ logger: env.logger,
|
||||
+ scheduler,
|
||||
+ }),
|
||||
+ );
|
||||
```
|
||||
|
||||
Instead of configuring the refresh schedule inside the config (per provider instance),
|
||||
you can define it in code (for all of them):
|
||||
|
||||
```diff
|
||||
- scheduler,
|
||||
+ schedule: env.scheduler.createScheduledTaskRunner({
|
||||
+ frequency: { hours: 1 },
|
||||
+ timeout: { minutes: 50 },
|
||||
+ initialDelay: { seconds: 15}
|
||||
+ initialDelay: { seconds: 15},
|
||||
+ }),
|
||||
+ }),
|
||||
+ );
|
||||
```
|
||||
|
||||
## Customize the Processor or Entity Provider
|
||||
@@ -161,10 +176,7 @@ export async function myGroupTransformer(
|
||||
builder.addEntityProvider(
|
||||
MicrosoftGraphOrgEntityProvider.fromConfig(env.config, {
|
||||
logger: env.logger,
|
||||
schedule: env.scheduler.createScheduledTaskRunner({
|
||||
frequency: { minutes: 5 },
|
||||
timeout: { minutes: 3 },
|
||||
}),
|
||||
scheduler,
|
||||
+ groupTransformer: myGroupTransformer,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { BackendFeature } from '@backstage/backend-plugin-api';
|
||||
import { CatalogProcessor } from '@backstage/plugin-catalog-backend';
|
||||
import { CatalogProcessorEmit } from '@backstage/plugin-catalog-backend';
|
||||
import { Config } from '@backstage/config';
|
||||
@@ -12,8 +13,10 @@ import { GroupEntity } from '@backstage/catalog-model';
|
||||
import { LocationSpec } from '@backstage/plugin-catalog-backend';
|
||||
import { Logger } from 'winston';
|
||||
import * as MicrosoftGraph from '@microsoft/microsoft-graph-types';
|
||||
import { PluginTaskScheduler } from '@backstage/backend-tasks';
|
||||
import { Response as Response_2 } from 'node-fetch';
|
||||
import { TaskRunner } from '@backstage/backend-tasks';
|
||||
import { TaskScheduleDefinition } from '@backstage/backend-tasks';
|
||||
import { TokenCredential } from '@azure/identity';
|
||||
import { UserEntity } from '@backstage/catalog-model';
|
||||
|
||||
@@ -131,6 +134,20 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider {
|
||||
read(options?: { logger?: Logger }): Promise<void>;
|
||||
}
|
||||
|
||||
// @alpha
|
||||
export const microsoftGraphOrgEntityProviderCatalogModule: (
|
||||
options?: MicrosoftGraphOrgEntityProviderCatalogModuleOptions | undefined,
|
||||
) => BackendFeature;
|
||||
|
||||
// @alpha
|
||||
export interface MicrosoftGraphOrgEntityProviderCatalogModuleOptions {
|
||||
groupTransformer?: GroupTransformer | Record<string, GroupTransformer>;
|
||||
organizationTransformer?:
|
||||
| OrganizationTransformer
|
||||
| Record<string, OrganizationTransformer>;
|
||||
userTransformer?: UserTransformer | Record<string, UserTransformer>;
|
||||
}
|
||||
|
||||
// @public @deprecated
|
||||
export interface MicrosoftGraphOrgEntityProviderLegacyOptions {
|
||||
groupTransformer?: GroupTransformer;
|
||||
@@ -147,7 +164,8 @@ export type MicrosoftGraphOrgEntityProviderOptions =
|
||||
| MicrosoftGraphOrgEntityProviderLegacyOptions
|
||||
| {
|
||||
logger: Logger;
|
||||
schedule: 'manual' | TaskRunner;
|
||||
schedule?: 'manual' | TaskRunner;
|
||||
scheduler?: PluginTaskScheduler;
|
||||
userTransformer?: UserTransformer | Record<string, UserTransformer>;
|
||||
groupTransformer?: GroupTransformer | Record<string, GroupTransformer>;
|
||||
organizationTransformer?:
|
||||
@@ -202,6 +220,7 @@ export type MicrosoftGraphProviderConfig = {
|
||||
groupSearch?: string;
|
||||
groupSelect?: string[];
|
||||
queryMode?: 'basic' | 'advanced';
|
||||
schedule?: TaskScheduleDefinition;
|
||||
};
|
||||
|
||||
// @public
|
||||
|
||||
+13
-9
@@ -14,14 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { TaskScheduleDefinitionConfig } from '@backstage/backend-tasks';
|
||||
|
||||
export interface Config {
|
||||
/**
|
||||
* Configuration options for the catalog plugin.
|
||||
*/
|
||||
catalog?: {
|
||||
/**
|
||||
* List of processor-specific options and attributes
|
||||
*/
|
||||
processors?: {
|
||||
/**
|
||||
* MicrosoftGraphOrgReaderProcessor configuration
|
||||
@@ -109,9 +105,7 @@ export interface Config {
|
||||
}>;
|
||||
};
|
||||
};
|
||||
/**
|
||||
* List of provider-specific options and attributes
|
||||
*/
|
||||
|
||||
providers?: {
|
||||
/**
|
||||
* MicrosoftGraphOrgEntityProvider configuration.
|
||||
@@ -209,6 +203,11 @@ export interface Config {
|
||||
*/
|
||||
search?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* (Optional) TaskScheduleDefinition for the refresh.
|
||||
*/
|
||||
schedule?: TaskScheduleDefinitionConfig;
|
||||
}
|
||||
| Record<
|
||||
string,
|
||||
@@ -292,6 +291,11 @@ export interface Config {
|
||||
*/
|
||||
search?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* (Optional) TaskScheduleDefinition for the refresh.
|
||||
*/
|
||||
schedule?: TaskScheduleDefinitionConfig;
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"alphaTypes": "dist/index.alpha.d.ts",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
@@ -23,20 +24,22 @@
|
||||
"backstage"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "backstage-cli package build",
|
||||
"start": "backstage-cli package start",
|
||||
"build": "backstage-cli package build --experimental-type-build",
|
||||
"lint": "backstage-cli package lint",
|
||||
"test": "backstage-cli package test",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack",
|
||||
"clean": "backstage-cli package clean",
|
||||
"start": "backstage-cli package start"
|
||||
"clean": "backstage-cli package clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@azure/identity": "^2.1.0",
|
||||
"@backstage/backend-plugin-api": "workspace:^",
|
||||
"@backstage/backend-tasks": "workspace:^",
|
||||
"@backstage/catalog-model": "workspace:^",
|
||||
"@backstage/config": "workspace:^",
|
||||
"@backstage/plugin-catalog-backend": "workspace:^",
|
||||
"@backstage/plugin-catalog-node": "workspace:^",
|
||||
"@microsoft/microsoft-graph-types": "^2.6.0",
|
||||
"@types/node-fetch": "^2.5.12",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -51,11 +54,13 @@
|
||||
"@backstage/backend-test-utils": "workspace:^",
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@types/lodash": "^4.14.151",
|
||||
"luxon": "^3.0.0",
|
||||
"msw": "^0.47.0"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"config.d.ts"
|
||||
"alpha",
|
||||
"config.d.ts",
|
||||
"dist"
|
||||
],
|
||||
"configSchema": "config.d.ts"
|
||||
}
|
||||
|
||||
@@ -20,5 +20,7 @@
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export * from './processors';
|
||||
export * from './microsoftGraph';
|
||||
export * from './processors';
|
||||
export { microsoftGraphOrgEntityProviderCatalogModule } from './service/MicrosoftGraphOrgEntityProviderCatalogModule';
|
||||
export type { MicrosoftGraphOrgEntityProviderCatalogModuleOptions } from './service/MicrosoftGraphOrgEntityProviderCatalogModule';
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { Duration } from 'luxon';
|
||||
import { readMicrosoftGraphConfig, readProviderConfigs } from './config';
|
||||
|
||||
describe('readMicrosoftGraphConfig', () => {
|
||||
@@ -172,6 +173,12 @@ describe('readProviderConfigs', () => {
|
||||
filter: 'securityEnabled eq false',
|
||||
select: ['id', 'displayName', 'description'],
|
||||
},
|
||||
schedule: {
|
||||
frequency: 'PT30M',
|
||||
timeout: {
|
||||
minutes: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -192,6 +199,12 @@ describe('readProviderConfigs', () => {
|
||||
groupExpand: 'member',
|
||||
groupSelect: ['id', 'displayName', 'description'],
|
||||
groupFilter: 'securityEnabled eq false',
|
||||
schedule: {
|
||||
frequency: Duration.fromISO('PT30M'),
|
||||
timeout: {
|
||||
minutes: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
expect(actual).toEqual(expected);
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
readTaskScheduleDefinitionFromConfig,
|
||||
TaskScheduleDefinition,
|
||||
} from '@backstage/backend-tasks';
|
||||
import { Config } from '@backstage/config';
|
||||
import { trimEnd } from 'lodash';
|
||||
|
||||
@@ -121,6 +125,11 @@ export type MicrosoftGraphProviderConfig = {
|
||||
* Some features like `$expand` are not available for advanced queries, though.
|
||||
*/
|
||||
queryMode?: 'basic' | 'advanced';
|
||||
|
||||
/**
|
||||
* Schedule configuration for refresh tasks.
|
||||
*/
|
||||
schedule?: TaskScheduleDefinition;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -296,6 +305,10 @@ export function readProviderConfig(
|
||||
throw new Error(`clientId must be provided when clientSecret is defined.`);
|
||||
}
|
||||
|
||||
const schedule = config.has('schedule')
|
||||
? readTaskScheduleDefinitionFromConfig(config.getConfig('schedule'))
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
id,
|
||||
target,
|
||||
@@ -312,5 +325,6 @@ export function readProviderConfig(
|
||||
queryMode,
|
||||
userGroupMemberFilter,
|
||||
userGroupMemberSearch,
|
||||
schedule,
|
||||
};
|
||||
}
|
||||
|
||||
+207
-58
@@ -14,6 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import {
|
||||
PluginTaskScheduler,
|
||||
TaskInvocationDefinition,
|
||||
TaskRunner,
|
||||
} from '@backstage/backend-tasks';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import {
|
||||
ANNOTATION_LOCATION,
|
||||
@@ -43,10 +48,21 @@ const readMicrosoftGraphOrgMocked = readMicrosoftGraphOrg as jest.Mock<
|
||||
Promise<{ users: UserEntity[]; groups: GroupEntity[] }>
|
||||
>;
|
||||
|
||||
describe('MicrosoftGraphOrgEntityProvider', () => {
|
||||
afterEach(() => jest.resetAllMocks());
|
||||
class PersistingTaskRunner implements TaskRunner {
|
||||
private tasks: TaskInvocationDefinition[] = [];
|
||||
|
||||
it('should apply mutation', async () => {
|
||||
getTasks() {
|
||||
return this.tasks;
|
||||
}
|
||||
|
||||
run(task: TaskInvocationDefinition): Promise<void> {
|
||||
this.tasks.push(task);
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
describe('MicrosoftGraphOrgEntityProvider', () => {
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.spyOn(MicrosoftGraphClient, 'create')
|
||||
.mockReturnValue({} as unknown as MicrosoftGraphClient);
|
||||
@@ -78,8 +94,65 @@ describe('MicrosoftGraphOrgEntityProvider', () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
const config = {
|
||||
afterEach(() => jest.resetAllMocks());
|
||||
|
||||
const logger = getVoidLogger();
|
||||
const taskRunner = new PersistingTaskRunner();
|
||||
const scheduler = {
|
||||
createScheduledTaskRunner: (_: any) => taskRunner,
|
||||
} as unknown as PluginTaskScheduler;
|
||||
const entityProviderConnection: EntityProviderConnection = {
|
||||
applyMutation: jest.fn(),
|
||||
refresh: jest.fn(),
|
||||
};
|
||||
|
||||
const expectedMutation = {
|
||||
entities: [
|
||||
{
|
||||
entity: {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'User',
|
||||
metadata: {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location': 'msgraph:customProviderId/u1',
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'msgraph:customProviderId/u1',
|
||||
},
|
||||
name: 'u1',
|
||||
},
|
||||
spec: {
|
||||
memberOf: [],
|
||||
},
|
||||
},
|
||||
locationKey: 'msgraph-org-provider:customProviderId',
|
||||
},
|
||||
{
|
||||
entity: {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Group',
|
||||
metadata: {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location': 'msgraph:customProviderId/g1',
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'msgraph:customProviderId/g1',
|
||||
},
|
||||
name: 'g1',
|
||||
},
|
||||
spec: {
|
||||
children: [],
|
||||
type: 'team',
|
||||
},
|
||||
},
|
||||
locationKey: 'msgraph-org-provider:customProviderId',
|
||||
},
|
||||
],
|
||||
type: 'full',
|
||||
};
|
||||
|
||||
it('should apply mutation - manual', async () => {
|
||||
const config = new ConfigReader({
|
||||
catalog: {
|
||||
providers: {
|
||||
microsoftGraphOrg: {
|
||||
@@ -92,67 +165,143 @@ describe('MicrosoftGraphOrgEntityProvider', () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const entityProviderConnection: EntityProviderConnection = {
|
||||
applyMutation: jest.fn(),
|
||||
refresh: jest.fn(),
|
||||
};
|
||||
const provider = MicrosoftGraphOrgEntityProvider.fromConfig(
|
||||
new ConfigReader(config),
|
||||
{
|
||||
logger: getVoidLogger(),
|
||||
schedule: 'manual',
|
||||
},
|
||||
)[0];
|
||||
|
||||
provider.connect(entityProviderConnection);
|
||||
});
|
||||
const provider = MicrosoftGraphOrgEntityProvider.fromConfig(config, {
|
||||
logger,
|
||||
schedule: 'manual',
|
||||
})[0];
|
||||
|
||||
await provider.connect(entityProviderConnection);
|
||||
await provider.read();
|
||||
|
||||
expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({
|
||||
entities: [
|
||||
{
|
||||
entity: {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'User',
|
||||
metadata: {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location':
|
||||
'msgraph:customProviderId/u1',
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'msgraph:customProviderId/u1',
|
||||
},
|
||||
name: 'u1',
|
||||
},
|
||||
spec: {
|
||||
memberOf: [],
|
||||
expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith(
|
||||
expectedMutation,
|
||||
);
|
||||
});
|
||||
|
||||
it('should apply mutation - schedule', async () => {
|
||||
const config = new ConfigReader({
|
||||
catalog: {
|
||||
providers: {
|
||||
microsoftGraphOrg: {
|
||||
customProviderId: {
|
||||
target: 'target',
|
||||
tenantId: 'tenantId',
|
||||
clientId: 'clientId',
|
||||
clientSecret: 'clientSecret',
|
||||
},
|
||||
},
|
||||
locationKey: 'msgraph-org-provider:customProviderId',
|
||||
},
|
||||
{
|
||||
entity: {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Group',
|
||||
metadata: {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location':
|
||||
'msgraph:customProviderId/g1',
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'msgraph:customProviderId/g1',
|
||||
},
|
||||
name: 'g1',
|
||||
},
|
||||
spec: {
|
||||
children: [],
|
||||
type: 'team',
|
||||
},
|
||||
},
|
||||
locationKey: 'msgraph-org-provider:customProviderId',
|
||||
},
|
||||
],
|
||||
type: 'full',
|
||||
},
|
||||
});
|
||||
const provider = MicrosoftGraphOrgEntityProvider.fromConfig(config, {
|
||||
logger,
|
||||
schedule: taskRunner,
|
||||
})[0];
|
||||
expect(provider.getProviderName()).toEqual(
|
||||
'MicrosoftGraphOrgEntityProvider:customProviderId',
|
||||
);
|
||||
|
||||
await provider.connect(entityProviderConnection);
|
||||
|
||||
const taskDef = taskRunner.getTasks()[0];
|
||||
expect(taskDef.id).toEqual(
|
||||
'MicrosoftGraphOrgEntityProvider:customProviderId:refresh',
|
||||
);
|
||||
await (taskDef.fn as () => Promise<void>)();
|
||||
|
||||
expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith(
|
||||
expectedMutation,
|
||||
);
|
||||
});
|
||||
|
||||
it('should apply mutation - scheduler', async () => {
|
||||
const config = new ConfigReader({
|
||||
catalog: {
|
||||
providers: {
|
||||
microsoftGraphOrg: {
|
||||
customProviderId: {
|
||||
target: 'target',
|
||||
tenantId: 'tenantId',
|
||||
clientId: 'clientId',
|
||||
clientSecret: 'clientSecret',
|
||||
schedule: {
|
||||
frequency: 'PT30M',
|
||||
timeout: 'PT3M',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const provider = MicrosoftGraphOrgEntityProvider.fromConfig(config, {
|
||||
logger,
|
||||
scheduler,
|
||||
})[0];
|
||||
expect(provider.getProviderName()).toEqual(
|
||||
'MicrosoftGraphOrgEntityProvider:customProviderId',
|
||||
);
|
||||
|
||||
await provider.connect(entityProviderConnection);
|
||||
|
||||
const taskDef = taskRunner.getTasks()[0];
|
||||
expect(taskDef.id).toEqual(
|
||||
'MicrosoftGraphOrgEntityProvider:customProviderId:refresh',
|
||||
);
|
||||
await (taskDef.fn as () => Promise<void>)();
|
||||
|
||||
expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith(
|
||||
expectedMutation,
|
||||
);
|
||||
});
|
||||
|
||||
it('fail without schedule and scheduler', () => {
|
||||
const config = new ConfigReader({
|
||||
catalog: {
|
||||
providers: {
|
||||
microsoftGraphOrg: {
|
||||
customProviderId: {
|
||||
target: 'target',
|
||||
tenantId: 'tenantId',
|
||||
clientId: 'clientId',
|
||||
clientSecret: 'clientSecret',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
MicrosoftGraphOrgEntityProvider.fromConfig(config, {
|
||||
logger,
|
||||
}),
|
||||
).toThrow('Either schedule or scheduler must be provided');
|
||||
});
|
||||
|
||||
it('fail with scheduler but no schedule config', () => {
|
||||
const config = new ConfigReader({
|
||||
catalog: {
|
||||
providers: {
|
||||
microsoftGraphOrg: {
|
||||
customProviderId: {
|
||||
target: 'target',
|
||||
tenantId: 'tenantId',
|
||||
clientId: 'clientId',
|
||||
clientSecret: 'clientSecret',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
MicrosoftGraphOrgEntityProvider.fromConfig(config, {
|
||||
logger,
|
||||
scheduler,
|
||||
}),
|
||||
).toThrow(
|
||||
'No schedule provided neither via code nor config for MicrosoftGraphOrgEntityProvider:customProviderId',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+31
-12
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { TaskRunner } from '@backstage/backend-tasks';
|
||||
import { PluginTaskScheduler, TaskRunner } from '@backstage/backend-tasks';
|
||||
import {
|
||||
ANNOTATION_LOCATION,
|
||||
ANNOTATION_ORIGIN_LOCATION,
|
||||
@@ -67,7 +67,13 @@ export type MicrosoftGraphOrgEntityProviderOptions =
|
||||
* {@link @backstage/backend-tasks#PluginTaskScheduler.createScheduledTaskRunner}
|
||||
* to enable automatic scheduling of tasks.
|
||||
*/
|
||||
schedule: 'manual' | TaskRunner;
|
||||
schedule?: 'manual' | TaskRunner;
|
||||
|
||||
/**
|
||||
* Scheduler used to schedule refreshes based on
|
||||
* the schedule config.
|
||||
*/
|
||||
scheduler?: PluginTaskScheduler;
|
||||
|
||||
/**
|
||||
* The function that transforms a user entry in msgraph to an entity.
|
||||
@@ -168,6 +174,10 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider {
|
||||
];
|
||||
}
|
||||
|
||||
if (!options.schedule && !options.scheduler) {
|
||||
throw new Error('Either schedule or scheduler must be provided.');
|
||||
}
|
||||
|
||||
function getTransformer<T extends Function>(
|
||||
id: string,
|
||||
transformers?: T | Record<string, T>,
|
||||
@@ -180,6 +190,16 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider {
|
||||
}
|
||||
|
||||
return readProviderConfigs(configRoot).map(providerConfig => {
|
||||
if (!options.schedule && !providerConfig.schedule) {
|
||||
throw new Error(
|
||||
`No schedule provided neither via code nor config for MicrosoftGraphOrgEntityProvider:${providerConfig.id}.`,
|
||||
);
|
||||
}
|
||||
|
||||
const taskRunner =
|
||||
options.schedule ??
|
||||
options.scheduler!.createScheduledTaskRunner(providerConfig.schedule!);
|
||||
|
||||
const provider = new MicrosoftGraphOrgEntityProvider({
|
||||
id: providerConfig.id,
|
||||
provider: providerConfig,
|
||||
@@ -197,7 +217,10 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider {
|
||||
options.organizationTransformer,
|
||||
),
|
||||
});
|
||||
provider.schedule(options.schedule);
|
||||
|
||||
if (taskRunner !== 'manual') {
|
||||
provider.schedule(taskRunner);
|
||||
}
|
||||
|
||||
return provider;
|
||||
});
|
||||
@@ -238,7 +261,9 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider {
|
||||
provider,
|
||||
});
|
||||
|
||||
result.schedule(options.schedule);
|
||||
if (options.schedule !== 'manual') {
|
||||
result.schedule(options.schedule);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -311,16 +336,10 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider {
|
||||
markCommitComplete();
|
||||
}
|
||||
|
||||
private schedule(
|
||||
schedule: MicrosoftGraphOrgEntityProviderOptions['schedule'],
|
||||
) {
|
||||
if (schedule === 'manual') {
|
||||
return;
|
||||
}
|
||||
|
||||
private schedule(taskRunner: TaskRunner) {
|
||||
this.scheduleFn = async () => {
|
||||
const id = `${this.getProviderName()}:refresh`;
|
||||
await schedule.run({
|
||||
await taskRunner.run({
|
||||
id,
|
||||
fn: async () => {
|
||||
const logger = this.options.logger.child({
|
||||
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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.
|
||||
*/
|
||||
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import {
|
||||
configServiceRef,
|
||||
loggerServiceRef,
|
||||
schedulerServiceRef,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import {
|
||||
PluginTaskScheduler,
|
||||
TaskScheduleDefinition,
|
||||
} from '@backstage/backend-tasks';
|
||||
import { startTestBackend } from '@backstage/backend-test-utils';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node';
|
||||
import { Duration } from 'luxon';
|
||||
import { microsoftGraphOrgEntityProviderCatalogModule } from './MicrosoftGraphOrgEntityProviderCatalogModule';
|
||||
import { MicrosoftGraphOrgEntityProvider } from '../processors';
|
||||
|
||||
describe('awsS3EntityProviderCatalogModule', () => {
|
||||
it('should register provider at the catalog extension point', async () => {
|
||||
let addedProviders: Array<MicrosoftGraphOrgEntityProvider> | undefined;
|
||||
let usedSchedule: TaskScheduleDefinition | undefined;
|
||||
|
||||
const extensionPoint = {
|
||||
addEntityProvider: (providers: any) => {
|
||||
addedProviders = providers;
|
||||
},
|
||||
};
|
||||
const runner = jest.fn();
|
||||
const scheduler = {
|
||||
createScheduledTaskRunner: (schedule: TaskScheduleDefinition) => {
|
||||
usedSchedule = schedule;
|
||||
return runner;
|
||||
},
|
||||
} as unknown as PluginTaskScheduler;
|
||||
|
||||
const config = new ConfigReader({
|
||||
catalog: {
|
||||
providers: {
|
||||
microsoftGraphOrg: {
|
||||
customProviderId: {
|
||||
target: 'target',
|
||||
tenantId: 'tenantId',
|
||||
clientId: 'clientId',
|
||||
clientSecret: 'clientSecret',
|
||||
schedule: {
|
||||
frequency: 'PT30M',
|
||||
timeout: 'PT3M',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await startTestBackend({
|
||||
extensionPoints: [[catalogProcessingExtensionPoint, extensionPoint]],
|
||||
services: [
|
||||
[configServiceRef, config],
|
||||
[loggerServiceRef, getVoidLogger()],
|
||||
[schedulerServiceRef, scheduler],
|
||||
],
|
||||
features: [microsoftGraphOrgEntityProviderCatalogModule()],
|
||||
});
|
||||
|
||||
expect(usedSchedule?.frequency).toEqual(Duration.fromISO('PT30M'));
|
||||
expect(usedSchedule?.timeout).toEqual(Duration.fromISO('PT3M'));
|
||||
expect(addedProviders?.length).toEqual(1);
|
||||
expect(addedProviders?.pop()?.getProviderName()).toEqual(
|
||||
'MicrosoftGraphOrgEntityProvider:customProviderId',
|
||||
);
|
||||
expect(runner).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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.
|
||||
*/
|
||||
|
||||
import {
|
||||
configServiceRef,
|
||||
createBackendModule,
|
||||
loggerServiceRef,
|
||||
loggerToWinstonLogger,
|
||||
schedulerServiceRef,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node';
|
||||
import {
|
||||
GroupTransformer,
|
||||
OrganizationTransformer,
|
||||
UserTransformer,
|
||||
} from '../microsoftGraph';
|
||||
import { MicrosoftGraphOrgEntityProvider } from '../processors';
|
||||
|
||||
/**
|
||||
* Options for {@link microsoftGraphOrgEntityProviderCatalogModule}.
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export interface MicrosoftGraphOrgEntityProviderCatalogModuleOptions {
|
||||
/**
|
||||
* The function that transforms a user entry in msgraph to an entity.
|
||||
* Optionally, you can pass separate transformers per provider ID.
|
||||
*/
|
||||
userTransformer?: UserTransformer | Record<string, UserTransformer>;
|
||||
|
||||
/**
|
||||
* The function that transforms a group entry in msgraph to an entity.
|
||||
* Optionally, you can pass separate transformers per provider ID.
|
||||
*/
|
||||
groupTransformer?: GroupTransformer | Record<string, GroupTransformer>;
|
||||
|
||||
/**
|
||||
* The function that transforms an organization entry in msgraph to an entity.
|
||||
* Optionally, you can pass separate transformers per provider ID.
|
||||
*/
|
||||
organizationTransformer?:
|
||||
| OrganizationTransformer
|
||||
| Record<string, OrganizationTransformer>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the MicrosoftGraphOrgEntityProvider with the catalog processing extension point.
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export const microsoftGraphOrgEntityProviderCatalogModule = createBackendModule(
|
||||
{
|
||||
pluginId: 'catalog',
|
||||
moduleId: 'microsoftGraphOrgEntityProvider',
|
||||
register(
|
||||
env,
|
||||
options?: MicrosoftGraphOrgEntityProviderCatalogModuleOptions,
|
||||
) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
catalog: catalogProcessingExtensionPoint,
|
||||
config: configServiceRef,
|
||||
logger: loggerServiceRef,
|
||||
scheduler: schedulerServiceRef,
|
||||
},
|
||||
async init({ catalog, config, logger, scheduler }) {
|
||||
catalog.addEntityProvider(
|
||||
MicrosoftGraphOrgEntityProvider.fromConfig(config, {
|
||||
groupTransformer: options?.groupTransformer,
|
||||
logger: loggerToWinstonLogger(logger),
|
||||
organizationTransformer: options?.organizationTransformer,
|
||||
scheduler,
|
||||
userTransformer: options?.userTransformer,
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -98,7 +98,7 @@ export const EntityListComponent = (props: EntityListComponentProps) => {
|
||||
<ListItem
|
||||
dense
|
||||
button={Boolean(onItemClick) as any}
|
||||
onClick={() => onItemClick?.call(this, r.target)}
|
||||
onClick={() => onItemClick?.(r.target)}
|
||||
>
|
||||
<ListItemIcon>{locationListItemIcon(r.target)}</ListItemIcon>
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
import React from 'react';
|
||||
import { fireEvent, waitFor } from '@testing-library/react';
|
||||
import { capitalize } from 'lodash';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { EntityTypePicker } from './EntityTypePicker';
|
||||
import { MockEntityListContextProvider } from '../../testUtils/providers';
|
||||
@@ -99,11 +98,11 @@ describe('<EntityTypePicker/>', () => {
|
||||
const input = rendered.getByTestId('select');
|
||||
fireEvent.click(input);
|
||||
|
||||
await waitFor(() => rendered.getByText('Service'));
|
||||
await waitFor(() => rendered.getByText('service'));
|
||||
|
||||
entities.forEach(entity => {
|
||||
expect(
|
||||
rendered.getByText(capitalize(entity.spec!.type as string)),
|
||||
rendered.getByText(entity.spec!.type as string),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -125,15 +124,15 @@ describe('<EntityTypePicker/>', () => {
|
||||
const input = rendered.getByTestId('select');
|
||||
fireEvent.click(input);
|
||||
|
||||
await waitFor(() => rendered.getByText('Service'));
|
||||
fireEvent.click(rendered.getByText('Service'));
|
||||
await waitFor(() => rendered.getByText('service'));
|
||||
fireEvent.click(rendered.getByText('service'));
|
||||
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
type: new EntityTypeFilter(['service']),
|
||||
});
|
||||
|
||||
fireEvent.click(input);
|
||||
fireEvent.click(rendered.getByText('All'));
|
||||
fireEvent.click(rendered.getByText('all'));
|
||||
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({ type: undefined });
|
||||
});
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
import { Box } from '@material-ui/core';
|
||||
import { useEntityTypeFilter } from '../../hooks/useEntityTypeFilter';
|
||||
|
||||
@@ -54,10 +53,10 @@ export const EntityTypePicker = (props: EntityTypePickerProps) => {
|
||||
if (availableTypes.length === 0 || error) return null;
|
||||
|
||||
const items = [
|
||||
{ value: 'all', label: 'All' },
|
||||
{ value: 'all', label: 'all' },
|
||||
...availableTypes.map((type: string) => ({
|
||||
value: type,
|
||||
label: capitalize(type),
|
||||
label: type,
|
||||
})),
|
||||
];
|
||||
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
|
||||
Website: [https://circleci.com/](https://circleci.com/)
|
||||
|
||||
<img src="./src/assets/screenshot-1.png" />
|
||||
<img src="./src/assets/screenshot-2.png" />
|
||||
## Screenshots
|
||||
|
||||
<img src="./src/assets/screenshot-pipeline-list.png" />
|
||||
<img src="./src/assets/screenshot-build-details.png" />
|
||||
<img src="./src/assets/screenshot-build-failure.png" />
|
||||
|
||||
## Setup
|
||||
|
||||
@@ -43,7 +46,7 @@ proxy:
|
||||
```
|
||||
|
||||
5. Get and provide a `CIRCLECI_AUTH_TOKEN` as an environment variable (see the [CircleCI docs](https://circleci.com/docs/api/#add-an-api-token)).
|
||||
6. Add a `circleci.com/project-slug` annotation to your respective `catalog-info.yaml` files, on the format <git-provider>/<owner>/<project> (https://backstage.io/docs/architecture-decisions/adrs-adr002#format).
|
||||
6. Add an annotation to your respective `catalog-info.yaml` files, with the format `circleci.com/project-slug: <git-provider>/<owner>/<project>` (See reference in [ADR002](https://backstage.io/docs/architecture-decisions/adrs-adr002#format)).
|
||||
|
||||
```yaml
|
||||
# Example catalog-info.yaml entity definition file
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 155 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 135 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 399 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 326 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 380 KiB |
@@ -13,7 +13,7 @@ import { FactLifecycle } from '@backstage/plugin-tech-insights-node';
|
||||
import { FactRetriever } from '@backstage/plugin-tech-insights-node';
|
||||
import { FactRetrieverRegistration } from '@backstage/plugin-tech-insights-node';
|
||||
import { FactSchema } from '@backstage/plugin-tech-insights-node';
|
||||
import { HumanDuration } from '@backstage/backend-tasks';
|
||||
import { HumanDuration } from '@backstage/types';
|
||||
import { Logger } from 'winston';
|
||||
import { PluginDatabaseManager } from '@backstage/backend-common';
|
||||
import { PluginEndpointDiscovery } from '@backstage/backend-common';
|
||||
@@ -60,6 +60,7 @@ export type FactRetrieverRegistrationOptions = {
|
||||
factRetriever: FactRetriever;
|
||||
lifecycle?: FactLifecycle;
|
||||
timeout?: Duration | HumanDuration;
|
||||
initialDelay?: Duration | HumanDuration;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@backstage/plugin-tech-insights-common": "workspace:^",
|
||||
"@backstage/plugin-tech-insights-node": "workspace:^",
|
||||
"@backstage/types": "workspace:^",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/luxon": "^3.0.0",
|
||||
"express": "^4.17.1",
|
||||
|
||||
@@ -76,6 +76,7 @@ export class DefaultFactRetrieverEngine implements FactRetrieverEngine {
|
||||
private readonly scheduler: PluginTaskScheduler,
|
||||
private readonly defaultCadence?: string,
|
||||
private readonly defaultTimeout?: Duration,
|
||||
private readonly defaultInitialDelay?: Duration,
|
||||
) {}
|
||||
|
||||
static async create(options: {
|
||||
@@ -85,6 +86,7 @@ export class DefaultFactRetrieverEngine implements FactRetrieverEngine {
|
||||
scheduler: PluginTaskScheduler;
|
||||
defaultCadence?: string;
|
||||
defaultTimeout?: Duration;
|
||||
defaultInitialDelay?: Duration;
|
||||
}) {
|
||||
const {
|
||||
repository,
|
||||
@@ -93,6 +95,7 @@ export class DefaultFactRetrieverEngine implements FactRetrieverEngine {
|
||||
scheduler,
|
||||
defaultCadence,
|
||||
defaultTimeout,
|
||||
defaultInitialDelay,
|
||||
} = options;
|
||||
|
||||
const retrievers = await factRetrieverRegistry.listRetrievers();
|
||||
@@ -106,6 +109,7 @@ export class DefaultFactRetrieverEngine implements FactRetrieverEngine {
|
||||
scheduler,
|
||||
defaultCadence,
|
||||
defaultTimeout,
|
||||
defaultInitialDelay,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -115,17 +119,25 @@ export class DefaultFactRetrieverEngine implements FactRetrieverEngine {
|
||||
|
||||
await Promise.all(
|
||||
registrations.map(async registration => {
|
||||
const { factRetriever, cadence, lifecycle, timeout } = registration;
|
||||
const { factRetriever, cadence, lifecycle, timeout, initialDelay } =
|
||||
registration;
|
||||
const cronExpression =
|
||||
cadence || this.defaultCadence || randomDailyCron();
|
||||
const timeLimit =
|
||||
timeout || this.defaultTimeout || Duration.fromObject({ minutes: 5 });
|
||||
const initialDelaySetting =
|
||||
initialDelay ||
|
||||
this.defaultInitialDelay ||
|
||||
Duration.fromObject({ seconds: 5 });
|
||||
try {
|
||||
await this.scheduler.scheduleTask({
|
||||
id: factRetriever.id,
|
||||
frequency: { cron: cronExpression },
|
||||
fn: this.createFactRetrieverHandler(factRetriever, lifecycle),
|
||||
timeout: timeLimit,
|
||||
// We add a delay in order to prevent errors due to the
|
||||
// fact that the backend is not yet online in a cold-start scenario
|
||||
initialDelay: initialDelaySetting,
|
||||
});
|
||||
newRegs.push(factRetriever.id);
|
||||
} catch (e) {
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { HumanDuration } from '@backstage/backend-tasks';
|
||||
|
||||
import { HumanDuration } from '@backstage/types';
|
||||
import {
|
||||
FactLifecycle,
|
||||
FactRetriever,
|
||||
@@ -35,6 +36,7 @@ export type FactRetrieverRegistrationOptions = {
|
||||
factRetriever: FactRetriever;
|
||||
lifecycle?: FactLifecycle;
|
||||
timeout?: Duration | HumanDuration;
|
||||
initialDelay?: Duration | HumanDuration;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -45,6 +47,7 @@ export type FactRetrieverRegistrationOptions = {
|
||||
* @param factRetriever - Implementation of fact retriever consisting of at least id, version, schema and handler
|
||||
* @param lifecycle - Optional lifecycle definition indicating the cleanup logic of facts when this retriever is run
|
||||
* @param timeout - Optional duration to determine how long the fact retriever should be allowed to run, defaults to 5 minutes
|
||||
* @param initialDelay - Optional initial delay to determine how long the fact retriever should wait before the initial run, defaults to 5 seconds
|
||||
*
|
||||
*
|
||||
* @remarks
|
||||
@@ -68,11 +71,12 @@ export type FactRetrieverRegistrationOptions = {
|
||||
export function createFactRetrieverRegistration(
|
||||
options: FactRetrieverRegistrationOptions,
|
||||
): FactRetrieverRegistration {
|
||||
const { cadence, factRetriever, lifecycle, timeout } = options;
|
||||
const { cadence, factRetriever, lifecycle, timeout, initialDelay } = options;
|
||||
return {
|
||||
cadence,
|
||||
factRetriever,
|
||||
lifecycle,
|
||||
timeout,
|
||||
initialDelay,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Config } from '@backstage/config';
|
||||
import { DateTime } from 'luxon';
|
||||
import { Duration } from 'luxon';
|
||||
import { DurationLike } from 'luxon';
|
||||
import { HumanDuration } from '@backstage/backend-tasks';
|
||||
import { HumanDuration } from '@backstage/types';
|
||||
import { JsonValue } from '@backstage/types';
|
||||
import { Logger } from 'winston';
|
||||
import { PluginEndpointDiscovery } from '@backstage/backend-common';
|
||||
@@ -75,6 +75,7 @@ export type FactRetrieverRegistration = {
|
||||
cadence?: string;
|
||||
timeout?: Duration | HumanDuration;
|
||||
lifecycle?: FactLifecycle;
|
||||
initialDelay?: Duration | HumanDuration;
|
||||
};
|
||||
|
||||
// @public
|
||||
|
||||
@@ -15,13 +15,12 @@
|
||||
*/
|
||||
import { DateTime, Duration, DurationLike } from 'luxon';
|
||||
import { Config } from '@backstage/config';
|
||||
import { JsonValue } from '@backstage/types';
|
||||
import { HumanDuration, JsonValue } from '@backstage/types';
|
||||
import {
|
||||
PluginEndpointDiscovery,
|
||||
TokenManager,
|
||||
} from '@backstage/backend-common';
|
||||
import { Logger } from 'winston';
|
||||
import { HumanDuration } from '@backstage/backend-tasks';
|
||||
|
||||
/**
|
||||
* A container for facts. The shape of the fact records needs to correspond to the FactSchema with same `ref` value.
|
||||
@@ -279,4 +278,11 @@ export type FactRetrieverRegistration = {
|
||||
* If defined this value will be used to determine expired items which will deleted when this fact retriever is run
|
||||
*/
|
||||
lifecycle?: FactLifecycle;
|
||||
|
||||
/**
|
||||
* A duration to determine the initial delay for the fact retriever. Useful for cold start scenarios when e.g. the
|
||||
* catalog backend is not yet available. Defaults to 5 seconds.
|
||||
*
|
||||
*/
|
||||
initialDelay?: Duration | HumanDuration;
|
||||
};
|
||||
|
||||
@@ -3674,7 +3674,6 @@ __metadata:
|
||||
"@types/node": ^16.11.26
|
||||
"@types/recursive-readdir": ^2.2.0
|
||||
chalk: ^4.0.0
|
||||
command-exists: ^1.2.9
|
||||
commander: ^9.1.0
|
||||
fs-extra: 10.1.0
|
||||
handlebars: ^4.7.3
|
||||
@@ -4680,16 +4679,19 @@ __metadata:
|
||||
dependencies:
|
||||
"@azure/identity": ^2.1.0
|
||||
"@backstage/backend-common": "workspace:^"
|
||||
"@backstage/backend-plugin-api": "workspace:^"
|
||||
"@backstage/backend-tasks": "workspace:^"
|
||||
"@backstage/backend-test-utils": "workspace:^"
|
||||
"@backstage/catalog-model": "workspace:^"
|
||||
"@backstage/cli": "workspace:^"
|
||||
"@backstage/config": "workspace:^"
|
||||
"@backstage/plugin-catalog-backend": "workspace:^"
|
||||
"@backstage/plugin-catalog-node": "workspace:^"
|
||||
"@microsoft/microsoft-graph-types": ^2.6.0
|
||||
"@types/lodash": ^4.14.151
|
||||
"@types/node-fetch": ^2.5.12
|
||||
lodash: ^4.17.21
|
||||
luxon: ^3.0.0
|
||||
msw: ^0.47.0
|
||||
node-fetch: ^2.6.7
|
||||
p-limit: ^3.0.2
|
||||
@@ -7112,6 +7114,7 @@ __metadata:
|
||||
"@backstage/errors": "workspace:^"
|
||||
"@backstage/plugin-tech-insights-common": "workspace:^"
|
||||
"@backstage/plugin-tech-insights-node": "workspace:^"
|
||||
"@backstage/types": "workspace:^"
|
||||
"@types/express": ^4.17.6
|
||||
"@types/luxon": ^3.0.0
|
||||
"@types/semver": ^7.3.8
|
||||
@@ -7690,6 +7693,7 @@ __metadata:
|
||||
dependencies:
|
||||
"@backstage/cli": "workspace:^"
|
||||
"@types/zen-observable": ^0.8.0
|
||||
luxon: ^3.0.0
|
||||
zen-observable: ^0.8.15
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
@@ -7729,15 +7733,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@changesets/apply-release-plan@npm:^6.1.0":
|
||||
version: 6.1.0
|
||||
resolution: "@changesets/apply-release-plan@npm:6.1.0"
|
||||
"@changesets/apply-release-plan@npm:^6.1.1":
|
||||
version: 6.1.1
|
||||
resolution: "@changesets/apply-release-plan@npm:6.1.1"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.10.4
|
||||
"@changesets/config": ^2.1.1
|
||||
"@changesets/config": ^2.2.0
|
||||
"@changesets/get-version-range-type": ^0.3.2
|
||||
"@changesets/git": ^1.4.1
|
||||
"@changesets/types": ^5.1.0
|
||||
"@changesets/git": ^1.5.0
|
||||
"@changesets/types": ^5.2.0
|
||||
"@manypkg/get-packages": ^1.1.3
|
||||
detect-indent: ^6.0.0
|
||||
fs-extra: ^7.0.1
|
||||
@@ -7746,51 +7750,51 @@ __metadata:
|
||||
prettier: ^2.7.1
|
||||
resolve-from: ^5.0.0
|
||||
semver: ^5.4.1
|
||||
checksum: dbbe5301ba6b3651c788c25e9ae73a765ce8332afb013b417b91004b63be608ee898cbf1916a1e40d9d4ef11bfbe46eefe3e39524f42ef54d8a6eb615fffb292
|
||||
checksum: 34da2d52eced00bc5f51c8e8ce16c0e487743219f057da2b3e6c6597bb2d50498f3996c779e4fc29b19d357c9b700d82310d723395ba09350b9a139c9e0e0d23
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@changesets/assemble-release-plan@npm:^5.2.1":
|
||||
version: 5.2.1
|
||||
resolution: "@changesets/assemble-release-plan@npm:5.2.1"
|
||||
"@changesets/assemble-release-plan@npm:^5.2.2":
|
||||
version: 5.2.2
|
||||
resolution: "@changesets/assemble-release-plan@npm:5.2.2"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.10.4
|
||||
"@changesets/errors": ^0.1.4
|
||||
"@changesets/get-dependents-graph": ^1.3.3
|
||||
"@changesets/types": ^5.1.0
|
||||
"@changesets/get-dependents-graph": ^1.3.4
|
||||
"@changesets/types": ^5.2.0
|
||||
"@manypkg/get-packages": ^1.1.3
|
||||
semver: ^5.4.1
|
||||
checksum: 34e00f9ac98163cd81338962ce5cf27e7a8ecbd60a5040e245976fba87336aa7efdd6c54cc2112db0cda2099fefc3ad37bd24d29307a30e49a515a3279022d74
|
||||
checksum: 4f4a2108537ecbfbf9a554e441f4899f9c6d7e2b933deddd186d6f670ea1e52ebbbef3da01bc61b5cb735470536258b94d7bbac1035fe98dc277615d5017c61a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@changesets/changelog-git@npm:^0.1.12":
|
||||
version: 0.1.12
|
||||
resolution: "@changesets/changelog-git@npm:0.1.12"
|
||||
"@changesets/changelog-git@npm:^0.1.13":
|
||||
version: 0.1.13
|
||||
resolution: "@changesets/changelog-git@npm:0.1.13"
|
||||
dependencies:
|
||||
"@changesets/types": ^5.1.0
|
||||
checksum: 68ae2ccb7e18f4559c954bcc104cd2e311c6daf5a39e9339290c0e1db8db926a1432e54485c593d977083253736147d855e923053d50daaef1041aa486172538
|
||||
"@changesets/types": ^5.2.0
|
||||
checksum: c0e3b11a0a63794304e064ef01444503a864a1fbfc4c592bd3aec91ba6aa5c24fac91a47f7f0159f449cd6ce99c334290b465d84b1c98b6577937ff55974cc7e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@changesets/cli@npm:^2.14.0":
|
||||
version: 2.24.4
|
||||
resolution: "@changesets/cli@npm:2.24.4"
|
||||
version: 2.25.0
|
||||
resolution: "@changesets/cli@npm:2.25.0"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.10.4
|
||||
"@changesets/apply-release-plan": ^6.1.0
|
||||
"@changesets/assemble-release-plan": ^5.2.1
|
||||
"@changesets/changelog-git": ^0.1.12
|
||||
"@changesets/config": ^2.1.1
|
||||
"@changesets/apply-release-plan": ^6.1.1
|
||||
"@changesets/assemble-release-plan": ^5.2.2
|
||||
"@changesets/changelog-git": ^0.1.13
|
||||
"@changesets/config": ^2.2.0
|
||||
"@changesets/errors": ^0.1.4
|
||||
"@changesets/get-dependents-graph": ^1.3.3
|
||||
"@changesets/get-release-plan": ^3.0.14
|
||||
"@changesets/git": ^1.4.1
|
||||
"@changesets/get-dependents-graph": ^1.3.4
|
||||
"@changesets/get-release-plan": ^3.0.15
|
||||
"@changesets/git": ^1.5.0
|
||||
"@changesets/logger": ^0.0.5
|
||||
"@changesets/pre": ^1.0.12
|
||||
"@changesets/read": ^0.5.7
|
||||
"@changesets/types": ^5.1.0
|
||||
"@changesets/write": ^0.2.0
|
||||
"@changesets/pre": ^1.0.13
|
||||
"@changesets/read": ^0.5.8
|
||||
"@changesets/types": ^5.2.0
|
||||
"@changesets/write": ^0.2.1
|
||||
"@manypkg/get-packages": ^1.1.3
|
||||
"@types/is-ci": ^3.0.0
|
||||
"@types/semver": ^6.0.0
|
||||
@@ -7812,22 +7816,22 @@ __metadata:
|
||||
tty-table: ^4.1.5
|
||||
bin:
|
||||
changeset: bin.js
|
||||
checksum: 899e0f3b075e12d28f71ed05e385c642ce9d56de06f9f60da346c15004612c8629c4619bcfaca860c9a947cfb3f0df904ea6cc080ff4de5c60c814665771f4ab
|
||||
checksum: 54439bdfa7ca115964482f6323e9475b5917d68c2e0752a272ed133b863fce51eeba86366d8740a275dc0b891af8e272602da69d04144f40e4bf3531f48fe8fb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@changesets/config@npm:^2.1.1":
|
||||
version: 2.1.1
|
||||
resolution: "@changesets/config@npm:2.1.1"
|
||||
"@changesets/config@npm:^2.2.0":
|
||||
version: 2.2.0
|
||||
resolution: "@changesets/config@npm:2.2.0"
|
||||
dependencies:
|
||||
"@changesets/errors": ^0.1.4
|
||||
"@changesets/get-dependents-graph": ^1.3.3
|
||||
"@changesets/get-dependents-graph": ^1.3.4
|
||||
"@changesets/logger": ^0.0.5
|
||||
"@changesets/types": ^5.1.0
|
||||
"@changesets/types": ^5.2.0
|
||||
"@manypkg/get-packages": ^1.1.3
|
||||
fs-extra: ^7.0.1
|
||||
micromatch: ^4.0.2
|
||||
checksum: 12b47efd06f9ca19f00a170517dad0e2041c2f285eff308c37e955893d24e981addc941ae545e23f9507628674ef2b9321d7423fc495bc17d00ea09066d68fc7
|
||||
checksum: 18a6ae52150a7426bcaeb4018f7eb4e330dec69a448b093d604f1cd73d89b689eb791777cee5af57f9e572403ccbf196a572b33dbb702bae4bccfbbbd3be5ffc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -7840,31 +7844,31 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@changesets/get-dependents-graph@npm:^1.3.3":
|
||||
version: 1.3.3
|
||||
resolution: "@changesets/get-dependents-graph@npm:1.3.3"
|
||||
"@changesets/get-dependents-graph@npm:^1.3.4":
|
||||
version: 1.3.4
|
||||
resolution: "@changesets/get-dependents-graph@npm:1.3.4"
|
||||
dependencies:
|
||||
"@changesets/types": ^5.1.0
|
||||
"@changesets/types": ^5.2.0
|
||||
"@manypkg/get-packages": ^1.1.3
|
||||
chalk: ^2.1.0
|
||||
fs-extra: ^7.0.1
|
||||
semver: ^5.4.1
|
||||
checksum: 0ccde4d7e233fef875447e08219da45eb3cd9b073124b6ce051b35ed3fac89ce2cb185fce41eae1ed2a41aa741cd9f999f5384c5c76086c72c5b5e7609716fd9
|
||||
checksum: 584852e17fd102271dea520f851c85130605f232f7f52126d202b0041269af12d5681744bfa7fecf41048e5fd62a71da726be471207832060408d9cfb35ec3fb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@changesets/get-release-plan@npm:^3.0.14":
|
||||
version: 3.0.14
|
||||
resolution: "@changesets/get-release-plan@npm:3.0.14"
|
||||
"@changesets/get-release-plan@npm:^3.0.15":
|
||||
version: 3.0.15
|
||||
resolution: "@changesets/get-release-plan@npm:3.0.15"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.10.4
|
||||
"@changesets/assemble-release-plan": ^5.2.1
|
||||
"@changesets/config": ^2.1.1
|
||||
"@changesets/pre": ^1.0.12
|
||||
"@changesets/read": ^0.5.7
|
||||
"@changesets/types": ^5.1.0
|
||||
"@changesets/assemble-release-plan": ^5.2.2
|
||||
"@changesets/config": ^2.2.0
|
||||
"@changesets/pre": ^1.0.13
|
||||
"@changesets/read": ^0.5.8
|
||||
"@changesets/types": ^5.2.0
|
||||
"@manypkg/get-packages": ^1.1.3
|
||||
checksum: 32ea990b58b9c138ec0e335b0ee69e4c4e7ef802adcf28a5b843d25cb1155d68d7621063fbb18ea200bcc20112575df902722792b6428431b8ee30b8bb6b8b77
|
||||
checksum: f2d33982dfeabe12e0eba976a3cc68e05bdd834b4dd6283f42464d028852e46f90cb14b84f0b5425813c787494d0490dc12df5d25d63559d07f47f9a7cb87c12
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -7875,17 +7879,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@changesets/git@npm:^1.4.1":
|
||||
version: 1.4.1
|
||||
resolution: "@changesets/git@npm:1.4.1"
|
||||
"@changesets/git@npm:^1.5.0":
|
||||
version: 1.5.0
|
||||
resolution: "@changesets/git@npm:1.5.0"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.10.4
|
||||
"@changesets/errors": ^0.1.4
|
||||
"@changesets/types": ^5.1.0
|
||||
"@changesets/types": ^5.2.0
|
||||
"@manypkg/get-packages": ^1.1.3
|
||||
is-subdir: ^1.1.1
|
||||
spawndamnit: ^2.0.0
|
||||
checksum: 9b710d532a1816a2540fe4bc4212e13acb102f39f3b49d23396cad9b3fcae8bfe3f838902a9279a5b0b3e4533ca0965ffb169f0bebeb8b4933a48965679ed991
|
||||
checksum: 7208d5bff9c584aa752ca6f647ba5e97356213d00c88cce65c24c6bab1b3837c14f8977c2384a7fecb81e20a0586d4cc1fddb408a38e7dd818ef18377d01ee54
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -7898,42 +7902,42 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@changesets/parse@npm:^0.3.14":
|
||||
version: 0.3.14
|
||||
resolution: "@changesets/parse@npm:0.3.14"
|
||||
"@changesets/parse@npm:^0.3.15":
|
||||
version: 0.3.15
|
||||
resolution: "@changesets/parse@npm:0.3.15"
|
||||
dependencies:
|
||||
"@changesets/types": ^5.1.0
|
||||
"@changesets/types": ^5.2.0
|
||||
js-yaml: ^3.13.1
|
||||
checksum: 4234e93ba3081fc30ec6ed3425cb2f4ac9e69fec0fc90d6d0438d850840c247f7bc9365b78a5295a6084c4cdba1c657c1bc4acd2f666dc0097741f0933111cf7
|
||||
checksum: 1e17f494954140d7885f4be76ac7708c19930f08ecf0b58bcee09160ad6e59223da8899df0709a78e5b5fa419f0d60eccbb34bf0c11d564e24f766c4654e3384
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@changesets/pre@npm:^1.0.12":
|
||||
version: 1.0.12
|
||||
resolution: "@changesets/pre@npm:1.0.12"
|
||||
"@changesets/pre@npm:^1.0.13":
|
||||
version: 1.0.13
|
||||
resolution: "@changesets/pre@npm:1.0.13"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.10.4
|
||||
"@changesets/errors": ^0.1.4
|
||||
"@changesets/types": ^5.1.0
|
||||
"@changesets/types": ^5.2.0
|
||||
"@manypkg/get-packages": ^1.1.3
|
||||
fs-extra: ^7.0.1
|
||||
checksum: 6a81027feaca4727b2809410ec246481a324b4230e0f06b6add52ac2f28f8d7cb9b72cea56b2cfb64a15eb016fb6923bad49c3f2e767ecd2b30b13357a75a0da
|
||||
checksum: f1cc5721546c66977a2fc62428aae9d62f78a04aefac224ac7014172807afa9d07f6da1e326d0a14b1ff12840b0379a7ec24e7984839deb337cd203b63c24b1d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@changesets/read@npm:^0.5.7":
|
||||
version: 0.5.7
|
||||
resolution: "@changesets/read@npm:0.5.7"
|
||||
"@changesets/read@npm:^0.5.8":
|
||||
version: 0.5.8
|
||||
resolution: "@changesets/read@npm:0.5.8"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.10.4
|
||||
"@changesets/git": ^1.4.1
|
||||
"@changesets/git": ^1.5.0
|
||||
"@changesets/logger": ^0.0.5
|
||||
"@changesets/parse": ^0.3.14
|
||||
"@changesets/types": ^5.1.0
|
||||
"@changesets/parse": ^0.3.15
|
||||
"@changesets/types": ^5.2.0
|
||||
chalk: ^2.1.0
|
||||
fs-extra: ^7.0.1
|
||||
p-filter: ^2.1.0
|
||||
checksum: 5c9ea7f4ad1391e971ba7eea4e64fd2c1ad2b00db2884a35ec262fa356fc9c72f813ea1c077c7ce904cd4ce2e97b3599d21116df1c0452f7d240c0af71a34f84
|
||||
checksum: cc32c5a3366be58c5b00988940eb6677898814de7ce60a3c4785d064e0df7563627f8dcfa0620f3ef9cea6f328ce538b2ba7d430c35c883f212b71a94026b2d8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -7944,23 +7948,23 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@changesets/types@npm:^5.1.0":
|
||||
version: 5.1.0
|
||||
resolution: "@changesets/types@npm:5.1.0"
|
||||
checksum: fa903b53d5cc0ce1bd6054fd6c0053af54f604ade2c6436e735a7a66e4f31f7757ec324ceed3355ccd7f0653174fe8b51e1b3850a0b0d80315d13d92a45185b2
|
||||
"@changesets/types@npm:^5.2.0":
|
||||
version: 5.2.0
|
||||
resolution: "@changesets/types@npm:5.2.0"
|
||||
checksum: 579cf8bd2d3a03f293871976d8641531667527f248dc29310a70928d6400cef5df3d09e75beeb2ccf5d384fa1f294f0a2db243c6ebf53913d1f67e283e826f91
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@changesets/write@npm:^0.2.0":
|
||||
version: 0.2.0
|
||||
resolution: "@changesets/write@npm:0.2.0"
|
||||
"@changesets/write@npm:^0.2.1":
|
||||
version: 0.2.1
|
||||
resolution: "@changesets/write@npm:0.2.1"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.10.4
|
||||
"@changesets/types": ^5.1.0
|
||||
"@changesets/types": ^5.2.0
|
||||
fs-extra: ^7.0.1
|
||||
human-id: ^1.0.2
|
||||
prettier: ^2.7.1
|
||||
checksum: 0cc09ddc0669752f6856d3fcc1d609ad7dcd5f464c94501be7b406f2c00013bcec486a422cd09006a2eb0c2bdfe78dd1e44b37202fc7925dea9b855fead8c88b
|
||||
checksum: 98b4d9c12fe13177860407557979d475361076a596103895440f52b2724f7004d6c98af39105fda0eaa52ceca8c0dc0ec9c8ab10eec7a0cb9bf83301f2ca48b3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -8058,13 +8062,13 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"@codemirror/view@npm:^6.0.0":
|
||||
version: 6.3.0
|
||||
resolution: "@codemirror/view@npm:6.3.0"
|
||||
version: 6.4.0
|
||||
resolution: "@codemirror/view@npm:6.4.0"
|
||||
dependencies:
|
||||
"@codemirror/state": ^6.0.0
|
||||
style-mod: ^4.0.0
|
||||
w3c-keyname: ^2.2.4
|
||||
checksum: 8fe39a1c47799b9bca8b815d7fd5c33bcde06cc8b98c26c87729ccca6d5afbc005671c8b00d8678209bcfc079f6894f4e61f26618d8b8133a79befa0828c789f
|
||||
checksum: 57ed7d9d51907f1ea549a2a158872fb7affa6cdff72e29e214f8437b7142ea2a2431ac3c780cbc0187dfff7cb5fd1055a45a887ea9b8c2a09d8430c60ebe9083
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -10271,11 +10275,11 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"@keyv/redis@npm:^2.2.3":
|
||||
version: 2.3.8
|
||||
resolution: "@keyv/redis@npm:2.3.8"
|
||||
version: 2.5.2
|
||||
resolution: "@keyv/redis@npm:2.5.2"
|
||||
dependencies:
|
||||
ioredis: ^5.1.0
|
||||
checksum: 7927d870094161db5bf90677008f6a584235162a0981f875a8616d244d2ba1da525858f656f434aea2a923fb39c5ce0cf88fd9720061e5f025c9db59146e6f48
|
||||
ioredis: ^5.2.3
|
||||
checksum: 2439e5097c6d4bf14e316b7a19d532287d43c5860f0614569f125a3c0a0a8c322610ed26b86b941b9aa9243ebf1e265c1d3f99a43da42d2150d02fc8fd2d94dd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -13771,13 +13775,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/json-buffer@npm:~3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "@types/json-buffer@npm:3.0.0"
|
||||
checksum: 6b0a371dd603f0eec9d00874574bae195382570e832560dadf2193ee0d1062b8e0694bbae9798bc758632361c227b1e3b19e3bd914043b498640470a2da38b77
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/json-schema-merge-allof@npm:^0.6.0":
|
||||
version: 0.6.1
|
||||
resolution: "@types/json-schema-merge-allof@npm:0.6.1"
|
||||
@@ -13827,11 +13824,11 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"@types/keyv@npm:*":
|
||||
version: 3.1.1
|
||||
resolution: "@types/keyv@npm:3.1.1"
|
||||
version: 3.1.4
|
||||
resolution: "@types/keyv@npm:3.1.4"
|
||||
dependencies:
|
||||
"@types/node": "*"
|
||||
checksum: ee0d098693bf4af44be756eed02daf95f5d0fd4b5b02da952a5952e08842baddf6a986a9ea5f9e460729782f1a0a47848c892ad96ea188b66a363feb49a1536f
|
||||
checksum: e009a2bfb50e90ca9b7c6e8f648f8464067271fd99116f881073fa6fa76dc8d0133181dd65e6614d5fb1220d671d67b0124aef7d97dc02d7e342ab143a47779d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -18485,16 +18482,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"compress-brotli@npm:^1.3.8":
|
||||
version: 1.3.8
|
||||
resolution: "compress-brotli@npm:1.3.8"
|
||||
dependencies:
|
||||
"@types/json-buffer": ~3.0.0
|
||||
json-buffer: ~3.0.1
|
||||
checksum: de7589d692d40eb362f6c91070b5e51bc10b05a89eabb4a7c76c1aa21b625756f8c101c6999e4df0c4dc6199c5ca2e1353573bfdcca5615810f27485394162a5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"compress-commons@npm:^4.1.0":
|
||||
version: 4.1.0
|
||||
resolution: "compress-commons@npm:4.1.0"
|
||||
@@ -25131,9 +25118,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ioredis@npm:^5.1.0":
|
||||
version: 5.1.0
|
||||
resolution: "ioredis@npm:5.1.0"
|
||||
"ioredis@npm:^5.2.3":
|
||||
version: 5.2.3
|
||||
resolution: "ioredis@npm:5.2.3"
|
||||
dependencies:
|
||||
"@ioredis/commands": ^1.1.1
|
||||
cluster-key-slot: ^1.1.0
|
||||
@@ -25144,7 +25131,7 @@ __metadata:
|
||||
redis-errors: ^1.2.0
|
||||
redis-parser: ^3.0.0
|
||||
standard-as-callback: ^2.1.0
|
||||
checksum: 7b1c137836ee136a634926df4ec68cc2393772a32ecda8e30dd305d7b9182af02d600456e661f96a6ceada17560ce7b6458948fd09b8adc277a1d287325777dd
|
||||
checksum: 2cb7f0f4217e6774accad3620af1b7114722721c1d1824be2c9f0c2a77ab9629f2e0848d18b1a7208bc37796ae1207cb3e0898fce61900cfe797da0382724ad1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -27038,7 +27025,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"json-buffer@npm:3.0.1, json-buffer@npm:^3.0.1, json-buffer@npm:~3.0.1":
|
||||
"json-buffer@npm:3.0.1, json-buffer@npm:^3.0.1":
|
||||
version: 3.0.1
|
||||
resolution: "json-buffer@npm:3.0.1"
|
||||
checksum: 9026b03edc2847eefa2e37646c579300a1f3a4586cfb62bf857832b60c852042d0d6ae55d1afb8926163fa54c2b01d83ae24705f34990348bdac6273a29d4581
|
||||
@@ -27091,15 +27078,15 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"json-schema-library@npm:^7.0.0":
|
||||
version: 7.0.0
|
||||
resolution: "json-schema-library@npm:7.0.0"
|
||||
version: 7.2.1
|
||||
resolution: "json-schema-library@npm:7.2.1"
|
||||
dependencies:
|
||||
deepmerge: ^4.2.2
|
||||
fast-deep-equal: ^3.1.3
|
||||
gson-pointer: ^4.1.1
|
||||
gson-query: ^5.1.0
|
||||
valid-url: ^1.0.9
|
||||
checksum: 4e6b129b836c84d8080d46a02fef4886ffec8114182ea015a80536f6b8609c5a11354a5e2a98a15b0734bfb25ec60b6a0f1e7230172396d026c679cb385b8f21
|
||||
checksum: 65bc4014cdfe22c4f46d6cb0a0c059d94045128fb96189c4823dff653b6788c2dbab8d5d2e8475bbdc65fd47abf19cedcc3f02df608452c5413fea5d6e675003
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -27553,12 +27540,11 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"keyv@npm:^4.0.0, keyv@npm:^4.0.3":
|
||||
version: 4.3.2
|
||||
resolution: "keyv@npm:4.3.2"
|
||||
version: 4.5.0
|
||||
resolution: "keyv@npm:4.5.0"
|
||||
dependencies:
|
||||
compress-brotli: ^1.3.8
|
||||
json-buffer: 3.0.1
|
||||
checksum: 237952f5faa2ed08da36677d7a3faae48b7e3c305264698cbf4480443f293a2f0c6c63c1d05f5cd4a842ee864dbb395745e6636fecd07489565776a22de7b8d6
|
||||
checksum: d294873cf88ec8f691e5edeb7b4b884f886c5f021a01902a0e243c362449db2b55419d7fb7187d059add747b7398321e39e44d391b65f94935174ce13452714d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user