feat: add AIResource catalog entity kind as opt-in module

Introduces @backstage/plugin-catalog-backend-module-ai-resource-entity-model,
a new backend module that registers the AIResource kind with the catalog.

Signed-off-by: benjdlambert <ben@blam.sh>
This commit is contained in:
benjdlambert
2026-05-15 08:49:01 +02:00
parent 05f13d50c0
commit 3664148c2e
14 changed files with 578 additions and 0 deletions
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend-module-ai-resource-entity-model': minor
---
Introduced the `AIResource` catalog entity kind as an opt-in backend module. Install this module to register support for `AIResource` entities in your catalog, used to represent contextual information consumed by AI coding tools such as skills and rules.
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-plugin-catalog-backend-module-ai-resource-entity-model
title: '@backstage/plugin-catalog-backend-module-ai-resource-entity-model'
description: Adds support for the AIResource entity kind to the catalog backend plugin.
spec:
lifecycle: experimental
type: backstage-backend-plugin-module
owner: maintainers
@@ -0,0 +1,58 @@
{
"name": "@backstage/plugin-catalog-backend-module-ai-resource-entity-model",
"version": "0.1.0",
"description": "Adds support for the AIResource entity kind to the catalog backend plugin.",
"backstage": {
"role": "backend-plugin-module",
"pluginId": "catalog",
"pluginPackage": "@backstage/plugin-catalog-backend"
},
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "plugins/catalog-backend-module-ai-resource-entity-model"
},
"license": "Apache-2.0",
"exports": {
".": "./src/index.ts",
"./alpha": "./src/alpha.ts",
"./package.json": "./package.json"
},
"main": "src/index.ts",
"types": "src/index.ts",
"typesVersions": {
"*": {
"alpha": [
"src/alpha.ts"
],
"package.json": [
"package.json"
]
}
},
"files": [
"dist"
],
"scripts": {
"build": "backstage-cli package build",
"clean": "backstage-cli package clean",
"lint": "backstage-cli package lint",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack",
"start": "backstage-cli package start",
"test": "backstage-cli package test"
},
"dependencies": {
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/catalog-model": "workspace:^",
"@backstage/plugin-catalog-node": "workspace:^",
"@backstage/types": "workspace:^"
},
"devDependencies": {
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^"
}
}
@@ -0,0 +1,12 @@
## API Report File for "@backstage/plugin-catalog-backend-module-ai-resource-entity-model"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { CatalogModelLayer } from '@backstage/catalog-model/alpha';
// @alpha
export const aiResourceEntityModel: CatalogModelLayer;
// (No @packageDocumentation comment for this package)
```
@@ -0,0 +1,36 @@
## API Report File for "@backstage/plugin-catalog-backend-module-ai-resource-entity-model"
> 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 { Entity } from '@backstage/catalog-model';
import { KindValidator } from '@backstage/catalog-model';
// @public
export interface AIResourceEntityV1alpha1 extends Entity {
// (undocumented)
apiVersion: 'backstage.io/v1alpha1';
// (undocumented)
kind: 'AIResource';
// (undocumented)
spec: {
type: string;
lifecycle: string;
owner: string;
system?: string;
};
}
// @public
export const aiResourceEntityV1alpha1Validator: KindValidator;
// @public
const catalogModuleAIResourceEntityModel: BackendFeature;
export default catalogModuleAIResourceEntityModel;
// @public
export const isAIResourceEntity: (
entity: Entity,
) => entity is AIResourceEntityV1alpha1;
```
@@ -0,0 +1,145 @@
/*
* Copyright 2026 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 type { Entity } from '@backstage/catalog-model';
import {
type AIResourceEntityV1alpha1,
aiResourceEntityV1alpha1Validator as validator,
isAIResourceEntity,
} from './AIResourceEntityV1alpha1';
describe('AIResourceV1alpha1Validator', () => {
let entity: AIResourceEntityV1alpha1;
beforeEach(() => {
entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'AIResource',
metadata: {
name: 'frontend-design',
},
spec: {
type: 'skill',
lifecycle: 'production',
owner: 'ai-platform-team',
system: 'ai-tooling',
},
};
});
it('accepts valid data', async () => {
await expect(validator.check(entity)).resolves.toBe(true);
});
it('ignores unknown apiVersion', async () => {
(entity as any).apiVersion = 'backstage.io/v1beta0';
await expect(validator.check(entity)).resolves.toBe(false);
});
it('ignores unknown kind', async () => {
(entity as any).kind = 'Wizard';
await expect(validator.check(entity)).resolves.toBe(false);
});
it('rejects missing type', async () => {
delete (entity as any).spec.type;
await expect(validator.check(entity)).rejects.toThrow(/type/);
});
it('rejects wrong type', async () => {
(entity as any).spec.type = 7;
await expect(validator.check(entity)).rejects.toThrow(/type/);
});
it('rejects empty type', async () => {
(entity as any).spec.type = '';
await expect(validator.check(entity)).rejects.toThrow(/type/);
});
it('rejects missing lifecycle', async () => {
delete (entity as any).spec.lifecycle;
await expect(validator.check(entity)).rejects.toThrow(/lifecycle/);
});
it('rejects wrong lifecycle', async () => {
(entity as any).spec.lifecycle = 7;
await expect(validator.check(entity)).rejects.toThrow(/lifecycle/);
});
it('rejects empty lifecycle', async () => {
(entity as any).spec.lifecycle = '';
await expect(validator.check(entity)).rejects.toThrow(/lifecycle/);
});
it('rejects missing owner', async () => {
delete (entity as any).spec.owner;
await expect(validator.check(entity)).rejects.toThrow(/owner/);
});
it('rejects wrong owner', async () => {
(entity as any).spec.owner = 7;
await expect(validator.check(entity)).rejects.toThrow(/owner/);
});
it('rejects empty owner', async () => {
(entity as any).spec.owner = '';
await expect(validator.check(entity)).rejects.toThrow(/owner/);
});
it('accepts missing system', async () => {
delete (entity as any).spec.system;
await expect(validator.check(entity)).resolves.toBe(true);
});
it('rejects wrong system', async () => {
(entity as any).spec.system = 7;
await expect(validator.check(entity)).rejects.toThrow(/system/);
});
it('rejects empty system', async () => {
(entity as any).spec.system = '';
await expect(validator.check(entity)).rejects.toThrow(/system/);
});
});
describe('isAIResourceEntity', () => {
it('returns true for a valid AIResource entity', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'AIResource',
metadata: { name: 'test' },
};
expect(isAIResourceEntity(entity)).toBe(true);
});
it('returns false for a different kind', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: { name: 'test' },
};
expect(isAIResourceEntity(entity)).toBe(false);
});
it('returns false for a different apiVersion', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1beta1',
kind: 'AIResource',
metadata: { name: 'test' },
};
expect(isAIResourceEntity(entity)).toBe(false);
});
});
@@ -0,0 +1,108 @@
/*
* Copyright 2026 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 {
type Entity,
entityKindSchemaValidator,
type KindValidator,
} from '@backstage/catalog-model';
import { createCatalogModelLayer } from '@backstage/catalog-model/alpha';
import type { JsonObject } from '@backstage/types';
import jsonSchema from './schema/AIResource.v1alpha1.schema.json';
/**
* Backstage catalog AIResource kind Entity. Represents contextual information
* consumed by AI coding tools, such as skills and rules.
*
* @public
*/
export interface AIResourceEntityV1alpha1 extends Entity {
apiVersion: 'backstage.io/v1alpha1';
kind: 'AIResource';
spec: {
type: string;
lifecycle: string;
owner: string;
system?: string;
};
}
const validator = entityKindSchemaValidator(jsonSchema);
/**
* Entity data validator for {@link AIResourceEntityV1alpha1}.
*
* @public
*/
export const aiResourceEntityV1alpha1Validator: KindValidator = {
async check(data: Entity) {
return validator(data) === data;
},
};
/**
* Type guard for {@link AIResourceEntityV1alpha1}.
*
* @public
*/
export const isAIResourceEntity = (
entity: Entity,
): entity is AIResourceEntityV1alpha1 =>
entity.apiVersion === 'backstage.io/v1alpha1' && entity.kind === 'AIResource';
/**
* Extends the catalog model with the AIResource kind.
*
* @alpha
*/
export const aiResourceEntityModel = createCatalogModelLayer({
layerId: 'catalog.backstage.io/kind-ai-resource',
builder: model => {
model.addKind({
group: 'backstage.io',
names: {
kind: 'AIResource',
singular: 'airesource',
plural: 'airesources',
},
description:
'An AI resource represents contextual information consumed by AI coding tools, such as skills and rules.',
versions: [
{
name: 'v1alpha1',
relationFields: [
{
selector: { path: 'spec.owner' },
relation: 'ownedBy',
defaultKind: 'Group',
defaultNamespace: 'inherit',
allowedKinds: ['Group', 'User'],
},
{
selector: { path: 'spec.system' },
relation: 'partOf',
defaultKind: 'System',
defaultNamespace: 'inherit',
},
],
schema: {
jsonSchema: jsonSchema as JsonObject,
},
},
],
});
},
});
@@ -0,0 +1,17 @@
/*
* Copyright 2026 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 { aiResourceEntityModel } from './AIResourceEntityV1alpha1';
@@ -0,0 +1,28 @@
/*
* Copyright 2026 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.
*/
/**
* Adds support for the AIResource entity kind to the catalog backend plugin.
*
* @packageDocumentation
*/
export type { AIResourceEntityV1alpha1 } from './AIResourceEntityV1alpha1';
export {
aiResourceEntityV1alpha1Validator,
isAIResourceEntity,
} from './AIResourceEntityV1alpha1';
export { catalogModuleAIResourceEntityModel as default } from './module';
@@ -0,0 +1,36 @@
/*
* Copyright 2026 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 { startTestBackend } from '@backstage/backend-test-utils';
import { catalogModelExtensionPoint } from '@backstage/plugin-catalog-node/alpha';
import { catalogModuleAIResourceEntityModel } from './module';
describe('catalogModuleAIResourceEntityModel', () => {
it('should register the model source', async () => {
const extensionPoint = {
setFieldValidators: jest.fn(),
setEntityDataParser: jest.fn(),
addModelSource: jest.fn(),
};
await startTestBackend({
extensionPoints: [[catalogModelExtensionPoint, extensionPoint]],
features: [catalogModuleAIResourceEntityModel],
});
expect(extensionPoint.addModelSource).toHaveBeenCalledTimes(1);
});
});
@@ -0,0 +1,42 @@
/*
* Copyright 2026 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 { createBackendModule } from '@backstage/backend-plugin-api';
import { CatalogModelSources } from '@backstage/catalog-model/alpha';
import { catalogModelExtensionPoint } from '@backstage/plugin-catalog-node/alpha';
import { aiResourceEntityModel } from './AIResourceEntityV1alpha1';
/**
* Registers support for the AIResource entity kind in the catalog.
*
* @public
*/
export const catalogModuleAIResourceEntityModel = createBackendModule({
pluginId: 'catalog',
moduleId: 'ai-resource-entity-model',
register(reg) {
reg.registerInit({
deps: {
model: catalogModelExtensionPoint,
},
async init({ model }) {
model.addModelSource(
CatalogModelSources.static([aiResourceEntityModel]),
);
},
});
},
});
@@ -0,0 +1,67 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "AIResourceV1alpha1",
"description": "An AI resource represents contextual information consumed by AI coding tools, such as skills and rules.",
"examples": [
{
"apiVersion": "backstage.io/v1alpha1",
"kind": "AIResource",
"metadata": {
"name": "frontend-design",
"description": "Skill for creating production-grade frontend interfaces"
},
"spec": {
"type": "skill",
"lifecycle": "production",
"owner": "ai-platform-team",
"system": "ai-tooling"
}
}
],
"allOf": [
{
"$ref": "Entity"
},
{
"type": "object",
"required": ["spec"],
"properties": {
"apiVersion": {
"enum": ["backstage.io/v1alpha1"]
},
"kind": {
"enum": ["AIResource"]
},
"spec": {
"type": "object",
"required": ["type", "lifecycle", "owner"],
"properties": {
"type": {
"type": "string",
"description": "The type of AI resource.",
"examples": ["skill", "rule"],
"minLength": 1
},
"lifecycle": {
"type": "string",
"description": "The lifecycle state of the AI resource.",
"examples": ["experimental", "production", "deprecated"],
"minLength": 1
},
"owner": {
"type": "string",
"description": "An entity reference to the owner of the AI resource.",
"examples": ["ai-platform-team", "user:john.johnson"],
"minLength": 1
},
"system": {
"type": "string",
"description": "An entity reference to the system that the AI resource belongs to.",
"minLength": 1
}
}
}
}
}
]
}
+13
View File
@@ -4759,6 +4759,19 @@ __metadata:
languageName: unknown
linkType: soft
"@backstage/plugin-catalog-backend-module-ai-resource-entity-model@workspace:plugins/catalog-backend-module-ai-resource-entity-model":
version: 0.0.0-use.local
resolution: "@backstage/plugin-catalog-backend-module-ai-resource-entity-model@workspace:plugins/catalog-backend-module-ai-resource-entity-model"
dependencies:
"@backstage/backend-plugin-api": "workspace:^"
"@backstage/backend-test-utils": "workspace:^"
"@backstage/catalog-model": "workspace:^"
"@backstage/cli": "workspace:^"
"@backstage/plugin-catalog-node": "workspace:^"
"@backstage/types": "workspace:^"
languageName: unknown
linkType: soft
"@backstage/plugin-catalog-backend-module-aws@workspace:plugins/catalog-backend-module-aws":
version: 0.0.0-use.local
resolution: "@backstage/plugin-catalog-backend-module-aws@workspace:plugins/catalog-backend-module-aws"