diff --git a/plugins/scaffolder-backend-module-github/package.json b/plugins/scaffolder-backend-module-github/package.json index 9ebd61f809..2ad78b0000 100644 --- a/plugins/scaffolder-backend-module-github/package.json +++ b/plugins/scaffolder-backend-module-github/package.json @@ -44,9 +44,11 @@ "dependencies": { "@backstage/backend-common": "workspace:^", "@backstage/backend-plugin-api": "workspace:^", + "@backstage/catalog-client": "workspace:^", "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/integration": "workspace:^", + "@backstage/plugin-catalog-node": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", "@octokit/webhooks": "^10.0.0", "libsodium-wrappers": "^0.7.11", diff --git a/plugins/scaffolder-backend-module-github/src/actions/gitHubEnvironment.examples.ts b/plugins/scaffolder-backend-module-github/src/actions/gitHubEnvironment.examples.ts index 44ddcc2175..877a5bdf35 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/gitHubEnvironment.examples.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/gitHubEnvironment.examples.ts @@ -328,7 +328,7 @@ export const examples: TemplateExample[] = [ input: { repoUrl: 'github.com?repo=repository&owner=owner', name: 'envname', - prevent_self_review: true, + preventSelfReview: true, }, }, ], @@ -344,16 +344,7 @@ export const examples: TemplateExample[] = [ input: { repoUrl: 'github.com?repo=repository&owner=owner', name: 'envname', - reviewers: [ - { - Type: 'User', - ID: 1, - }, - { - Type: 'Team', - ID: 2, - }, - ], + reviewers: ['group:defautl/team-a', 'user:defautl/johndoe'], }, }, ], diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.examples.test.ts index 7d0b72c8ba..3c710928ca 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.examples.test.ts @@ -909,11 +909,11 @@ describe('github:environment:create examples', () => { wait_timer: 0, reviewers: [ { - Type: 'User', + Type: 'Team', ID: 1, }, { - Type: 'Team', + Type: 'User', ID: 2, }, ], diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.test.ts index 2c729e2e5a..9a4ffac162 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.test.ts @@ -19,6 +19,7 @@ import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test- import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; +import { CatalogClient } from '@backstage/catalog-client'; const mockOctokit = { rest: { @@ -32,8 +33,19 @@ const mockOctokit = { createOrUpdateEnvironment: jest.fn(), get: jest.fn(), }, + teams: { + getByName: jest.fn(), + }, + users: { + getByUsername: jest.fn(), + }, }, }; + +const mockCatalogClient: Partial = { + getEntitiesByRefs: jest.fn(), +}; + jest.mock('octokit', () => ({ Octokit: class { constructor() { @@ -42,6 +54,10 @@ jest.mock('octokit', () => ({ }, })); +jest.mock('@backstage/catalog-client', () => ({ + CatalogClient: mockCatalogClient, +})); + const publicKey = '2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r+dB7TJyvvcCU='; describe('github:environment:create', () => { @@ -76,9 +92,36 @@ describe('github:environment:create', () => { id: 'repoid', }, }); + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { + id: 1, + }, + }); + mockOctokit.rest.teams.getByName.mockResolvedValue({ + data: { + id: 2, + }, + }); + (mockCatalogClient.getEntitiesByRefs as jest.Mock).mockResolvedValue({ + items: [ + { + kind: 'User', + metadata: { + name: 'johndoe', + }, + }, + { + kind: 'Group', + metadata: { + name: 'team-a', + }, + }, + ], + }); action = createGithubEnvironmentAction({ integrations, + catalog: mockCatalogClient as unknown as CatalogClient, }); }); @@ -340,7 +383,7 @@ describe('github:environment:create', () => { ...mockContext, input: { ...mockContext.input, - wait_timer: 1000, + waitTimer: 1000, }, }); @@ -357,12 +400,12 @@ describe('github:environment:create', () => { }); }); - it('should work with prevent_self_review set to true', async () => { + it('should work with preventSelfReview set to true', async () => { await action.handler({ ...mockContext, input: { ...mockContext.input, - prevent_self_review: true, + preventSelfReview: true, }, }); @@ -379,12 +422,12 @@ describe('github:environment:create', () => { }); }); - it('should work with prevent_self_review set to false', async () => { + it('should work with preventSelfReview set to false', async () => { await action.handler({ ...mockContext, input: { ...mockContext.input, - prevent_self_review: false, + preventSelfReview: false, }, }); @@ -406,16 +449,7 @@ describe('github:environment:create', () => { ...mockContext, input: { ...mockContext.input, - reviewers: [ - { - Type: 'User', - ID: 1, - }, - { - Type: 'Team', - ID: 2, - }, - ], + reviewers: ['group:defautl/team-a', 'user:defautl/johndoe'], }, }); @@ -430,12 +464,12 @@ describe('github:environment:create', () => { prevent_self_review: false, reviewers: [ { - Type: 'User', - ID: 1, + id: 1, + type: 'User', }, { - Type: 'Team', - ID: 2, + id: 2, + type: 'Team', }, ], }); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts index 8861aa5bd3..b482503717 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts @@ -24,6 +24,7 @@ import { getOctokitOptions } from './helpers'; import { Octokit } from 'octokit'; import Sodium from 'libsodium-wrappers'; import { examples } from './gitHubEnvironment.examples'; +import { CatalogClient } from '@backstage/catalog-client'; /** * Creates an `github:environment:create` Scaffolder action that creates a Github Environment. @@ -32,8 +33,9 @@ import { examples } from './gitHubEnvironment.examples'; */ export function createGithubEnvironmentAction(options: { integrations: ScmIntegrationRegistry; + catalog: CatalogClient; }) { - const { integrations } = options; + const { integrations, catalog } = options; // For more information on how to define custom actions, see // https://backstage.io/docs/features/software-templates/writing-custom-actions return createTemplateAction<{ @@ -48,9 +50,9 @@ export function createGithubEnvironmentAction(options: { environmentVariables?: { [key: string]: string }; secrets?: { [key: string]: string }; token?: string; - wait_timer?: number; - prevent_self_review?: boolean; - reviewers?: Array<{ type?: 'User' | 'Team'; id?: number }> | null; + waitTimer?: number; + preventSelfReview?: boolean; + reviewers?: string[]; }>({ id: 'github:environment:create', description: 'Creates Deployment Environments', @@ -123,13 +125,13 @@ export function createGithubEnvironmentAction(options: { type: 'string', description: 'The token to use for authorization to GitHub', }, - wait_timer: { + waitTimer: { title: 'Wait Timer', type: 'integer', description: 'The time to wait before creating or updating the environment (in milliseconds)', }, - prevent_self_review: { + preventSelfReview: { title: 'Prevent Self Review', type: 'boolean', description: 'Whether to prevent self-review for this environment', @@ -139,17 +141,7 @@ export function createGithubEnvironmentAction(options: { type: 'array', description: 'Reviewers for this environment', items: { - type: 'object', - properties: { - type: { - title: 'Type', - type: 'string', - }, - id: { - title: 'ID', - type: 'integer', - }, - }, + type: 'string', }, }, }, @@ -165,15 +157,14 @@ export function createGithubEnvironmentAction(options: { environmentVariables, secrets, token: providedToken, - wait_timer, - prevent_self_review, + waitTimer, + preventSelfReview, reviewers, } = ctx.input; - // Wait for the specified time before creating or updating the environment - if (wait_timer) { - await new Promise(resolve => setTimeout(resolve, wait_timer)); - } + // When environment creation step is executed right after a repo publish step, the repository might not be available immediately. + // Add a 2-second delay before initiating the steps in this action. + await new Promise(resolve => setTimeout(resolve, 2000)); const octokitOptions = await getOctokitOptions({ integrations, @@ -193,14 +184,51 @@ export function createGithubEnvironmentAction(options: { repo: repo, }); + // convert reviewers from catalog entity to Github user or team + const githubReviewers: { type: 'User' | 'Team'; id: number }[] = []; + if (reviewers) { + // Fetch reviewers from Catalog + const { items: reviewersEntityRefs } = await catalog.getEntitiesByRefs({ + entityRefs: reviewers, + }); + for (const reviewerEntityRef of reviewersEntityRefs) { + if (reviewerEntityRef?.kind === 'User') { + try { + const user = await client.rest.users.getByUsername({ + username: reviewerEntityRef.metadata.name, + }); + githubReviewers.push({ + type: 'User', + id: user.data.id, + }); + } catch (error) { + console.log('User not found:', error); + } + } else if (reviewerEntityRef?.kind === 'Group') { + try { + const team = await client.rest.teams.getByName({ + org: owner, + team_slug: reviewerEntityRef.metadata.name, + }); + githubReviewers.push({ + type: 'Team', + id: team.data.id, + }); + } catch (error) { + console.log('Team not found:', error); + } + } + } + } + await client.rest.repos.createOrUpdateEnvironment({ owner: owner, repo: repo, environment_name: name, deployment_branch_policy: deploymentBranchPolicy ?? null, - wait_timer: wait_timer ?? 0, - prevent_self_review: prevent_self_review ?? false, - reviewers: reviewers ?? null, + wait_timer: waitTimer ?? 0, + prevent_self_review: preventSelfReview ?? false, + reviewers: githubReviewers.length ? githubReviewers : null, }); if (customBranchPolicyNames) { diff --git a/plugins/scaffolder-backend-module-github/src/module.ts b/plugins/scaffolder-backend-module-github/src/module.ts index a5676ab8c9..4873cf445f 100644 --- a/plugins/scaffolder-backend-module-github/src/module.ts +++ b/plugins/scaffolder-backend-module-github/src/module.ts @@ -35,6 +35,7 @@ import { DefaultGithubCredentialsProvider, ScmIntegrations, } from '@backstage/integration'; +import { catalogServiceRef } from '@backstage/plugin-catalog-node'; /** * @public @@ -48,8 +49,9 @@ export const githubModule = createBackendModule({ deps: { scaffolder: scaffolderActionsExtensionPoint, config: coreServices.rootConfig, + catalog: catalogServiceRef, }, - async init({ scaffolder, config }) { + async init({ scaffolder, config, catalog }) { const integrations = ScmIntegrations.fromConfig(config); const githubCredentialsProvider = DefaultGithubCredentialsProvider.fromIntegrations(integrations); @@ -68,6 +70,7 @@ export const githubModule = createBackendModule({ }), createGithubEnvironmentAction({ integrations, + catalog, }), createGithubIssuesLabelAction({ integrations,