feat(catalog-backend-module-azure): add Azure DevOps SCM events bridge wiring

Signed-off-by: Lokesh Kaki <lokeshkaki1@gmail.com>
This commit is contained in:
Lokesh Kaki
2026-03-15 19:08:33 -05:00
parent baa269f33f
commit 0cd53934b0
2 changed files with 142 additions and 1 deletions
@@ -0,0 +1,115 @@
/*
* 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 { LoggerService } from '@backstage/backend-plugin-api';
import { CatalogScmEventsService } from '@backstage/plugin-catalog-node/alpha';
import { EventParams, EventsService } from '@backstage/plugin-events-node';
import { analyzeAzureDevOpsWebhookEvent } from './analyzeAzureDevOpsWebhookEvent';
/**
* Takes Azure DevOps webhook events, analyzes them, and publishes them as
* catalog SCM events that entity providers and others can subscribe to.
*/
export class AzureDevOpsScmEventsBridge {
readonly #logger: LoggerService;
readonly #events: EventsService;
readonly #catalogScmEvents: CatalogScmEventsService;
#shuttingDown: boolean;
#pendingPublish: Promise<void> | undefined;
constructor(options: {
logger: LoggerService;
events: EventsService;
catalogScmEvents: CatalogScmEventsService;
}) {
this.#logger = options.logger;
this.#events = options.events;
this.#catalogScmEvents = options.catalogScmEvents;
this.#shuttingDown = false;
}
async start() {
await this.#events.subscribe({
id: 'catalog-azure-devops-scm-events-bridge',
topics: ['azureDevOps'],
onEvent: this.#onEvent.bind(this),
});
}
async stop() {
this.#shuttingDown = true;
await this.#pendingPublish;
}
async #onEvent(params: EventParams): Promise<void> {
const eventPayload = params.eventPayload as
| { eventType?: string }
| undefined;
const eventType = eventPayload?.eventType;
if (!eventType || !eventPayload) {
return;
}
while (this.#pendingPublish) {
await this.#pendingPublish;
}
if (this.#shuttingDown) {
this.#logger.warn(
`Skipping Azure DevOps webhook event of type "${eventType}" on topic "${params.topic}" because the bridge is shutting down`,
);
return;
}
this.#pendingPublish = Promise.resolve().then(async () => {
try {
const output = await analyzeAzureDevOpsWebhookEvent(
eventType,
eventPayload,
{
isRelevantPath: path =>
path.endsWith('.yaml') || path.endsWith('.yml'),
},
);
if (output.result === 'ok') {
await this.#catalogScmEvents.publish(output.events);
} else if (output.result === 'ignored') {
this.#logger.debug(
`Skipping Azure DevOps webhook event of type "${eventType}" on topic "${params.topic}" because it is ignored: ${output.reason}`,
);
} else if (output.result === 'aborted') {
this.#logger.warn(
`Skipping Azure DevOps webhook event of type "${eventType}" on topic "${params.topic}" because it is aborted: ${output.reason}`,
);
} else if (output.result === 'unsupported-event') {
this.#logger.debug(
`Skipping Azure DevOps webhook event of type "${eventType}" on topic "${params.topic}" because it is unsupported: ${output.event}`,
);
}
} catch (error) {
this.#logger.warn(
`Failed to handle Azure DevOps webhook event of type "${eventType}"`,
error,
);
} finally {
this.#pendingPublish = undefined;
}
});
await this.#pendingPublish;
}
}
@@ -19,10 +19,13 @@ import {
createBackendModule,
} from '@backstage/backend-plugin-api';
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node';
import { catalogScmEventsServiceRef } from '@backstage/plugin-catalog-node/alpha';
import { eventsServiceRef } from '@backstage/plugin-events-node';
import {
AzureBlobStorageEntityProvider,
AzureDevOpsEntityProvider,
} from '../providers';
import { AzureDevOpsScmEventsBridge } from '../events/AzureDevOpsScmEventsBridge';
/**
* Registers the AzureDevOpsEntityProvider with the catalog processing extension point.
@@ -39,8 +42,19 @@ export const catalogModuleAzureEntityProvider = createBackendModule({
catalog: catalogProcessingExtensionPoint,
logger: coreServices.logger,
scheduler: coreServices.scheduler,
events: eventsServiceRef,
catalogScmEvents: catalogScmEventsServiceRef,
lifecycle: coreServices.lifecycle,
},
async init({ config, catalog, logger, scheduler }) {
async init({
config,
catalog,
logger,
scheduler,
events,
catalogScmEvents,
lifecycle,
}) {
// Check for Azure Blob Storage provider configuration and register it
if (config.has('catalog.providers.azureBlob')) {
catalog.addEntityProvider(
@@ -60,6 +74,18 @@ export const catalogModuleAzureEntityProvider = createBackendModule({
}),
);
}
const bridge = new AzureDevOpsScmEventsBridge({
logger,
events,
catalogScmEvents,
});
lifecycle.addStartupHook(async () => {
await bridge.start();
});
lifecycle.addShutdownHook(async () => {
await bridge.stop();
});
},
});
},