diff --git a/.changeset/angry-cheetahs-hide.md b/.changeset/angry-cheetahs-hide.md new file mode 100644 index 0000000000..d294c507e9 --- /dev/null +++ b/.changeset/angry-cheetahs-hide.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Implement "Required approving review count", "Restrictions", and "Required commit signing" support for "publish:github" action diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 05285d9dc0..f3abd574e6 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -211,6 +211,14 @@ export function createGithubRepoCreateAction(options: { apps?: string[] | undefined; } | undefined; + requiredApprovingReviewCount?: number | undefined; + restrictions?: + | { + users: string[]; + teams: string[]; + apps?: string[] | undefined; + } + | undefined; requiredStatusCheckContexts?: string[] | undefined; requireBranchesToBeUpToDate?: boolean | undefined; requiredConversationResolution?: boolean | undefined; @@ -236,6 +244,7 @@ export function createGithubRepoCreateAction(options: { hasIssues?: boolean | undefined; token?: string | undefined; topics?: string[] | undefined; + requireCommitSigning?: boolean | undefined; }>; // @public @@ -261,11 +270,20 @@ export function createGithubRepoPushAction(options: { apps?: string[]; } | undefined; + requiredApprovingReviewCount?: number | undefined; + restrictions?: + | { + users: string[]; + teams: string[]; + apps?: string[]; + } + | undefined; requiredStatusCheckContexts?: string[] | undefined; requireBranchesToBeUpToDate?: boolean | undefined; requiredConversationResolution?: boolean | undefined; sourcePath?: string | undefined; token?: string | undefined; + requiredCommitSigning?: boolean | undefined; }>; // @public @@ -408,6 +426,14 @@ export function createPublishGithubAction(options: { apps?: string[]; } | undefined; + requiredApprovingReviewCount?: number | undefined; + restrictions?: + | { + users: string[]; + teams: string[]; + apps?: string[]; + } + | undefined; requireCodeOwnerReviews?: boolean | undefined; dismissStaleReviews?: boolean | undefined; requiredStatusCheckContexts?: string[] | undefined; @@ -435,6 +461,7 @@ export function createPublishGithubAction(options: { hasIssues?: boolean | undefined; token?: string | undefined; topics?: string[] | undefined; + requiredCommitSigning?: boolean | undefined; }>; // @public diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.ts index f41150b504..835fc79238 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.ts @@ -59,6 +59,12 @@ export function createGithubRepoCreateAction(options: { teams?: string[]; apps?: string[]; }; + requiredApprovingReviewCount?: number; + restrictions?: { + users: string[]; + teams: string[]; + apps?: string[]; + }; requiredStatusCheckContexts?: string[]; requireBranchesToBeUpToDate?: boolean; requiredConversationResolution?: boolean; @@ -83,6 +89,7 @@ export function createGithubRepoCreateAction(options: { hasIssues?: boolean; token?: string; topics?: string[]; + requireCommitSigning?: boolean; }>({ id: 'github:repo:create', description: 'Creates a GitHub repository.', @@ -97,6 +104,8 @@ export function createGithubRepoCreateAction(options: { access: inputProps.access, requireCodeOwnerReviews: inputProps.requireCodeOwnerReviews, bypassPullRequestAllowances: inputProps.bypassPullRequestAllowances, + requiredApprovingReviewCount: inputProps.requiredApprovingReviewCount, + restrictions: inputProps.restrictions, requiredStatusCheckContexts: inputProps.requiredStatusCheckContexts, requireBranchesToBeUpToDate: inputProps.requireBranchesToBeUpToDate, requiredConversationResolution: @@ -115,6 +124,7 @@ export function createGithubRepoCreateAction(options: { hasIssues: inputProps.hasIssues, token: inputProps.token, topics: inputProps.topics, + requiredCommitSigning: inputProps.requiredCommitSigning, }, }, output: { diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.test.ts index f8b45093e6..24777163d4 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.test.ts @@ -287,6 +287,10 @@ describe('github:repo:push', () => { requiredConversationResolution: false, enforceAdmins: true, dismissStaleReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + requiredCommitSigning: false, + restrictions: undefined, }); await action.handler({ @@ -309,6 +313,10 @@ describe('github:repo:push', () => { requiredConversationResolution: false, enforceAdmins: true, dismissStaleReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + requiredCommitSigning: false, + restrictions: undefined, }); await action.handler({ @@ -331,6 +339,10 @@ describe('github:repo:push', () => { requiredConversationResolution: false, enforceAdmins: true, dismissStaleReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + requiredCommitSigning: false, + restrictions: undefined, }); }); @@ -356,6 +368,10 @@ describe('github:repo:push', () => { requiredConversationResolution: false, enforceAdmins: true, dismissStaleReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + requiredCommitSigning: false, + restrictions: undefined, }); await action.handler({ @@ -378,6 +394,10 @@ describe('github:repo:push', () => { requiredConversationResolution: false, enforceAdmins: true, dismissStaleReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + requiredCommitSigning: false, + restrictions: undefined, }); await action.handler({ @@ -400,6 +420,10 @@ describe('github:repo:push', () => { requiredConversationResolution: false, enforceAdmins: false, dismissStaleReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + requiredCommitSigning: false, + restrictions: undefined, }); }); @@ -425,6 +449,10 @@ describe('github:repo:push', () => { requiredConversationResolution: false, enforceAdmins: true, dismissStaleReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + requiredCommitSigning: false, + restrictions: undefined, }); await action.handler({ @@ -449,6 +477,10 @@ describe('github:repo:push', () => { requiredConversationResolution: false, enforceAdmins: true, dismissStaleReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + requiredCommitSigning: false, + restrictions: undefined, }); await action.handler({ @@ -472,6 +504,10 @@ describe('github:repo:push', () => { requiredConversationResolution: false, enforceAdmins: true, dismissStaleReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + requiredCommitSigning: false, + restrictions: undefined, }); await action.handler({ @@ -496,6 +532,10 @@ describe('github:repo:push', () => { requiredConversationResolution: false, enforceAdmins: true, dismissStaleReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + requiredCommitSigning: false, + restrictions: undefined, }); }); @@ -540,6 +580,10 @@ describe('github:repo:push', () => { requiredConversationResolution: false, enforceAdmins: true, dismissStaleReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + requiredCommitSigning: false, + restrictions: undefined, }); await action.handler({ @@ -562,6 +606,10 @@ describe('github:repo:push', () => { requiredConversationResolution: false, enforceAdmins: true, dismissStaleReviews: true, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + requiredCommitSigning: false, + restrictions: undefined, }); await action.handler({ @@ -584,6 +632,10 @@ describe('github:repo:push', () => { requiredConversationResolution: false, enforceAdmins: true, dismissStaleReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + requiredCommitSigning: false, + restrictions: undefined, }); }); @@ -609,6 +661,10 @@ describe('github:repo:push', () => { requiredConversationResolution: false, enforceAdmins: true, dismissStaleReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + requiredCommitSigning: false, + restrictions: undefined, }); await action.handler({ @@ -631,6 +687,10 @@ describe('github:repo:push', () => { requiredConversationResolution: true, enforceAdmins: true, dismissStaleReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + requiredCommitSigning: false, + restrictions: undefined, }); await action.handler({ @@ -653,6 +713,10 @@ describe('github:repo:push', () => { requiredConversationResolution: false, enforceAdmins: true, dismissStaleReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + requiredCommitSigning: false, + restrictions: undefined, }); }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.ts index 9d470790fa..5a5ffc015c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.ts @@ -57,11 +57,20 @@ export function createGithubRepoPushAction(options: { apps?: string[]; } | undefined; + requiredApprovingReviewCount?: number; + restrictions?: + | { + users: string[]; + teams: string[]; + apps?: string[]; + } + | undefined; requiredStatusCheckContexts?: string[]; requireBranchesToBeUpToDate?: boolean; requiredConversationResolution?: boolean; sourcePath?: string; token?: string; + requiredCommitSigning?: boolean; }>({ id: 'github:repo:push', description: @@ -76,6 +85,8 @@ export function createGithubRepoPushAction(options: { dismissStaleReviews: inputProps.dismissStaleReviews, requiredStatusCheckContexts: inputProps.requiredStatusCheckContexts, bypassPullRequestAllowances: inputProps.bypassPullRequestAllowances, + requiredApprovingReviewCount: inputProps.requiredApprovingReviewCount, + restrictions: inputProps.restrictions, requireBranchesToBeUpToDate: inputProps.requireBranchesToBeUpToDate, requiredConversationResolution: inputProps.requiredConversationResolution, @@ -87,6 +98,7 @@ export function createGithubRepoPushAction(options: { gitAuthorEmail: inputProps.gitAuthorEmail, sourcePath: inputProps.sourcePath, token: inputProps.token, + requiredCommitSigning: inputProps.requiredCommitSigning, }, }, output: { @@ -109,10 +121,13 @@ export function createGithubRepoPushAction(options: { requireCodeOwnerReviews = false, dismissStaleReviews = false, bypassPullRequestAllowances, + requiredApprovingReviewCount = 1, + restrictions, requiredStatusCheckContexts = [], requireBranchesToBeUpToDate = true, requiredConversationResolution = false, token: providedToken, + requiredCommitSigning = false, } = ctx.input; const { owner, repo } = parseRepoUrl(repoUrl, integrations); @@ -148,6 +163,8 @@ export function createGithubRepoPushAction(options: { repo, requireCodeOwnerReviews, bypassPullRequestAllowances, + requiredApprovingReviewCount, + restrictions, requiredStatusCheckContexts, requireBranchesToBeUpToDate, requiredConversationResolution, @@ -157,6 +174,7 @@ export function createGithubRepoPushAction(options: { gitAuthorName, gitAuthorEmail, dismissStaleReviews, + requiredCommitSigning, ); ctx.output('remoteUrl', remoteUrl); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts index 9caee49f98..dae5e16374 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts @@ -271,6 +271,14 @@ export async function initRepoPushAndProtect( apps?: string[]; } | undefined, + requiredApprovingReviewCount: number, + restrictions: + | { + users: string[]; + teams: string[]; + apps?: string[]; + } + | undefined, requiredStatusCheckContexts: string[], requireBranchesToBeUpToDate: boolean, requiredConversationResolution: boolean, @@ -280,6 +288,7 @@ export async function initRepoPushAndProtect( gitAuthorName?: string, gitAuthorEmail?: string, dismissStaleReviews?: boolean, + requiredCommitSigning?: boolean, ) { const gitAuthorInfo = { name: gitAuthorName @@ -316,12 +325,15 @@ export async function initRepoPushAndProtect( logger, defaultBranch, bypassPullRequestAllowances, + requiredApprovingReviewCount, + restrictions, requireCodeOwnerReviews, requiredStatusCheckContexts, requireBranchesToBeUpToDate, requiredConversationResolution, enforceAdmins: protectEnforceAdmins, dismissStaleReviews: dismissStaleReviews, + requiredCommitSigning: requiredCommitSigning, }); } catch (e) { assertError(e); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/inputProperties.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/inputProperties.ts index 3fc7126754..b1286953a6 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/inputProperties.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/inputProperties.ts @@ -225,6 +225,46 @@ const sourcePath = { type: 'string', }; +const requiredApprovingReviewCount = { + title: 'Required approving review count', + type: 'number', + description: `Specify the number of reviewers required to approve pull requests. Use a number between 1 and 6 or 0 to not require reviewers. Defaults to 1.`, +}; + +const restrictions = { + title: 'Restrict who can push to the protected branch', + description: + 'Restrict who can push to the protected branch. User, app, and team restrictions are only available for organization-owned repositories.', + type: 'object', + additionalProperties: false, + properties: { + apps: { + type: 'array', + items: { + type: 'string', + }, + }, + users: { + type: 'array', + items: { + type: 'string', + }, + }, + teams: { + type: 'array', + items: { + type: 'string', + }, + }, + }, +}; + +const requiredCommitSigning = { + title: 'Require commit signing', + type: 'boolean', + description: `Require commit signing so that you must sign commits on this branch.`, +}; + export { access }; export { allowMergeCommit }; export { allowRebaseMerge }; @@ -243,6 +283,8 @@ export { homepage }; export { protectDefaultBranch }; export { protectEnforceAdmins }; export { bypassPullRequestAllowances }; +export { requiredApprovingReviewCount }; +export { restrictions }; export { repoUrl }; export { repoVisibility }; export { requireCodeOwnerReviews }; @@ -256,3 +298,4 @@ export { hasWiki }; export { sourcePath }; export { token }; export { topics }; +export { requiredCommitSigning }; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts index 74e5d4580b..8bed279a1b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts @@ -190,11 +190,18 @@ type BranchProtectionOptions = { teams?: string[]; apps?: string[]; }; + requiredApprovingReviewCount?: number; + restrictions?: { + users: string[]; + teams: string[]; + apps?: string[]; + }; requireBranchesToBeUpToDate?: boolean; requiredConversationResolution?: boolean; defaultBranch?: string; enforceAdmins?: boolean; dismissStaleReviews?: boolean; + requiredCommitSigning?: boolean; }; export const enableBranchProtectionOnDefaultRepoBranch = async ({ @@ -204,12 +211,15 @@ export const enableBranchProtectionOnDefaultRepoBranch = async ({ logger, requireCodeOwnerReviews, bypassPullRequestAllowances, + requiredApprovingReviewCount, + restrictions, requiredStatusCheckContexts = [], requireBranchesToBeUpToDate = true, requiredConversationResolution = false, defaultBranch = 'master', enforceAdmins = true, dismissStaleReviews = false, + requiredCommitSigning = false, }: BranchProtectionOptions): Promise => { const tryOnce = async () => { try { @@ -231,16 +241,24 @@ export const enableBranchProtectionOnDefaultRepoBranch = async ({ strict: requireBranchesToBeUpToDate, contexts: requiredStatusCheckContexts, }, - restrictions: null, + restrictions: restrictions ?? null, enforce_admins: enforceAdmins, required_pull_request_reviews: { - required_approving_review_count: 1, + required_approving_review_count: requiredApprovingReviewCount, require_code_owner_reviews: requireCodeOwnerReviews, bypass_pull_request_allowances: bypassPullRequestAllowances, dismiss_stale_reviews: dismissStaleReviews, }, required_conversation_resolution: requiredConversationResolution, }); + + if (requiredCommitSigning) { + await client.rest.repos.createCommitSignatureProtection({ + owner, + repo: repoName, + branch: defaultBranch, + }); + } } catch (e) { assertError(e); if ( diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts index e4c92c624e..c2681ed527 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts @@ -788,11 +788,15 @@ describe('publish:github', () => { logger: mockContext.logger, defaultBranch: 'master', requireCodeOwnerReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + restrictions: undefined, requiredStatusCheckContexts: [], requireBranchesToBeUpToDate: true, requiredConversationResolution: false, enforceAdmins: true, dismissStaleReviews: false, + requiredCommitSigning: false, }); await action.handler({ @@ -810,11 +814,15 @@ describe('publish:github', () => { logger: mockContext.logger, defaultBranch: 'master', requireCodeOwnerReviews: true, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + restrictions: undefined, requiredStatusCheckContexts: [], requireBranchesToBeUpToDate: true, requiredConversationResolution: false, enforceAdmins: true, dismissStaleReviews: false, + requiredCommitSigning: false, }); await action.handler({ @@ -832,11 +840,15 @@ describe('publish:github', () => { logger: mockContext.logger, defaultBranch: 'master', requireCodeOwnerReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + restrictions: undefined, requiredStatusCheckContexts: [], requireBranchesToBeUpToDate: true, requiredConversationResolution: false, enforceAdmins: true, dismissStaleReviews: false, + requiredCommitSigning: false, }); }); @@ -860,11 +872,15 @@ describe('publish:github', () => { logger: mockContext.logger, defaultBranch: 'master', requireCodeOwnerReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + restrictions: undefined, requiredStatusCheckContexts: [], requireBranchesToBeUpToDate: true, requiredConversationResolution: false, enforceAdmins: true, dismissStaleReviews: false, + requiredCommitSigning: false, }); await action.handler({ @@ -882,11 +898,15 @@ describe('publish:github', () => { logger: mockContext.logger, defaultBranch: 'master', requireCodeOwnerReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + restrictions: undefined, requiredStatusCheckContexts: [], requireBranchesToBeUpToDate: true, requiredConversationResolution: false, enforceAdmins: false, dismissStaleReviews: false, + requiredCommitSigning: false, }); await action.handler({ @@ -904,11 +924,15 @@ describe('publish:github', () => { logger: mockContext.logger, defaultBranch: 'master', requireCodeOwnerReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + restrictions: undefined, requiredStatusCheckContexts: [], requireBranchesToBeUpToDate: true, requiredConversationResolution: false, enforceAdmins: true, dismissStaleReviews: false, + requiredCommitSigning: false, }); }); @@ -932,11 +956,15 @@ describe('publish:github', () => { logger: mockContext.logger, defaultBranch: 'master', requireCodeOwnerReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + restrictions: undefined, requiredStatusCheckContexts: [], requireBranchesToBeUpToDate: true, requiredConversationResolution: false, enforceAdmins: true, dismissStaleReviews: false, + requiredCommitSigning: false, }); await action.handler({ @@ -956,11 +984,15 @@ describe('publish:github', () => { logger: mockContext.logger, defaultBranch: 'master', requireCodeOwnerReviews: false, - requiredStatusCheckContexts: ['statusCheck'], + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + restrictions: undefined, + requiredStatusCheckContexts: [], requireBranchesToBeUpToDate: true, requiredConversationResolution: false, enforceAdmins: true, dismissStaleReviews: false, + requiredCommitSigning: false, }); await action.handler({ @@ -979,11 +1011,15 @@ describe('publish:github', () => { logger: mockContext.logger, defaultBranch: 'master', requireCodeOwnerReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + restrictions: undefined, requiredStatusCheckContexts: ['statusCheck'], requireBranchesToBeUpToDate: false, requiredConversationResolution: false, enforceAdmins: true, dismissStaleReviews: false, + requiredCommitSigning: false, }); await action.handler({ @@ -1001,11 +1037,15 @@ describe('publish:github', () => { logger: mockContext.logger, defaultBranch: 'master', requireCodeOwnerReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + restrictions: undefined, requiredStatusCheckContexts: [], requireBranchesToBeUpToDate: true, requiredConversationResolution: false, enforceAdmins: true, dismissStaleReviews: false, + requiredCommitSigning: false, }); }); @@ -1084,11 +1124,15 @@ describe('publish:github', () => { logger: mockContext.logger, defaultBranch: 'master', requireCodeOwnerReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + restrictions: undefined, requiredStatusCheckContexts: [], requireBranchesToBeUpToDate: true, requiredConversationResolution: false, enforceAdmins: true, dismissStaleReviews: false, + requiredCommitSigning: false, }); await action.handler({ @@ -1106,11 +1150,15 @@ describe('publish:github', () => { logger: mockContext.logger, defaultBranch: 'master', requireCodeOwnerReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + restrictions: undefined, requiredStatusCheckContexts: [], requireBranchesToBeUpToDate: true, requiredConversationResolution: false, enforceAdmins: true, dismissStaleReviews: true, + requiredCommitSigning: false, }); await action.handler({ @@ -1128,11 +1176,15 @@ describe('publish:github', () => { logger: mockContext.logger, defaultBranch: 'master', requireCodeOwnerReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + restrictions: undefined, requiredStatusCheckContexts: [], requireBranchesToBeUpToDate: true, requiredConversationResolution: false, enforceAdmins: true, dismissStaleReviews: false, + requiredCommitSigning: false, }); }); it('should call enableBranchProtectionOnDefaultRepoBranch with the correct values of requiredConversationResolution', async () => { @@ -1155,11 +1207,15 @@ describe('publish:github', () => { logger: mockContext.logger, defaultBranch: 'master', requireCodeOwnerReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + restrictions: undefined, requiredStatusCheckContexts: [], requireBranchesToBeUpToDate: true, requiredConversationResolution: false, enforceAdmins: true, dismissStaleReviews: false, + requiredCommitSigning: false, }); await action.handler({ @@ -1177,11 +1233,15 @@ describe('publish:github', () => { logger: mockContext.logger, defaultBranch: 'master', requireCodeOwnerReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + restrictions: undefined, requiredStatusCheckContexts: [], requireBranchesToBeUpToDate: true, requiredConversationResolution: true, enforceAdmins: true, dismissStaleReviews: false, + requiredCommitSigning: false, }); await action.handler({ @@ -1199,11 +1259,567 @@ describe('publish:github', () => { logger: mockContext.logger, defaultBranch: 'master', requireCodeOwnerReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + restrictions: undefined, requiredStatusCheckContexts: [], requireBranchesToBeUpToDate: true, requiredConversationResolution: false, enforceAdmins: true, dismissStaleReviews: false, + requiredCommitSigning: false, + }); + }); + it('should call enableBranchProtectionOnDefaultRepoBranch with the correct values of requiredApprovingReviewCount', async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'User' }, + }); + + mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ + data: { + name: 'repo', + }, + }); + + await action.handler(mockContext); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockOctokit, + repoName: 'repo', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + restrictions: undefined, + requiredStatusCheckContexts: [], + requireBranchesToBeUpToDate: true, + requiredConversationResolution: false, + enforceAdmins: true, + dismissStaleReviews: false, + requiredCommitSigning: false, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + requiredApprovingReviewCount: 2, + }, + }); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockOctokit, + repoName: 'repo', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 2, + restrictions: undefined, + requiredStatusCheckContexts: [], + requireBranchesToBeUpToDate: true, + requiredConversationResolution: false, + enforceAdmins: true, + dismissStaleReviews: false, + requiredCommitSigning: false, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + requiredApprovingReviewCount: 0, + }, + }); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockOctokit, + repoName: 'repo', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 0, + restrictions: undefined, + requiredStatusCheckContexts: [], + requireBranchesToBeUpToDate: true, + requiredConversationResolution: false, + enforceAdmins: true, + dismissStaleReviews: false, + requiredCommitSigning: false, + }); + }); + it('should call enableBranchProtectionOnDefaultRepoBranch with the correct values of restrictions', async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'User' }, + }); + + mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ + data: { + name: 'repo', + }, + }); + + await action.handler(mockContext); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockOctokit, + repoName: 'repo', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + restrictions: undefined, + requiredStatusCheckContexts: [], + requireBranchesToBeUpToDate: true, + requiredConversationResolution: false, + enforceAdmins: true, + dismissStaleReviews: false, + requiredCommitSigning: false, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + restrictions: { + users: ['user'], + teams: [], + }, + }, + }); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockOctokit, + repoName: 'repo', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + restrictions: { + users: ['user'], + teams: [], + }, + requiredStatusCheckContexts: [], + requireBranchesToBeUpToDate: true, + requiredConversationResolution: false, + enforceAdmins: true, + dismissStaleReviews: false, + requiredCommitSigning: false, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + restrictions: { + users: [], + teams: ['team'], + }, + }, + }); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockOctokit, + repoName: 'repo', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + restrictions: { + users: [], + teams: ['team'], + }, + requiredStatusCheckContexts: [], + requireBranchesToBeUpToDate: true, + requiredConversationResolution: false, + enforceAdmins: true, + dismissStaleReviews: false, + requiredCommitSigning: false, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + restrictions: { + users: [], + teams: [], + apps: ['app'], + }, + }, + }); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockOctokit, + repoName: 'repo', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + restrictions: { + users: [], + teams: [], + apps: ['app'], + }, + requiredStatusCheckContexts: [], + requireBranchesToBeUpToDate: true, + requiredConversationResolution: false, + enforceAdmins: true, + dismissStaleReviews: false, + requiredCommitSigning: false, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + restrictions: { + users: ['user'], + teams: ['team'], + apps: ['app'], + }, + }, + }); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockOctokit, + repoName: 'repo', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + restrictions: { + users: ['user'], + teams: ['team'], + apps: ['app'], + }, + requiredStatusCheckContexts: [], + requireBranchesToBeUpToDate: true, + requiredConversationResolution: false, + enforceAdmins: true, + dismissStaleReviews: false, + requiredCommitSigning: false, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + restrictions: { + users: ['user1', 'user2'], + teams: ['team1', 'team2'], + apps: ['app1', 'app2'], + }, + }, + }); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockOctokit, + repoName: 'repo', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + restrictions: { + users: ['user1', 'user2'], + teams: ['team1', 'team2'], + apps: ['app1', 'app2'], + }, + requiredStatusCheckContexts: [], + requireBranchesToBeUpToDate: true, + requiredConversationResolution: false, + enforceAdmins: true, + dismissStaleReviews: false, + requiredCommitSigning: false, + }); + }); + it('should call enableBranchProtectionOnDefaultRepoBranch with the correct values of requiredCommitSigning', async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'User' }, + }); + + mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ + data: { + name: 'repo', + }, + }); + + await action.handler(mockContext); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockOctokit, + repoName: 'repo', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + restrictions: undefined, + requiredStatusCheckContexts: [], + requireBranchesToBeUpToDate: true, + requiredConversationResolution: false, + enforceAdmins: true, + dismissStaleReviews: false, + requiredCommitSigning: false, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + requiredCommitSigning: false, + }, + }); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockOctokit, + repoName: 'repo', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + restrictions: undefined, + requiredStatusCheckContexts: [], + requireBranchesToBeUpToDate: true, + requiredConversationResolution: false, + enforceAdmins: true, + dismissStaleReviews: false, + requiredCommitSigning: false, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + requiredCommitSigning: true, + }, + }); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockOctokit, + repoName: 'repo', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + restrictions: undefined, + requiredStatusCheckContexts: [], + requireBranchesToBeUpToDate: true, + requiredConversationResolution: false, + enforceAdmins: true, + dismissStaleReviews: false, + requiredCommitSigning: true, + }); + }); + it('should call enableBranchProtectionOnDefaultRepoBranch with the correct values of bypassPullRequestAllowances', async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'User' }, + }); + + mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ + data: { + name: 'repo', + }, + }); + + await action.handler(mockContext); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockOctokit, + repoName: 'repo', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + restrictions: undefined, + requiredStatusCheckContexts: [], + requireBranchesToBeUpToDate: true, + requiredConversationResolution: false, + enforceAdmins: true, + dismissStaleReviews: false, + requiredCommitSigning: false, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + bypassPullRequestAllowances: { + users: ['user'], + }, + }, + }); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockOctokit, + repoName: 'repo', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: false, + bypassPullRequestAllowances: { + users: ['user'], + }, + requiredApprovingReviewCount: 1, + restrictions: undefined, + requiredStatusCheckContexts: [], + requireBranchesToBeUpToDate: true, + requiredConversationResolution: false, + enforceAdmins: true, + dismissStaleReviews: false, + requiredCommitSigning: false, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + bypassPullRequestAllowances: { + teams: ['team'], + }, + }, + }); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockOctokit, + repoName: 'repo', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: false, + bypassPullRequestAllowances: { + teams: ['team'], + }, + requiredApprovingReviewCount: 1, + restrictions: undefined, + requiredStatusCheckContexts: [], + requireBranchesToBeUpToDate: true, + requiredConversationResolution: false, + enforceAdmins: true, + dismissStaleReviews: false, + requiredCommitSigning: false, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + bypassPullRequestAllowances: { + apps: ['app'], + }, + }, + }); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockOctokit, + repoName: 'repo', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: false, + bypassPullRequestAllowances: { + apps: ['app'], + }, + requiredApprovingReviewCount: 1, + restrictions: undefined, + requiredStatusCheckContexts: [], + requireBranchesToBeUpToDate: true, + requiredConversationResolution: false, + enforceAdmins: true, + dismissStaleReviews: false, + requiredCommitSigning: false, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + bypassPullRequestAllowances: { + users: ['user'], + teams: ['team'], + apps: ['app'], + }, + }, + }); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockOctokit, + repoName: 'repo', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: false, + bypassPullRequestAllowances: { + users: ['user'], + teams: ['team'], + apps: ['app'], + }, + requiredApprovingReviewCount: 1, + restrictions: undefined, + requiredStatusCheckContexts: [], + requireBranchesToBeUpToDate: true, + requiredConversationResolution: false, + enforceAdmins: true, + dismissStaleReviews: false, + requiredCommitSigning: false, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + bypassPullRequestAllowances: { + users: ['user1', 'user2'], + teams: ['team1', 'team2'], + apps: ['app1', 'app2'], + }, + }, + }); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockOctokit, + repoName: 'repo', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: false, + bypassPullRequestAllowances: { + users: ['user1', 'user2'], + teams: ['team1', 'team2'], + apps: ['app1', 'app2'], + }, + requiredApprovingReviewCount: 1, + restrictions: undefined, + requiredStatusCheckContexts: [], + requireBranchesToBeUpToDate: true, + requiredConversationResolution: false, + enforceAdmins: true, + dismissStaleReviews: false, + requiredCommitSigning: false, }); }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts index 5430f30657..add6fcc79c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts @@ -69,6 +69,14 @@ export function createPublishGithubAction(options: { apps?: string[]; } | undefined; + requiredApprovingReviewCount?: number; + restrictions?: + | { + users: string[]; + teams: string[]; + apps?: string[]; + } + | undefined; requireCodeOwnerReviews?: boolean; dismissStaleReviews?: boolean; requiredStatusCheckContexts?: string[]; @@ -95,6 +103,7 @@ export function createPublishGithubAction(options: { hasIssues?: boolean | undefined; token?: string; topics?: string[]; + requiredCommitSigning?: boolean; }>({ id: 'publish:github', description: @@ -109,6 +118,8 @@ export function createPublishGithubAction(options: { homepage: inputProps.homepage, access: inputProps.access, bypassPullRequestAllowances: inputProps.bypassPullRequestAllowances, + requiredApprovingReviewCount: inputProps.requiredApprovingReviewCount, + restrictions: inputProps.restrictions, requireCodeOwnerReviews: inputProps.requireCodeOwnerReviews, dismissStaleReviews: inputProps.dismissStaleReviews, requiredStatusCheckContexts: inputProps.requiredStatusCheckContexts, @@ -136,6 +147,7 @@ export function createPublishGithubAction(options: { hasIssues: inputProps.hasIssues, token: inputProps.token, topics: inputProps.topics, + requiredCommitSigning: inputProps.requiredCommitSigning, }, }, output: { @@ -155,6 +167,8 @@ export function createPublishGithubAction(options: { requireCodeOwnerReviews = false, dismissStaleReviews = false, bypassPullRequestAllowances, + requiredApprovingReviewCount = 1, + restrictions, requiredStatusCheckContexts = [], requireBranchesToBeUpToDate = true, requiredConversationResolution = false, @@ -178,6 +192,7 @@ export function createPublishGithubAction(options: { hasIssues = undefined, topics, token: providedToken, + requiredCommitSigning = false, } = ctx.input; const octokitOptions = await getOctokitOptions({ @@ -233,6 +248,8 @@ export function createPublishGithubAction(options: { repo, requireCodeOwnerReviews, bypassPullRequestAllowances, + requiredApprovingReviewCount, + restrictions, requiredStatusCheckContexts, requireBranchesToBeUpToDate, requiredConversationResolution, @@ -242,6 +259,7 @@ export function createPublishGithubAction(options: { gitAuthorName, gitAuthorEmail, dismissStaleReviews, + requiredCommitSigning, ); ctx.output('remoteUrl', remoteUrl);