From 1764b17fb0bd8376e0d2843a864cf06f7d2541a8 Mon Sep 17 00:00:00 2001 From: Daniel Hagen <5039466+dbhagen@users.noreply.github.com> Date: Thu, 7 Nov 2024 20:43:10 -0600 Subject: [PATCH] feat: add autogov processor, testing simplification --- .../.eslintrc.js | 1 + .../README.md | 5 + .../package.json | 55 ++ .../src/index.ts | 8 + .../src/module.ts | 28 + .../src/processor/AutogovProcessor.ts | 660 ++++++++++++++++++ .../src/processor/index.ts | 5 + .../tsconfig.json | 20 + lerna.json | 5 + package.json | 59 +- tsconfig.json | 3 +- yarn.lock | 53 +- 12 files changed, 889 insertions(+), 13 deletions(-) create mode 100644 backstage-plugin-module-autogov-processor/.eslintrc.js create mode 100644 backstage-plugin-module-autogov-processor/README.md create mode 100644 backstage-plugin-module-autogov-processor/package.json create mode 100644 backstage-plugin-module-autogov-processor/src/index.ts create mode 100644 backstage-plugin-module-autogov-processor/src/module.ts create mode 100644 backstage-plugin-module-autogov-processor/src/processor/AutogovProcessor.ts create mode 100644 backstage-plugin-module-autogov-processor/src/processor/index.ts create mode 100644 backstage-plugin-module-autogov-processor/tsconfig.json create mode 100644 lerna.json diff --git a/backstage-plugin-module-autogov-processor/.eslintrc.js b/backstage-plugin-module-autogov-processor/.eslintrc.js new file mode 100644 index 0000000..e2a53a6 --- /dev/null +++ b/backstage-plugin-module-autogov-processor/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/backstage-plugin-module-autogov-processor/README.md b/backstage-plugin-module-autogov-processor/README.md new file mode 100644 index 0000000..5738cd0 --- /dev/null +++ b/backstage-plugin-module-autogov-processor/README.md @@ -0,0 +1,5 @@ +# @internal/backstage-plugin-catalog-backend-module-autogov-processor + +The autogov-processor backend module for the catalog plugin. + +_This plugin was created through the Backstage CLI_ diff --git a/backstage-plugin-module-autogov-processor/package.json b/backstage-plugin-module-autogov-processor/package.json new file mode 100644 index 0000000..b15e671 --- /dev/null +++ b/backstage-plugin-module-autogov-processor/package.json @@ -0,0 +1,55 @@ +{ + "name": "@liatrio/backstage-plugin-module-autogov-processor", + "description": "The autogov-processor backend module for the catalog plugin.", + "version": "1.2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/liatrio/backstage-github-autogov-plugin.git", + "directory": "backstage-plugin-autogov-common" + }, + "backstage": { + "role": "backend-plugin-module", + "pluginId": "autogov-processor", + "pluginPackages": [] + }, + "publishConfig": { + "registry": "https://npm.pkg.github.com/", + "access": "public" + }, + "sideEffects": false, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "lint:all": "yarn lint && yarn prettier:check", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "prettier:check": "npx --yes prettier --check .", + "prettier:fix": "npx --yes prettier --write .", + "tsc:full": "tsc --skipLibCheck true --incremental false", + "prepare": "husky" + }, + "dependencies": { + "@backstage/backend-plugin-api": "^1.0.0", + "@backstage/catalog-model": "^1.7.0", + "@backstage/config": "^1.2.0", + "@backstage/integration": "^1.15.1", + "@backstage/plugin-catalog-node": "^1.13.1", + "@backstage/types": "^1.1.1", + "@liatrio/backstage-plugin-autogov-common": "^v1.2.4", + "node-fetch": "2" + }, + "devDependencies": { + "@backstage/backend-test-utils": "^1.0.0", + "@backstage/cli": "^0.27.1", + "@types/node-fetch": "2" + }, + "files": [ + "dist" + ] +} diff --git a/backstage-plugin-module-autogov-processor/src/index.ts b/backstage-plugin-module-autogov-processor/src/index.ts new file mode 100644 index 0000000..0fda5cf --- /dev/null +++ b/backstage-plugin-module-autogov-processor/src/index.ts @@ -0,0 +1,8 @@ +/***/ +/** + * The autogov-processor backend module for the catalog plugin. + * + * @packageDocumentation + */ + +export { catalogModuleAutogovProcessor as default } from './module'; diff --git a/backstage-plugin-module-autogov-processor/src/module.ts b/backstage-plugin-module-autogov-processor/src/module.ts new file mode 100644 index 0000000..26b1d1a --- /dev/null +++ b/backstage-plugin-module-autogov-processor/src/module.ts @@ -0,0 +1,28 @@ +import { + coreServices, + createBackendModule, +} from '@backstage/backend-plugin-api'; +import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; +import { AutogovProcessor } from './processor'; + +export const catalogModuleAutogovProcessor = createBackendModule({ + pluginId: 'catalog', + moduleId: 'autogov-processor', + register(reg) { + reg.registerInit({ + deps: { + catalog: catalogProcessingExtensionPoint, + config: coreServices.rootConfig, + logger: coreServices.logger, + }, + async init({ catalog, config, logger }) { + logger.info('Initializing Autogov Processor'); + catalog.addProcessor( + AutogovProcessor.fromConfig(config, { + logger, + }), + ); + }, + }); + }, +}); diff --git a/backstage-plugin-module-autogov-processor/src/processor/AutogovProcessor.ts b/backstage-plugin-module-autogov-processor/src/processor/AutogovProcessor.ts new file mode 100644 index 0000000..dc9b6fb --- /dev/null +++ b/backstage-plugin-module-autogov-processor/src/processor/AutogovProcessor.ts @@ -0,0 +1,660 @@ +/** + * @file AutoGovProcessor.ts + * @description Processes entities to check their autogov status from release assets + * + * @author Daniel Hagen + * @author Amber Beasley + * @copyright 2024 Liatrio, Inc. + */ + +import { LoggerService } from '@backstage/backend-plugin-api'; +import { Config } from '@backstage/config'; +import { + Entity, + getEntitySourceLocation, + stringifyEntityRef, +} from '@backstage/catalog-model'; +import { + CatalogProcessor, + CatalogProcessorCache, +} from '@backstage/plugin-catalog-node'; +import { + GithubIntegration, + ScmIntegrationRegistry, + ScmIntegrations, +} from '@backstage/integration'; +import { durationToMilliseconds, HumanDuration } from '@backstage/types'; +import fetch from 'node-fetch'; + +import { + AUTOGOV_STATUS_FILE_ANNOTATION, + AUTOGOV_STATUS_ANNOTATION, + AUTOGOV_STATUSES, +} from '@liatrio/backstage-plugin-autogov-common'; + +export type ShouldProcessEntity = (entity: Entity) => boolean; + +interface CachedData { + [key: string]: number | string; + autogovStatus: string; + cachedTime: number; +} + +interface AutogovProcessorOptionsResultsFile { + allowOverride?: boolean; + default?: string; +} + +export interface AutogovProcessorOptions { + logger: LoggerService; + + cacheTTL?: HumanDuration; + resultsFile?: AutogovProcessorOptionsResultsFile; + requireAnnotation?: boolean; + entityKinds?: string[]; + entityTypes?: string[]; + + scmIntegrations?: ScmIntegrationRegistry; +} + +/** + * Represents a release which can either be an object containing release details or undefined. + * + * @typedef {Object} Release + * @property {string} url - The URL of the release. + * @property {number} id - The unique identifier of the release. + * @property {Array} assets - An array of assets associated with the release. + * @property {string} assets[].url - The URL of the asset. + * @property {string} assets[].browser_download_url - The browser download URL of the asset. + * @property {number} assets[].id - The unique identifier of the asset. + */ +type Release = + | { + url: string; + id: number; + assets: Array<{ + url: string; + browser_download_url: string; + id: number; + }>; + } + | undefined; + +/** + * Processor that checks git repositories for autogov status files in their latest release. + * + * This processor will: + * - Look for entities matching configured kinds and types + * - Check if they have GitHub source locations + * - Fetch the latest release from their GitHub repository + * - Look for a results file in the release assets + * - Parse the results file to determine autogov status + * - Cache results for configured TTL + * - Add autogov status as an annotation on the entity + * + * @implements {CatalogProcessor} + * @class + */ + +/** + * Manages caching and fetching of autogov statuses from GitHub repositories. + * Processes entities through the catalog to add autogov status annotations. + * + * @property {LoggerService} logger - Logger service for the processor + * @property {ScmIntegrationRegistry} scmIntegrations - Registry of SCM integrations + * @property {AutogovProcessorOptionsResultsFile} resultsFile - Configuration for results file handling + * @property {boolean} requireAnnotation - Whether to require autogov annotation + * @property {string[]} entityKinds - Array of entity kinds to process + * @property {string[]} entityTypes - Array of entity types to process + * @property {Object} loggerMeta - Metadata for logging + * @property {number} cacheTTLMilliseconds - Cache TTL in milliseconds + */ +export class AutogovProcessor implements CatalogProcessor { + private readonly logger: LoggerService; + private readonly scmIntegrations: ScmIntegrationRegistry | undefined; + private readonly resultsFile: AutogovProcessorOptionsResultsFile; + private readonly requireAnnotation: boolean; + private readonly entityKinds: string[]; + private readonly entityTypes: string[]; + private readonly loggerMeta = { plugins: 'AutogovProcessor' }; + private cacheTTLMilliseconds: number; + + /** + * Determines if a given entity should be processed based on its kind and type. + * + * @param entity - The entity to evaluate for processing + * @returns boolean indicating whether the entity should be processed + * + * The method uses the following logic: + * 1. If entityTypes array has entries: + * - Both entity kind and spec.type must match configured kinds/types + * - Returns false if spec.type is undefined + * 2. If entityTypes array is empty: + * - Only checks if entity kind matches configured kinds + */ + private shouldProcessEntity: ShouldProcessEntity = (entity: Entity) => { + const entityKind = entity.kind.toLowerCase(); + const entitySpecType = + typeof entity.spec?.type === 'string' + ? entity.spec?.type?.toLowerCase() + : undefined; + this.logger.debug( + `Checking if entity ${stringifyEntityRef( + entity, + )} should be processed, kind: ${entityKind}, from: ${JSON.stringify( + this.entityKinds, + )}, type: ${entitySpecType}, from ${JSON.stringify(this.entityTypes)}`, + { + ...this.loggerMeta, + }, + ); + this.logger.debug( + `Entity Types Length ${ + this.entityTypes.length + }, kind match ${this.entityKinds.includes( + entityKind, + )}, type match: ${this.entityTypes.includes( + entitySpecType || 'undefined', + )}`, + { ...this.loggerMeta }, + ); + if (this.entityTypes.length > 0) { + if (entitySpecType) { + return ( + this.entityKinds.includes(entityKind) && + this.entityTypes.includes(entitySpecType) + ); + } + return false; + } + return this.entityTypes.includes(entityKind); + }; + + /** + * Returns the name of the processor. + * @returns {string} The name of the processor as 'github-autogov-processor' + */ + getProcessorName(): string { + return 'github-autogov-processor'; + } + + /** + * Creates a new instance of AutogovProcessor. + * @param options - Configuration options for the GitHub Autogov Processor + * @param options.logger - Logger instance for recording processor activities + * @param options.scmIntegrations - Source Control Management integrations configuration + * @param options.resultsFile - Configuration for results file location and override settings + * @param options.resultsFile.allowOverride - Whether to allow override of default results file location + * @param options.resultsFile.default - Default name for the results file + * @param options.requireAnnotation - Whether to require annotations for processing (defaults to true) + * @param options.entityKinds - Array of entity kinds to process (defaults to ['component']) + * @param options.entityTypes - Array of entity types to process (defaults to ['website']) + * @param options.cacheTTL - Cache time-to-live duration (defaults to 30 minutes) + */ + constructor(options: AutogovProcessorOptions) { + this.logger = options.logger; + this.logger.info(`Autogov Processor initialized`, { + ...this.loggerMeta, + }); + this.scmIntegrations = options.scmIntegrations; + this.logger.debug( + `Autogov Processor SCM Integrations: ${JSON.stringify( + options.scmIntegrations, + )}`, + { ...this.loggerMeta }, + ); + + this.resultsFile = { + allowOverride: options.resultsFile?.allowOverride ?? false, + default: options.resultsFile?.default ?? 'results', + }; + this.logger.debug( + `Autogov Processor results file set to ${JSON.stringify( + this.resultsFile, + )}`, + { ...this.loggerMeta }, + ); + + this.requireAnnotation = options.requireAnnotation ?? true; + this.logger.debug( + `Autogov Processor require annotation set to ${this.requireAnnotation}`, + { ...this.loggerMeta }, + ); + + this.entityKinds = options.entityKinds ?? ['component']; + this.logger.debug( + `Autogov Processor entity kinds set to ${this.entityKinds}`, + { ...this.loggerMeta }, + ); + + this.entityTypes = options.entityTypes ?? ['website']; + this.logger.debug( + `Autogov Processor entity types set to ${this.entityTypes}`, + { ...this.loggerMeta }, + ); + + this.cacheTTLMilliseconds = durationToMilliseconds( + options.cacheTTL || { minutes: 30 }, + ); + this.logger.debug( + `Autogov Processor Cache TTL set to ${this.cacheTTLMilliseconds}ms`, + { + ...this.loggerMeta, + }, + ); + } + + /** + * Creates a new instance of AutogovProcessor from a Config object. + * + * @param config - The application configuration object + * @param [options] - Configuration options for the Autogov processor + * @returns A new instance of AutogovProcessor configured with the provided options + * + * @remarks + * This method initializes a AutogovProcessor by: + * - Reading optional autogov configuration section + * - Setting up cache TTL, results file path, and annotation requirements + * - Configuring entity kinds and types filters + * - Integrating SCM configuration + * + * @example + * ```ts + * const processor = AutogovProcessor.fromConfig(config, { + * cacheTTL: 3600, + * requireAnnotation: true + * }); + * ``` + */ + static fromConfig( + config: Config, + options: AutogovProcessorOptions, + ): AutogovProcessor { + const c = config.getOptionalConfig('autogov'); + const githubConfig = c?.getOptionalConfig('github'); + const resultsFileConfig = githubConfig?.getOptionalConfig('resultsFile'); + if (githubConfig) { + options.cacheTTL = githubConfig.getOptional('cacheTTL'); + options.resultsFile = { + allowOverride: githubConfig.getOptional('resultsFile'), + default: resultsFileConfig?.getOptional('default'), + }; + options.requireAnnotation = + githubConfig.getOptionalBoolean('requireAnnotation'); + options.entityKinds = githubConfig + .getOptionalStringArray('entityKinds') + ?.map(v => v.toLowerCase()); + options.entityTypes = githubConfig + .getOptionalStringArray('entityTypes') + ?.map(v => v.toLowerCase()); + } + const scmIntegrations = ScmIntegrations.fromConfig(config); + if (scmIntegrations) { + options.scmIntegrations = scmIntegrations; + } + return new AutogovProcessor(options); + } + + /** + * Retrieves autogov data from GitHub API for a given entity using the specified GitHub integration. + * The method fetches the latest release from the repository and looks for a results file within its assets. + * + * @param entity - The entity containing metadata and annotations for processing + * @param integration - GitHub integration configuration containing token and API base URL + * @returns Promise - Returns the parsed result from the autogov data file, or an error/N/A status + * + * @throws Will not throw errors directly, but returns error status strings in case of failures + * + * @remarks + * The method performs the following steps: + * 1. Validates input entity and required configurations + * 2. Determines the results file name (using default or override from annotations) + * 3. Fetches the latest release from the repository + * 4. Locates and downloads the results file from release assets + * 5. Parses and returns the result content + * + * @example + * const result = await getAutogovDataFromGithubAPI(entity, githubIntegration); + * // Returns: "SUCCESS", "ERROR", "N_A", or other status strings + */ + private async getAutogovDataFromGithubAPI( + entity: Entity, + integration: GithubIntegration, + ): Promise { + if (!entity || !entity.metadata?.annotations) { + this.logger.error(`Entity input incorrect`, { + ...this.loggerMeta, + }); + return AUTOGOV_STATUSES.ERROR; + } + + // Get results file source + let resultsFile = this.resultsFile.default; + if (this.resultsFile.allowOverride) { + if (entity.metadata.annotations[AUTOGOV_STATUS_FILE_ANNOTATION]) { + resultsFile = + entity.metadata.annotations[AUTOGOV_STATUS_FILE_ANNOTATION]; + this.logger.debug( + `Overriding results file with annotation value: ${resultsFile}`, + { + ...this.loggerMeta, + }, + ); + } + } + this.logger.debug(`Using results file: ${resultsFile}`, { + ...this.loggerMeta, + }); + + // Check for required config values + const { token, apiBaseUrl } = integration.config; + if (!token || !apiBaseUrl) { + this.logger.error(`Github Integration missing token or apiBaseUrl`, { + ...this.loggerMeta, + }); + return AUTOGOV_STATUSES.ERROR; + } + + // Get project slug from entity annotations + const projectSlug = + entity?.metadata?.annotations['github.com/project-slug']; + if (!projectSlug) { + this.logger.error(`No project slug found in entity annotations`, { + ...this.loggerMeta, + }); + return AUTOGOV_STATUSES.ERROR; + } + + // Get latest release + const latestReleaseResponse = await fetch( + `${apiBaseUrl}/repos/${projectSlug}/releases/latest`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + }, + ); + if (!latestReleaseResponse) { + this.logger.error(`Error fetching latest release`, { + ...this.loggerMeta, + }); + return AUTOGOV_STATUSES.ERROR; + } + + let latestRelease: Release = undefined; + try { + latestRelease = await latestReleaseResponse.json(); + } catch (error) { + this.logger.error(`Error parsing latest release`, { + ...this.loggerMeta, + }); + return AUTOGOV_STATUSES.ERROR; + } + + if (latestRelease && latestRelease?.assets.length <= 0) { + this.logger.debug(`No assets found in latest release`, { + ...this.loggerMeta, + }); + return AUTOGOV_STATUSES.N_A; + } + + const result = latestRelease?.assets.find((asset: any) => { + return asset.name === resultsFile; + }); + if (!result) { + this.logger.debug(`No results asset found`, { ...this.loggerMeta }); + return AUTOGOV_STATUSES.N_A; + } + + const resultsFileContentResponse = await fetch(result.url, { + headers: { + Accept: 'application/octet-stream', + Authorization: `Bearer ${token}`, + }, + }); + if (!resultsFileContentResponse) { + this.logger.error(`Error fetching results file content`, { + ...this.loggerMeta, + }); + return AUTOGOV_STATUSES.ERROR; + } + + const content = await resultsFileContentResponse.text(); + try { + const parsedResultsFileContent = JSON.parse(content); + return parsedResultsFileContent.result; + } catch (error) { + this.logger.error(`Error parsing results file content`, { + ...this.loggerMeta, + }); + return AUTOGOV_STATUSES.ERROR; + } + } + + /** + * Retrieves cached GitHub releases data for a given entity, or fetches new data if cache is expired/empty. + * + * @param entity - The entity to fetch releases data for + * @param integration - GitHub integration instance used to make API calls + * @param cache - Cache instance to store/retrieve release data + * @returns Promise containing the autogov status string + * + * @remarks + * The method follows this flow: + * 1. Attempts to retrieve cached data for the entity + * 2. Check if the last result was an error, and if so, fetch fresh data + * 3. If cache is missing/expired, fetches fresh data from GitHub API + * 4. Updates cache with new data if needed + * 5. Logs debug information about cache status + * + * @private + */ + private async getCachedReleases( + entity: Entity, + integration: GithubIntegration, + cache: CatalogProcessorCache, + ): Promise { + let cachedData = (await cache.get(stringifyEntityRef(entity))) as + | CachedData + | undefined; + if (cachedData && cachedData.autogovStatus === AUTOGOV_STATUSES.ERROR) { + cachedData = undefined; + } + if (!cachedData || this.isExpired(cachedData)) { + const autogovStatus = await this.getAutogovDataFromGithubAPI( + entity, + integration, + ); + cachedData = { + autogovStatus: autogovStatus, + cachedTime: Date.now(), + }; + await cache.set(stringifyEntityRef(entity), cachedData); + } + const cacheTimeRemaining = + this.cacheTTLMilliseconds - (Date.now() - (cachedData.cachedTime || 0)); + this.logger.debug( + `Fetched cached autogovStatus for ${stringifyEntityRef( + entity, + )} cached on ${ + cachedData.cachedTime + }, expires in ${cacheTimeRemaining}ms`, + { + ...this.loggerMeta, + entity, + }, + ); + return cachedData.autogovStatus; + } + + /** + * Determines if the cached data has exceeded its time-to-live (TTL). + * + * @param cachedData - The cached data object to check for expiration + * @returns True if the cached data has expired, false otherwise + * + * @private + */ + private isExpired(cachedData: CachedData): boolean { + const elapsed = Date.now() - (cachedData.cachedTime || 0); + return elapsed > this.cacheTTLMilliseconds; + } + + /** + * Processes an entity before it is added to the catalog, checking for autogov status. + * + * This method performs several validation steps: + * 1. Verifies the entity should be processed based on entity type + * 2. Checks for SCM integration availability + * 3. Validates entity has a URL source location + * 4. Checks for required autogov annotation if enabled + * 5. Verifies the URL is a Github URL + * 6. Retrieves and processes autogov status data + * + * @param entity - The entity to process + * @param _ - Unused parameter + * @param __ - Unused parameter + * @param ___ - Unused parameter + * @param cache - Cache to store/retrieve autogov data + * + * @returns The processed entity with autogov status information added + * + * @throws Error if there is an issue retrieving or processing autogov data + */ + async preProcessEntity( + entity: Entity, + _: any, + __: any, + ___: any, + cache: CatalogProcessorCache, + ): Promise { + const entityRef = stringifyEntityRef(entity); + + // Skip entities that are not in the entityType list + if (!this.shouldProcessEntity(entity)) { + this.logger.debug( + `Skipping entity ${entityRef} because not in entityType list`, + { + ...this.loggerMeta, + }, + ); + return entity; + } + + // Skip entities that don't have a source location + if (!this.scmIntegrations) { + this.logger.warn( + `No SCM Integrations available, skipping entity ${entityRef}`, + { ...this.loggerMeta }, + ); + return entity; + } + + // Skip entities that don't have a URL source location + const entitySourceLocation = getEntitySourceLocation(entity); + if (entitySourceLocation?.type !== 'url') { + this.logger.debug(`Skipping entity ${entityRef} because it's not a URL`, { + ...this.loggerMeta, + }); + return entity; + } + + // If requireAnnotation is true, skip entities that don't have the autogov annotation + if ( + this.requireAnnotation && + !entity.metadata?.annotations?.[AUTOGOV_STATUS_FILE_ANNOTATION] + ) { + this.logger.info( + `Skipping entity ${entityRef} because it's missing the autogov annotation`, + { ...this.loggerMeta, entityRef }, + ); + return entity; + } + + this.logger.info(`Processing autogov entity ${entityRef}`, { + ...this.loggerMeta, + }); + + // Skip entities that are not Github URLs + const detectedIntegration = this.scmIntegrations.byUrl( + entitySourceLocation.target, + ); + if (detectedIntegration?.type !== 'github') { + this.logger.debug( + `Skipping entity ${entityRef} because not a Github URL`, + { ...this.loggerMeta }, + ); + return entity; + } + + let autogovStatus = + this.cacheTTLMilliseconds > 0 + ? await this.getCachedReleases( + entity, + detectedIntegration as GithubIntegration, + cache, + ) + : await this.getAutogovDataFromGithubAPI( + entity, + detectedIntegration as GithubIntegration, + ); + + // If requireAnnotation is false, set autogovStatus to N/A if it's an error + if (!this.requireAnnotation && autogovStatus === AUTOGOV_STATUSES.ERROR) { + this.logger.debug( + `Setting autogovStatus to ${autogovStatus} for entity ${entityRef}`, + { + ...this.loggerMeta, + entityRef, + }, + ); + autogovStatus = AUTOGOV_STATUSES.N_A; + } + + this.logger.info( + `Found autogovStatus ${autogovStatus} releases for ${entityRef}`, + { + ...this.loggerMeta, + entityRef, + }, + ); + + if (autogovStatus === AUTOGOV_STATUSES.ERROR) { + this.logger.error(`Error processing entity ${entityRef}`, { + ...this.loggerMeta, + entityRef, + }); + return entity; + } + + this.addAutogovStatusToEntity(entity, autogovStatus); + + return entity; + } + + /** + * Adds or updates the autogov status annotation on an entity. + * + * @param entity - The entity to update with autogov status annotation + * @param autogovStatus - The status string to set (PASSED, FAILED, N/A, ERROR) + * @private + * + * @remarks + * This method mutates the entity object by adding or updating the + * 'liatrio.com/autogov-latest-release-status' annotation with + * the provided status value. + * + * If the entity or entity.metadata is undefined, no changes are made. + */ + private addAutogovStatusToEntity( + entity: Entity, + autogovStatus: string, + ): void { + if (entity.metadata) { + const annotations = entity.metadata?.annotations || {}; + entity.metadata.annotations = annotations; + annotations[AUTOGOV_STATUS_ANNOTATION] = autogovStatus; + } + } +} diff --git a/backstage-plugin-module-autogov-processor/src/processor/index.ts b/backstage-plugin-module-autogov-processor/src/processor/index.ts new file mode 100644 index 0000000..176d443 --- /dev/null +++ b/backstage-plugin-module-autogov-processor/src/processor/index.ts @@ -0,0 +1,5 @@ +export type { + AutogovProcessorOptions, + ShouldProcessEntity, +} from './AutogovProcessor'; +export { AutogovProcessor } from './AutogovProcessor'; diff --git a/backstage-plugin-module-autogov-processor/tsconfig.json b/backstage-plugin-module-autogov-processor/tsconfig.json new file mode 100644 index 0000000..787bf90 --- /dev/null +++ b/backstage-plugin-module-autogov-processor/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "@backstage/cli/config/tsconfig.json", + "compilerOptions": { + "outDir": "dist", // Compiled JS files go to 'dist' + "rootDir": ".", // Use the root of the plugin folder to ensure the 'src' folder is preserved in output + "declaration": true, // Generate declaration files + "declarationDir": "../dist-types/backstage-plugin-module-autogov-processor", // Output declaration files to correct path + "declarationMap": true, + "sourceMap": true, + "module": "commonjs", + "target": "es6", + "strict": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/lerna.json b/lerna.json new file mode 100644 index 0000000..847a514 --- /dev/null +++ b/lerna.json @@ -0,0 +1,5 @@ +{ + "$schema": "node_modules/lerna/schemas/lerna-schema.json", + "version": "0.0.0", + "npmClient": "yarn" +} diff --git a/package.json b/package.json index 184e312..eb1ee8f 100644 --- a/package.json +++ b/package.json @@ -30,13 +30,66 @@ "build": "backstage-cli repo build --all", "lint": "backstage-cli repo lint", "lint:all": "yarn lint && yarn prettier:check", - "install:all:local": "yarn --cwd backstage-plugin-autogov-common install --immutable && yarn --cwd backstage-plugin-github-releases-assets-backend install --immutable && yarn --cwd backstage-plugin-github-releases-autogov install --immutable", - "test:all": "yarn install:all:local && yarn tsc:full && yarn --cwd backstage-plugin-autogov-common test --all --no-watch && yarn --cwd backstage-plugin-github-releases-assets-backend test --all --no-watch && yarn --cwd backstage-plugin-github-releases-autogov test --all --no-watch", + "install:all:local": "yarn --cwd backstage-plugin-autogov-common install --immutable && yarn --cwd backstage-plugin-github-releases-assets-backend install --immutable && yarn --cwd backstage-plugin-github-releases-autogov install --immutable && yarn --cwd backstage-plugin-module-autogov-processor install --immutable", + "test:all": "yarn install:all:local && yarn tsc:full && CI=true lerna run --scope '@liatrio/*' test", "prettier:check": "npx --yes prettier --check .", "prettier:fix": "npx --yes prettier --write .", "tsc:full": "tsc --skipLibCheck true --incremental false", "prepare": "husky", "clean": "backstage-cli repo clean" }, - "packageManager": "yarn@4.4.1" + "packageManager": "yarn@4.4.1", + "configSchema": { + "$schema": "https://backstage.io/schema/config-v1", + "title": "@liatrio/backend-module-autogov-processor", + "type": "object", + "properties": { + "github": { + "type": "object", + "description": "GitHub configuration", + "properties": { + "resultsFile": { + "type": "object", + "description": "Autogov results file configuration", + "properties": { + "allowOverride": { + "type": "boolean", + "description": "Whether to allow override of default results file location", + "default": false + }, + "default": { + "type": "string", + "description": "Default name for the results file", + "default": "results" + } + } + }, + "requireAnnotation": { + "type": "boolean", + "description": "Whether to require annotations for processing", + "default": true + }, + "entityKinds": { + "type": "array", + "description": "Array of entity kinds to process", + "default": [ + "component" + ] + }, + "entityTypes": { + "type": "array", + "description": "Array of entity types to process", + "default": [ + "website" + ] + }, + "maxReleasesResults": { + "type": "number", + "description": "Maximum number of releases to pull from GitHub and show", + "default": 5 + } + } + } + } + } } diff --git a/tsconfig.json b/tsconfig.json index bee9d2e..73d5724 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,8 @@ "include": [ "backstage-plugin-autogov-common/src", "backstage-plugin-github-releases-assets-backend/src", - "backstage-plugin-github-releases-autogov/src" + "backstage-plugin-github-releases-autogov/src", + "backstage-plugin-module-autogov-processor/src" ], "exclude": ["node_modules"], "compilerOptions": { diff --git a/yarn.lock b/yarn.lock index 6b9f581..c4a3a5d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5244,6 +5244,23 @@ __metadata: languageName: unknown linkType: soft +"@liatrio/backstage-plugin-autogov-common@npm:^v1.2.4": + version: 1.2.4 + resolution: "@liatrio/backstage-plugin-autogov-common@npm:1.2.4::__archiveUrl=https%3A%2F%2Fnpm.pkg.github.com%2Fdownload%2F%40liatrio%2Fbackstage-plugin-autogov-common%2F1.2.4%2F0e65dbc6122dbad8cde581a20d8f1feb3092bbdf" + dependencies: + "@backstage/core-components": "npm:^0.15.0" + "@backstage/core-plugin-api": "npm:^1.9.4" + "@backstage/theme": "npm:^0.5.7" + "@material-ui/core": "npm:^4.9.13" + "@material-ui/icons": "npm:^4.9.1" + "@material-ui/lab": "npm:^4.0.0-alpha.61" + react-use: "npm:^17.2.4" + peerDependencies: + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + checksum: 10c0/d628c847a6fe57c1d24cad2a266766d9b9713a7826bf84ee5beff88f3d302bfeba066a65b376e28e442f0877c29d7d70bb66ba5e0b444015ef4f5a886b7dc775 + languageName: node + linkType: hard + "@liatrio/backstage-plugin-autogov-common@workspace:backstage-plugin-autogov-common": version: 0.0.0-use.local resolution: "@liatrio/backstage-plugin-autogov-common@workspace:backstage-plugin-autogov-common" @@ -5344,6 +5361,24 @@ __metadata: languageName: unknown linkType: soft +"@liatrio/backstage-plugin-module-autogov-processor@workspace:backstage-plugin-module-autogov-processor": + version: 0.0.0-use.local + resolution: "@liatrio/backstage-plugin-module-autogov-processor@workspace:backstage-plugin-module-autogov-processor" + dependencies: + "@backstage/backend-plugin-api": "npm:^1.0.0" + "@backstage/backend-test-utils": "npm:^1.0.0" + "@backstage/catalog-model": "npm:^1.7.0" + "@backstage/cli": "npm:^0.27.1" + "@backstage/config": "npm:^1.2.0" + "@backstage/integration": "npm:^1.15.1" + "@backstage/plugin-catalog-node": "npm:^1.13.1" + "@backstage/types": "npm:^1.1.1" + "@liatrio/backstage-plugin-autogov-common": "npm:^v1.2.4" + "@types/node-fetch": "npm:2" + node-fetch: "npm:2" + languageName: unknown + linkType: soft + "@manypkg/find-root@npm:^1.1.0": version: 1.1.0 resolution: "@manypkg/find-root@npm:1.1.0" @@ -9364,7 +9399,7 @@ __metadata: languageName: node linkType: hard -"@types/node-fetch@npm:^2.6.11": +"@types/node-fetch@npm:2, @types/node-fetch@npm:^2.6.11": version: 2.6.11 resolution: "@types/node-fetch@npm:2.6.11" dependencies: @@ -21476,9 +21511,9 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:2.6.7": - version: 2.6.7 - resolution: "node-fetch@npm:2.6.7" +"node-fetch@npm:2, node-fetch@npm:^2.6.12, node-fetch@npm:^2.6.7, node-fetch@npm:^2.6.9, node-fetch@npm:^2.7.0": + version: 2.7.0 + resolution: "node-fetch@npm:2.7.0" dependencies: whatwg-url: "npm:^5.0.0" peerDependencies: @@ -21486,13 +21521,13 @@ __metadata: peerDependenciesMeta: encoding: optional: true - checksum: 10c0/fcae80f5ac52fbf5012f5e19df2bd3915e67d3b3ad51cb5942943df2238d32ba15890fecabd0e166876a9f98a581ab50f3f10eb942b09405c49ef8da36b826c7 + checksum: 10c0/b55786b6028208e6fbe594ccccc213cab67a72899c9234eb59dba51062a299ea853210fcf526998eaa2867b0963ad72338824450905679ff0fa304b8c5093ae8 languageName: node linkType: hard -"node-fetch@npm:^2.6.12, node-fetch@npm:^2.6.7, node-fetch@npm:^2.6.9, node-fetch@npm:^2.7.0": - version: 2.7.0 - resolution: "node-fetch@npm:2.7.0" +"node-fetch@npm:2.6.7": + version: 2.6.7 + resolution: "node-fetch@npm:2.6.7" dependencies: whatwg-url: "npm:^5.0.0" peerDependencies: @@ -21500,7 +21535,7 @@ __metadata: peerDependenciesMeta: encoding: optional: true - checksum: 10c0/b55786b6028208e6fbe594ccccc213cab67a72899c9234eb59dba51062a299ea853210fcf526998eaa2867b0963ad72338824450905679ff0fa304b8c5093ae8 + checksum: 10c0/fcae80f5ac52fbf5012f5e19df2bd3915e67d3b3ad51cb5942943df2238d32ba15890fecabd0e166876a9f98a581ab50f3f10eb942b09405c49ef8da36b826c7 languageName: node linkType: hard