Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ contains backstage plugins written and maintained by myself
This plugin provides a way to run [Renovate](https://github.com/renovatebot/renovate) against repositories to extract reports and display them in the Backstage UI on an organization scale.
See [@secustor/backstage-plugin-renovate](./plugins/renovate/README.md) for more information.

**Supported features:**

- Organization-wide Renovate report aggregation
- Integration with Backstage UI
- Automated dependency update insights
- Report extraction and visualization

### Scaffolder Backend Module Filter Utilities

This plugin provides a set of utilities to filter modules in the backend scaffolder.
Expand Down
5 changes: 5 additions & 0 deletions app-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ backend:
connection: ':memory:'
# workingDirectory: /tmp # Use this to configure a working directory for the scaffolder, defaults to the OS temp-dir

actions:
pluginSources:
- 'catalog'
- 'renovate'

integrations:
github:
- host: github.com
Expand Down
1 change: 1 addition & 0 deletions packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"@backstage/plugin-catalog-backend-module-backstage-openapi": "^0.5.2",
"@backstage/plugin-catalog-backend-module-github": "^0.10.0",
"@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "^0.2.8",
"@backstage/plugin-mcp-actions-backend": "^0.1.0",
"@backstage/plugin-permission-common": "^0.9.0",
"@backstage/plugin-permission-node": "^0.10.0",
"@backstage/plugin-proxy-backend": "^0.6.2",
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ backend.add(import('@backstage/plugin-search-backend-module-techdocs'));

backend.add(import('@backstage/plugin-techdocs-backend'));

backend.add(import('@backstage/plugin-mcp-actions-backend'));

backend.add(import('@secustor/backstage-plugin-renovate-backend'));
backend.add(
import('@secustor/backstage-plugin-renovate-backend-module-runtime-direct'),
Expand Down
2 changes: 1 addition & 1 deletion plugins/renovate-backend-module-runtime-s3/config.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@ export interface Config {
};
};
};
}
}
4 changes: 2 additions & 2 deletions plugins/renovate-backend-module-runtime-s3/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function getS3Config(config: Config, logger: LoggerService): S3Config {
key: config.getString('key'),
endpoint: config.getOptionalString('endpoint'),
};

logger.debug('S3 configuration loaded successfully', {
bucket: settings.bucket,
region: settings.region,
Expand All @@ -30,4 +30,4 @@ export function getS3Config(config: Config, logger: LoggerService): S3Config {
logger.error(message);
throw new Error(message);
}
}
}
78 changes: 50 additions & 28 deletions plugins/renovate-backend-module-runtime-s3/src/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ class ReportReader {
};

this.s3Client = new S3Client(clientConfig);
this.logger.debug('Initialized S3 client', {
bucket: config.bucket,
this.logger.debug('Initialized S3 client', {
bucket: config.bucket,
region: config.region,
usingEndpoint: !!config.endpoint,
});
Expand All @@ -34,7 +34,9 @@ class ReportReader {
if (!objectKey) {
throw new Error('S3 key is required');
}
this.logger.debug(`Reading report from S3: ${this.config.bucket}/${objectKey}`);
this.logger.debug(
`Reading report from S3: ${this.config.bucket}/${objectKey}`,
);

try {
const response = await this.s3Client.send(
Expand All @@ -51,7 +53,8 @@ class ReportReader {
const fileContent = await response.Body.transformToString();
return this.parseReport(fileContent, objectKey);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
const errorMessage =
error instanceof Error ? error.message : String(error);
this.logger.warn(`Error reading report from S3: ${errorMessage}`);
throw error;
}
Expand All @@ -63,41 +66,52 @@ class ReportReader {
return {
problems: report.problems || [],
repositories: Object.fromEntries(
Object.entries(report.repositories || {}).map(([repoKey, value]: [string, any]) => [
repoKey,
{
problems: value?.problems || [],
branches: value?.branches || [],
packageFiles: value?.packageFiles || {},
libYears: value?.libYears,
}
])
)
Object.entries(report.repositories || {}).map(
([repoKey, value]: [string, any]) => [
repoKey,
{
problems: value?.problems || [],
branches: value?.branches || [],
packageFiles: value?.packageFiles || {},
libYears: value?.libYears,
},
],
),
),
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
const errorMessage =
error instanceof Error ? error.message : String(error);
this.logger.error(`Invalid JSON in ${key}: ${errorMessage}`);
throw new Error(`Invalid JSON in renovate report: ${errorMessage}`);
}
}
}

class ReportFormatter {
static formatResponse(targetRepo: string, data?: unknown, error?: string): RenovateRunResult {
static formatResponse(
targetRepo: string,
data?: unknown,
error?: string,
): RenovateRunResult {
const response = {
msg: error ? `Error reading report: ${error}` : 'Renovate report extracted from file',
msg: error
? `Error reading report: ${error}`
: 'Renovate report extracted from file',
logContext: targetRepo || 'unknown',
report: {
problems: [],
repositories: data ? {
[targetRepo]: {
problems: (data as any)?.problems || [],
branches: (data as any)?.branches || [],
packageFiles: (data as any)?.packageFiles || {},
libYears: (data as any)?.libYears,
}
} : {}
}
repositories: data
? {
[targetRepo]: {
problems: (data as any)?.problems || [],
branches: (data as any)?.branches || [],
packageFiles: (data as any)?.packageFiles || {},
libYears: (data as any)?.libYears,
},
}
: {},
},
};
return {
stdout: Readable.from(`${JSON.stringify(response)}\n`),
Expand Down Expand Up @@ -126,13 +140,21 @@ export class S3 implements RenovateWrapper {

if (!repositoryData) {
logger.warn(`No data found for repository: ${targetRepo}`);
return ReportFormatter.formatResponse(targetRepo, undefined, "no report found for this repository");
return ReportFormatter.formatResponse(
targetRepo,
undefined,
'no report found for this repository',
);
}

logger.debug(`Found data for repository: ${targetRepo}`);
return ReportFormatter.formatResponse(targetRepo, repositoryData);
} catch (error) {
return ReportFormatter.formatResponse(targetRepo, undefined, String(error));
return ReportFormatter.formatResponse(
targetRepo,
undefined,
String(error),
);
}
}
}
46 changes: 43 additions & 3 deletions plugins/renovate-backend/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@ import {
coreServices,
createBackendPlugin,
} from '@backstage/backend-plugin-api';
import { actionsRegistryServiceRef } from '@backstage/backend-plugin-api/alpha';
import { createRouter } from './service/router';
import { RenovateWrapper } from '@secustor/backstage-plugin-renovate-common';
import {
renovateReport,
RenovateWrapper,
targetRepo,
} from '@secustor/backstage-plugin-renovate-common';
import { RenovateRunner } from './wrapper';
import { RouterOptions } from './service/types';
import { DatabaseHandler } from './service/databaseHandler';
Expand Down Expand Up @@ -51,6 +56,7 @@ export const renovatePlugin = createBackendPlugin({

env.registerInit({
deps: {
actionsRegistry: actionsRegistryServiceRef,
rootConfig: coreServices.rootConfig,
logger: coreServices.logger,
httpRouter: coreServices.httpRouter,
Expand All @@ -60,11 +66,16 @@ export const renovatePlugin = createBackendPlugin({
auth: coreServices.auth,
},
async init(options) {
const { database, httpRouter, logger } = options;
const { actionsRegistry, database, httpRouter, logger } = options;

const databaseHandler = await DatabaseHandler.create({
database,
logger,
});

const routerOptions: RouterOptions = {
...options,
databaseHandler: await DatabaseHandler.create({ database, logger }),
databaseHandler,
runtimes,
queueFactories,
};
Expand All @@ -73,6 +84,35 @@ export const renovatePlugin = createBackendPlugin({
await scheduleJobSync(renovateRunner, routerOptions);
await scheduleCleanupTask(routerOptions);

actionsRegistry.register({
name: 'run-renovate',
title: 'Run Renovate',
description: `Run Renovate against a target repository to extract dependencies and generates a new report.
The report consists of:
- a list of dependencies grouped by under 'packageFile' by manager (e.g. npm, pip, etc.) and their updates if available
- a list of branches Renovate has created or would create and the updates the branch contain
- a list of problems Renovate has detected while processing the repository
- a record of libYearsWithStatus which contains the summarized age of dependencies grouped by their manager / ecosystem
- a summary dependencyStatus which contains the number of dependencies and the number of outdated dependencies`,
attributes: {},
schema: {
input: () => targetRepo,
output: () => renovateReport,
},
action: async ({ input, logger: localLogger }) => {
const report = await renovateRunner.renovate(
{
id: 'run-renovate',
target: input,
},
localLogger,
);
return {
output: report,
};
},
});

httpRouter.use(await createRouter(renovateRunner, routerOptions));
httpRouter.addAuthPolicy({
path: '/health',
Expand Down
Loading