feat(events,catalog/github): handle github:push events

Signed-off-by: Rogerio Angeliski <angeliski@hotmail.com>
This commit is contained in:
Rogerio Angeliski
2022-11-22 01:31:46 +00:00
parent 63947301d6
commit a0fd4af94a
10 changed files with 762 additions and 5 deletions
@@ -5,12 +5,15 @@
```ts
import { AnalyzeOptions } from '@backstage/plugin-catalog-backend';
import { BackendFeature } from '@backstage/backend-plugin-api';
import { CatalogApi } from '@backstage/catalog-client';
import { CatalogProcessor } from '@backstage/plugin-catalog-backend';
import { CatalogProcessorEmit } from '@backstage/plugin-catalog-backend';
import { Config } from '@backstage/config';
import { Entity } from '@backstage/catalog-model';
import { EntityProvider } from '@backstage/plugin-catalog-backend';
import { EntityProviderConnection } from '@backstage/plugin-catalog-backend';
import { EventParams } from '@backstage/plugin-events-node';
import { EventSubscriber } from '@backstage/plugin-events-node';
import { GithubCredentialsProvider } from '@backstage/integration';
import { GithubIntegrationConfig } from '@backstage/integration';
import { graphql } from '@octokit/graphql';
@@ -76,22 +79,28 @@ export class GitHubEntityProvider implements EntityProvider {
}
// @public
export class GithubEntityProvider implements EntityProvider {
export class GithubEntityProvider implements EntityProvider, EventSubscriber {
// (undocumented)
connect(connection: EntityProviderConnection): Promise<void>;
// (undocumented)
static fromConfig(
config: Config,
options: {
catalogApi?: CatalogApi;
logger: Logger;
schedule?: TaskRunner;
scheduler?: PluginTaskScheduler;
tokenManager?: TokenManager;
},
): GithubEntityProvider[];
// (undocumented)
getProviderName(): string;
// (undocumented)
onEvent(params: EventParams): Promise<void>;
// (undocumented)
refresh(logger: Logger): Promise<void>;
// (undocumented)
supportsEventTopics(): string[];
}
// @alpha
@@ -42,13 +42,16 @@
"@backstage/errors": "workspace:^",
"@backstage/integration": "workspace:^",
"@backstage/plugin-catalog-backend": "workspace:^",
"@backstage/plugin-catalog-common": "workspace:^",
"@backstage/plugin-catalog-node": "workspace:^",
"@backstage/plugin-events-node": "workspace:^",
"@backstage/types": "workspace:^",
"@octokit/graphql": "^5.0.0",
"@octokit/rest": "^19.0.3",
"git-url-parse": "^13.0.0",
"lodash": "^4.17.21",
"node-fetch": "^2.6.7",
"p-limit": "^3.0.2",
"uuid": "^8.0.0",
"winston": "^3.2.1"
},
@@ -34,6 +34,7 @@ export type QueryResponse = {
type RepositoryOwnerResponse = {
repositories?: Connection<RepositoryResponse>;
repository?: RepositoryResponse;
};
export type OrganizationResponse = {
@@ -300,6 +301,60 @@ export async function getOrganizationRepositories(
return { repositories };
}
export async function getOrganizationRepository(
client: typeof graphql,
org: string,
catalogPath: string,
repoName: string,
): Promise<{ repository: RepositoryResponse | undefined }> {
let relativeCatalogPathRef: string;
// We must strip the leading slash or the query for objects does not work
if (catalogPath.startsWith('/')) {
relativeCatalogPathRef = catalogPath.substring(1);
} else {
relativeCatalogPathRef = catalogPath;
}
const catalogPathRef = `HEAD:${relativeCatalogPathRef}`;
const query = `
query repositories($org: String!, $catalogPathRef: String!, $repoName: String!) {
repositoryOwner(login: $org) {
login
repository(name: $repoName) {
name
catalogInfoFile: object(expression: $catalogPathRef) {
__typename
... on Blob {
id
text
}
}
url
isArchived
repositoryTopics(first: 100) {
nodes {
... on RepositoryTopic {
topic {
name
}
}
}
}
defaultBranchRef {
name
}
}
}
}`;
const response: QueryResponse = await client(query, {
org,
catalogPathRef,
repoName,
});
return { repository: response.repositoryOwner?.repository };
}
/**
* Gets all the users out of a Github organization.
*
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { getVoidLogger } from '@backstage/backend-common';
import { getVoidLogger, TokenManager } from '@backstage/backend-common';
import {
PluginTaskScheduler,
TaskInvocationDefinition,
@@ -24,10 +24,13 @@ import { ConfigReader } from '@backstage/config';
import { EntityProviderConnection } from '@backstage/plugin-catalog-backend';
import { GithubEntityProvider } from './GithubEntityProvider';
import * as helpers from '../lib/github';
import { EventParams } from '@backstage/plugin-events-node';
import { CatalogApi } from '@backstage/catalog-client';
jest.mock('../lib/github', () => {
return {
getOrganizationRepositories: jest.fn(),
getOrganizationRepository: jest.fn(),
};
});
class PersistingTaskRunner implements TaskRunner {
@@ -45,6 +48,15 @@ class PersistingTaskRunner implements TaskRunner {
const logger = getVoidLogger();
const mockCatalogApi: Partial<CatalogApi> = {
refreshEntity: jest.fn(),
};
const mockTokenManager: jest.Mocked<TokenManager> = {
getToken: jest.fn(),
authenticate: jest.fn(),
};
describe('GithubEntityProvider', () => {
afterEach(() => jest.resetAllMocks());
@@ -716,4 +728,420 @@ describe('GithubEntityProvider', () => {
expect(providers).toHaveLength(1);
expect(providers[0].getProviderName()).toEqual('github-provider:default');
});
it('apply delta update on added files from push event', async () => {
const schedule = new PersistingTaskRunner();
const config = new ConfigReader({
catalog: {
providers: {
github: {
organization: 'test-org',
},
},
},
});
const provider = GithubEntityProvider.fromConfig(config, {
logger,
schedule,
})[0];
const entityProviderConnection: EntityProviderConnection = {
applyMutation: jest.fn(),
refresh: jest.fn(),
};
await provider.connect(entityProviderConnection);
const mockGetOrganizationRepository = jest.spyOn(
helpers,
'getOrganizationRepository',
);
mockGetOrganizationRepository.mockReturnValue(
Promise.resolve({
repository: {
name: 'test-repo',
url: 'https://github.com/test-org/test-repo',
repositoryTopics: { nodes: [] },
isArchived: false,
defaultBranchRef: {
name: 'main',
},
catalogInfoFile: {
__typename: 'Blob',
id: 'abc123',
text: 'some yaml',
},
},
}),
);
const event: EventParams = {
topic: 'github.push',
metadata: {
'x-github-event': 'push',
},
eventPayload: {
ref: 'refs/heads/main',
repository: {
name: 'teste-1',
url: 'https://github.com/test-org/test-repo',
default_branch: 'main',
stargazers: 0,
master_branch: 'main',
organization: 'test-org',
},
created: true,
deleted: false,
forced: false,
commits: [
{
added: ['new-file.yaml'],
removed: [],
modified: [],
},
{
added: ['catalog-info.yaml'],
removed: [],
modified: [],
},
],
},
};
const url =
'https://github.com/test-org/test-repo/blob/main/catalog-info.yaml';
const expectedEntities = [
{
entity: {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Location',
metadata: {
annotations: {
'backstage.io/managed-by-location': `url:${url}`,
'backstage.io/managed-by-origin-location': `url:${url}`,
},
name: 'generated-8688630f57e421bc85f12b9828ed7dad6aff3bb3',
},
spec: {
presence: 'optional',
target: `${url}`,
type: 'url',
},
},
locationKey: 'github-provider:default',
},
];
await provider.onEvent(event);
expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1);
expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({
type: 'delta',
added: expectedEntities,
removed: [],
});
});
it('apply delta update on removed files from push event', async () => {
const schedule = new PersistingTaskRunner();
const config = new ConfigReader({
catalog: {
providers: {
github: {
organization: 'test-org',
},
},
},
});
const provider = GithubEntityProvider.fromConfig(config, {
logger,
schedule,
})[0];
const entityProviderConnection: EntityProviderConnection = {
applyMutation: jest.fn(),
refresh: jest.fn(),
};
await provider.connect(entityProviderConnection);
const mockGetOrganizationRepository = jest.spyOn(
helpers,
'getOrganizationRepository',
);
mockGetOrganizationRepository.mockReturnValue(
Promise.resolve({
repository: {
name: 'test-repo',
url: 'https://github.com/test-org/test-repo',
repositoryTopics: { nodes: [] },
isArchived: false,
defaultBranchRef: {
name: 'main',
},
catalogInfoFile: {
__typename: 'Blob',
id: 'abc123',
text: 'some yaml',
},
},
}),
);
const event: EventParams = {
topic: 'github.push',
metadata: {
'x-github-event': 'push',
},
eventPayload: {
ref: 'refs/heads/main',
repository: {
name: 'teste-1',
url: 'https://github.com/test-org/test-repo',
default_branch: 'main',
stargazers: 0,
master_branch: 'main',
organization: 'test-org',
},
created: true,
deleted: false,
forced: false,
commits: [
{
added: ['new-file.yaml'],
removed: [],
modified: [],
},
{
added: [],
removed: ['catalog-info.yaml'],
modified: [],
},
],
},
};
const url =
'https://github.com/test-org/test-repo/blob/main/catalog-info.yaml';
const expectedEntities = [
{
entity: {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Location',
metadata: {
annotations: {
'backstage.io/managed-by-location': `url:${url}`,
'backstage.io/managed-by-origin-location': `url:${url}`,
},
name: 'generated-8688630f57e421bc85f12b9828ed7dad6aff3bb3',
},
spec: {
presence: 'optional',
target: `${url}`,
type: 'url',
},
},
locationKey: 'github-provider:default',
},
];
await provider.onEvent(event);
expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1);
expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({
type: 'delta',
added: [],
removed: expectedEntities,
});
});
it('apply refresh call on modified files from push event', async () => {
const schedule = new PersistingTaskRunner();
const config = new ConfigReader({
catalog: {
providers: {
github: {
organization: 'test-org',
},
},
},
});
mockTokenManager.getToken.mockResolvedValue({ token: '' });
const provider = GithubEntityProvider.fromConfig(config, {
logger,
schedule,
tokenManager: mockTokenManager,
catalogApi: mockCatalogApi as unknown as CatalogApi,
})[0];
const entityProviderConnection: EntityProviderConnection = {
applyMutation: jest.fn(),
refresh: jest.fn(),
};
await provider.connect(entityProviderConnection);
const mockGetOrganizationRepository = jest.spyOn(
helpers,
'getOrganizationRepository',
);
mockGetOrganizationRepository.mockReturnValue(
Promise.resolve({
repository: {
name: 'test-repo',
url: 'https://github.com/test-org/test-repo',
repositoryTopics: { nodes: [] },
isArchived: false,
defaultBranchRef: {
name: 'main',
},
catalogInfoFile: {
__typename: 'Blob',
id: 'abc123',
text: 'some yaml',
},
},
}),
);
const event: EventParams = {
topic: 'github.push',
metadata: {
'x-github-event': 'push',
},
eventPayload: {
ref: 'refs/heads/main',
repository: {
name: 'teste-1',
url: 'https://github.com/test-org/test-repo',
default_branch: 'main',
stargazers: 0,
master_branch: 'main',
organization: 'test-org',
},
created: true,
deleted: false,
forced: false,
commits: [
{
added: ['new-file.yaml'],
removed: [],
modified: [],
},
{
added: [],
removed: [],
modified: ['catalog-info.yaml'],
},
],
},
};
await provider.onEvent(event);
expect(mockCatalogApi.refreshEntity).toHaveBeenCalledTimes(1);
expect(mockCatalogApi.refreshEntity).toHaveBeenCalledWith(
'location:default/generated-8688630f57e421bc85f12b9828ed7dad6aff3bb3',
{ token: '' },
);
expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(0);
});
it('should recover repository information when match filters from push event', async () => {
const schedule = new PersistingTaskRunner();
const config = new ConfigReader({
catalog: {
providers: {
github: {
organization: 'test-org',
checkRepositoryFiltersForWebhook: true,
filters: {
branch: 'my-special-branch',
},
},
},
},
});
mockTokenManager.getToken.mockResolvedValue({ token: '' });
const provider = GithubEntityProvider.fromConfig(config, {
logger,
schedule,
tokenManager: mockTokenManager,
catalogApi: mockCatalogApi as unknown as CatalogApi,
})[0];
const entityProviderConnection: EntityProviderConnection = {
applyMutation: jest.fn(),
refresh: jest.fn(),
};
await provider.connect(entityProviderConnection);
const mockGetOrganizationRepository = jest.spyOn(
helpers,
'getOrganizationRepository',
);
mockGetOrganizationRepository.mockReturnValue(
Promise.resolve({
repository: {
name: 'test-repo',
url: 'https://github.com/test-org/test-repo',
repositoryTopics: { nodes: [] },
isArchived: false,
defaultBranchRef: {
name: 'main',
},
catalogInfoFile: {
__typename: 'Blob',
id: 'abc123',
text: 'some yaml',
},
},
}),
);
const event: EventParams = {
topic: 'github.push',
metadata: {
'x-github-event': 'push',
},
eventPayload: {
ref: 'refs/heads/my-special-branch',
repository: {
name: 'teste-1',
url: 'https://github.com/test-org/test-repo',
default_branch: 'main',
stargazers: 0,
master_branch: 'main',
organization: 'test-org',
},
created: true,
deleted: false,
forced: false,
commits: [
{
added: ['new-file.yaml'],
removed: [],
modified: [],
},
{
added: [],
removed: [],
modified: ['catalog-info.yaml'],
},
],
},
};
await provider.onEvent(event);
expect(mockCatalogApi.refreshEntity).toHaveBeenCalledTimes(1);
expect(mockCatalogApi.refreshEntity).toHaveBeenCalledWith(
'location:default/generated-6ffd478ea33caf9af61fa75cc09b5aa7770470f0de',
{ token: '' },
);
expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(0);
});
});
@@ -24,22 +24,36 @@ import {
SingleInstanceGithubCredentialsProvider,
} from '@backstage/integration';
import {
DeferredEntity,
EntityProvider,
EntityProviderConnection,
LocationSpec,
locationSpecToLocationEntity,
} from '@backstage/plugin-catalog-backend';
import { LocationSpec } from '@backstage/plugin-catalog-common';
import { graphql } from '@octokit/graphql';
import * as uuid from 'uuid';
import limiterFactory from 'p-limit';
import { Logger } from 'winston';
import {
readProviderConfigs,
GithubEntityProviderConfig,
} from './GithubEntityProviderConfig';
import { getOrganizationRepositories, RepositoryResponse } from '../lib/github';
import {
getOrganizationRepositories,
getOrganizationRepository,
RepositoryResponse,
} from '../lib/github';
import { satisfiesTopicFilter } from '../lib/util';
import { EventParams, EventSubscriber } from '@backstage/plugin-events-node';
import { PushEvent, Commit } from '@octokit/webhooks-types';
import { CatalogApi } from '@backstage/catalog-client';
import { TokenManager } from '@backstage/backend-common';
import { stringifyEntityRef } from '@backstage/catalog-model';
const TOPIC_REPO_PUSH = 'github.push';
/**
* Discovers catalog files located in [GitHub](https://github.com).
* The provider will search your GitHub account and register catalog files matching the configured path
@@ -48,20 +62,24 @@ import { satisfiesTopicFilter } from '../lib/util';
*
* @public
*/
export class GithubEntityProvider implements EntityProvider {
export class GithubEntityProvider implements EntityProvider, EventSubscriber {
private readonly config: GithubEntityProviderConfig;
private readonly logger: Logger;
private readonly integration: GithubIntegrationConfig;
private readonly scheduleFn: () => Promise<void>;
private connection?: EntityProviderConnection;
private readonly catalogApi?: CatalogApi;
private readonly tokenManager?: TokenManager;
private readonly githubCredentialsProvider: GithubCredentialsProvider;
static fromConfig(
config: Config,
options: {
catalogApi?: CatalogApi;
logger: Logger;
schedule?: TaskRunner;
scheduler?: PluginTaskScheduler;
tokenManager?: TokenManager;
},
): GithubEntityProvider[] {
if (!options.schedule && !options.scheduler) {
@@ -95,6 +113,8 @@ export class GithubEntityProvider implements EntityProvider {
integration,
options.logger,
taskRunner,
options.catalogApi,
options.tokenManager,
);
});
}
@@ -104,6 +124,8 @@ export class GithubEntityProvider implements EntityProvider {
integration: GithubIntegration,
logger: Logger,
taskRunner: TaskRunner,
catalogApi?: CatalogApi,
tokenManager?: TokenManager,
) {
this.config = config;
this.integration = integration.config;
@@ -113,6 +135,9 @@ export class GithubEntityProvider implements EntityProvider {
this.scheduleFn = this.createScheduleFn(taskRunner);
this.githubCredentialsProvider =
SingleInstanceGithubCredentialsProvider.create(integration.config);
this.catalogApi = catalogApi;
this.tokenManager = tokenManager;
}
/** {@inheritdoc @backstage/plugin-catalog-backend#EntityProvider.getProviderName} */
@@ -242,6 +267,200 @@ export class GithubEntityProvider implements EntityProvider {
presence: 'optional',
};
}
/** {@inheritdoc @backstage/plugin-events-node#EventSubscriber.onEvent} */
async onEvent(params: EventParams): Promise<void> {
this.logger.debug(`Received event from ${params.topic}`);
if (params.topic !== TOPIC_REPO_PUSH) {
return;
}
if (params.metadata?.['x-github-event'] === 'push') {
await this.onRepoPush(params.eventPayload as PushEvent);
}
return;
}
/** {@inheritdoc @backstage/plugin-events-node#EventSubscriber.supportsEventTopics} */
supportsEventTopics(): string[] {
return [TOPIC_REPO_PUSH];
}
private async onRepoPush(event: PushEvent) {
if (!this.connection) {
throw new Error('Not initialized');
}
const repoName = event.repository.name;
const repoUrl = event.repository.url;
this.logger.debug(`handle github:push event for ${repoName} - ${repoUrl}`);
const branch =
this.config.filters?.branch || event.repository.default_branch;
if (!event.ref.includes(branch)) {
this.logger.debug(`skipping push event from ref ${event.ref}`);
return;
}
if (this.config.checkRepositoryFiltersForWebhook) {
const repository = await this.getRepositoryInfo(repoName);
if (!repository) {
this.logger.debug(
`skipping push event from repository ${repoName} because didn't find information in Github`,
);
return;
}
const matchingTargets = this.matchesFilters([repository]);
if (matchingTargets.length === 0) {
this.logger.debug(
`skipping push event from repository ${repoName} because didn't match provider filters`,
);
return;
}
}
// the commit has information about the files (added,removed,modified)
// so we will process the change based in this data
// https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#push
const added = this.collectDeferredEntitiesFromCommit(
event.repository.url,
branch,
event.commits,
(commit: Commit) => [...commit.added],
);
const removed = this.collectDeferredEntitiesFromCommit(
event.repository.url,
branch,
event.commits,
(commit: Commit) => [...commit.removed],
);
const modifiedCatalogFiles = this.collectFilesFromCommit(
event.commits,
(commit: Commit) => [...commit.modified],
);
const limiter = limiterFactory(10);
let modified = [];
let promises: Promise<void>[] = [];
if (this.catalogApi && this.tokenManager) {
modified = modifiedCatalogFiles
.map(filePath => `${event.repository.url}/blob/${branch}/${filePath}`)
.map(url => {
const location = GithubEntityProvider.toLocationSpec(url);
return locationSpecToLocationEntity({ location });
});
const { token } = await this.tokenManager.getToken();
promises = modified.map(entity =>
limiter(async () =>
this.catalogApi!.refreshEntity(stringifyEntityRef(entity), { token }),
),
);
} else {
this.logger.debug(
`skipping modified operation because is missing CatalogApi and/or TokenManager.`,
);
}
if (added.length > 0 || removed.length > 0) {
const connection = this.connection;
promises.push(
limiter(async () =>
connection.applyMutation({
type: 'delta',
added: added,
removed: removed,
}),
),
);
}
await Promise.all(promises);
this.logger.info(
`Processed Github push event: added ${added.length} - removed ${removed.length} - modified ${modified.length}`,
);
}
private collectDeferredEntitiesFromCommit(
repositoryUrl: string,
branch: string,
commits: Commit[],
transformOperation: (commit: Commit) => string[],
): DeferredEntity[] {
const catalogFiles = this.collectFilesFromCommit(
commits,
transformOperation,
);
return this.toDeferredEntities(
catalogFiles.map(
filePath => `${repositoryUrl}/blob/${branch}/${filePath}`,
),
);
}
private collectFilesFromCommit(
commits: Commit[],
transformOperation: (commit: Commit) => string[],
): string[] {
const catalogFile = this.config.catalogPath.startsWith('/')
? this.config.catalogPath.substring(1)
: this.config.catalogPath;
return commits
.map(transformOperation)
.flat()
.filter(file => catalogFile.includes(file));
}
private async getRepositoryInfo(
repoName: string,
): Promise<RepositoryResponse | undefined> {
const organization = this.config.organization;
const host = this.integration.host;
const catalogPath = this.config.catalogPath;
const orgUrl = `https://${host}/${organization}`;
const { headers } = await this.githubCredentialsProvider.getCredentials({
url: orgUrl,
});
const client = graphql.defaults({
baseUrl: this.integration.apiBaseUrl,
headers,
});
const { repository } = await getOrganizationRepository(
client,
organization,
catalogPath,
repoName,
);
return repository;
}
private toDeferredEntities(targets: string[]): DeferredEntity[] {
return targets
.map(target => {
const location = GithubEntityProvider.toLocationSpec(target);
return locationSpecToLocationEntity({ location });
})
.map(entity => {
return {
locationKey: this.getProviderName(),
entity: entity,
};
});
}
}
/*
@@ -102,6 +102,7 @@ describe('readProviderConfigs', () => {
id: 'providerOrganizationOnly',
organization: 'test-org1',
catalogPath: '/catalog-info.yaml',
checkRepositoryFiltersForWebhook: false,
host: 'github.com',
filters: {
repository: undefined,
@@ -119,6 +120,7 @@ describe('readProviderConfigs', () => {
organization: 'test-org2',
catalogPath: 'custom/path/catalog-info.yaml',
host: 'github.com',
checkRepositoryFiltersForWebhook: false,
filters: {
repository: undefined,
branch: undefined,
@@ -134,6 +136,7 @@ describe('readProviderConfigs', () => {
id: 'providerWithRepositoryFilter',
organization: 'test-org3', // organization
catalogPath: '/catalog-info.yaml', // file
checkRepositoryFiltersForWebhook: false,
host: 'github.com',
filters: {
repository: /^repository.*filter$/, // repo
@@ -150,6 +153,7 @@ describe('readProviderConfigs', () => {
id: 'providerWithBranchFilter',
organization: 'test-org4',
catalogPath: '/catalog-info.yaml',
checkRepositoryFiltersForWebhook: false,
host: 'github.com',
filters: {
repository: undefined,
@@ -166,6 +170,7 @@ describe('readProviderConfigs', () => {
id: 'providerWithTopicFilter',
organization: 'test-org5',
catalogPath: '/catalog-info.yaml',
checkRepositoryFiltersForWebhook: false,
host: 'github.com',
filters: {
repository: undefined,
@@ -182,6 +187,7 @@ describe('readProviderConfigs', () => {
id: 'providerWithHost',
organization: 'test-org1',
catalogPath: '/catalog-info.yaml',
checkRepositoryFiltersForWebhook: false,
host: 'ghe.internal.com',
filters: {
repository: undefined,
@@ -198,6 +204,7 @@ describe('readProviderConfigs', () => {
id: 'providerWithSchedule',
organization: 'test-org1',
catalogPath: '/catalog-info.yaml',
checkRepositoryFiltersForWebhook: false,
host: 'github.com',
filters: {
repository: undefined,
@@ -34,6 +34,7 @@ export type GithubEntityProviderConfig = {
topic?: GithubTopicFilters;
};
validateLocationsExist: boolean;
checkRepositoryFiltersForWebhook: boolean;
schedule?: TaskScheduleDefinition;
};
@@ -81,6 +82,9 @@ function readProviderConfig(
const validateLocationsExist =
config?.getOptionalBoolean('validateLocationsExist') ?? false;
const checkRepositoryFiltersForWebhook =
config?.getOptionalBoolean('checkRepositoryFiltersForWebhook') ?? false;
const catalogPathContainsWildcard = catalogPath.includes('*');
if (validateLocationsExist && catalogPathContainsWildcard) {
@@ -110,6 +114,7 @@ function readProviderConfig(
},
schedule,
validateLocationsExist,
checkRepositoryFiltersForWebhook,
};
}