diff --git a/.changeset/chatty-files-sniff.md b/.changeset/chatty-files-sniff.md new file mode 100644 index 0000000000..11eebbb93b --- /dev/null +++ b/.changeset/chatty-files-sniff.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-github': patch +--- + +adding requiredLinearHistory property for branch protection settings diff --git a/plugins/catalog-backend/src/service/createRouter.test.ts b/plugins/catalog-backend/src/service/createRouter.test.ts index 53f831d320..2d158f7f16 100644 --- a/plugins/catalog-backend/src/service/createRouter.test.ts +++ b/plugins/catalog-backend/src/service/createRouter.test.ts @@ -53,7 +53,7 @@ describe('createRouter readonly disabled', () => { let locationAnalyzer: jest.Mocked; let permissionsService: jest.Mocked; - beforeAll(async () => { + beforeEach(async () => { entitiesCatalog = { entities: jest.fn(), entitiesBatch: jest.fn(), @@ -135,6 +135,38 @@ describe('createRouter readonly disabled', () => { { apiVersion: 'a', kind: 'b', metadata: { name: 'n' } }, ]; + entitiesCatalog.entities.mockResolvedValueOnce({ + entities: { type: 'object', entities: [entities[0]] }, + pageInfo: { hasNextPage: false }, + }); + + const response = await request(app).get('/entities'); + + expect(response.status).toEqual(200); + expect(response.body).toEqual(entities); + }); + + it('happy path: lists entities when by-entities emulation is enabled', async () => { + const router = await createRouter({ + entitiesCatalog, + locationService, + orchestrator, + logger: mockServices.logger.mock(), + refreshService, + config: new ConfigReader(undefined), + permissionIntegrationRouter: express.Router(), + auth: mockServices.auth(), + httpAuth: mockServices.httpAuth(), + locationAnalyzer, + permissionsService: permissionsService, + disableRelationsCompatibility: true, // added + }); + app = await wrapServer(express().use(router)); + + const entities: Entity[] = [ + { apiVersion: 'a', kind: 'b', metadata: { name: 'n' } }, + ]; + entitiesCatalog.queryEntities.mockResolvedValueOnce({ items: { type: 'object', entities: [entities[0]] }, pageInfo: {}, @@ -148,6 +180,49 @@ describe('createRouter readonly disabled', () => { }); it('parses single and multiple request parameters and passes them down', async () => { + entitiesCatalog.entities.mockResolvedValueOnce({ + entities: { type: 'object', entities: [] }, + pageInfo: { hasNextPage: false }, + }); + const response = await request(app).get( + '/entities?filter=a=1,a=2,b=3&filter=c=4', + ); + + expect(response.status).toEqual(200); + expect(entitiesCatalog.entities).toHaveBeenCalledTimes(1); + expect(entitiesCatalog.entities).toHaveBeenCalledWith({ + filter: { + anyOf: [ + { + allOf: [ + { key: 'a', values: ['1', '2'] }, + { key: 'b', values: ['3'] }, + ], + }, + { key: 'c', values: ['4'] }, + ], + }, + credentials: mockCredentials.user(), + }); + }); + + it('parses single and multiple request parameters and passes them down when by-entities emulation is enabled', async () => { + const router = await createRouter({ + entitiesCatalog, + locationService, + orchestrator, + logger: mockServices.logger.mock(), + refreshService, + config: new ConfigReader(undefined), + permissionIntegrationRouter: express.Router(), + auth: mockServices.auth(), + httpAuth: mockServices.httpAuth(), + locationAnalyzer, + permissionsService: permissionsService, + disableRelationsCompatibility: true, // added + }); + app = await wrapServer(express().use(router)); + entitiesCatalog.queryEntities.mockResolvedValueOnce({ items: { type: 'object', entities: [] }, pageInfo: {}, diff --git a/plugins/catalog-backend/src/service/createRouter.ts b/plugins/catalog-backend/src/service/createRouter.ts index 601b0b1098..58474f4dbc 100644 --- a/plugins/catalog-backend/src/service/createRouter.ts +++ b/plugins/catalog-backend/src/service/createRouter.ts @@ -43,7 +43,6 @@ import { LocationService, RefreshService } from './types'; import { disallowReadonlyMode, encodeCursor, - expandLegacyCompoundRelationsInEntity, locationInput, validateRequestBody, } from './util'; @@ -58,10 +57,8 @@ import { } from '@backstage/backend-plugin-api'; import { LocationAnalyzer } from '@backstage/plugin-catalog-node'; import { AuthorizedValidationService } from './AuthorizedValidationService'; -import { DeferredPromise, createDeferred } from '@backstage/types'; import { createEntityArrayJsonStream, - processEntitiesResponseItems, writeEntitiesResponse, writeSingleEntityResponse, } from './response'; @@ -154,7 +151,7 @@ export async function createRouter( // When pagination parameters are passed in, use the legacy slow path // that loads all entities into memory - if (pagination) { + if (pagination || disableRelationsCompatibility !== true) { const { entities, pageInfo } = await entitiesCatalog.entities({ filter, fields, @@ -175,22 +172,6 @@ export async function createRouter( return; } - // For other read-the-entire-world cases, use queryEntities and stream - // out results. - - // The write lock is used for back pressure, preventing slow readers - // from forcing our read loop to pile up response data in userspace - // buffers faster than the kernel buffer is emptied. - // https://nodejs.org/api/http.html#http_response_write_chunk_encoding_callback - const locks: { writeLock?: DeferredPromise } = {}; - const controller = new AbortController(); - const signal = controller.signal; - req.on('end', () => { - controller.abort(new Error('Client closed connection')); - locks.writeLock?.resolve(); - delete locks.writeLock; - }); - const responseStream = createEntityArrayJsonStream(res); const limit = 10000; let cursor: Cursor | undefined; @@ -211,31 +192,11 @@ export async function createRouter( ); if (result.items.entities.length) { - await locks?.writeLock; - - signal.throwIfAborted(); - - if (!disableRelationsCompatibility) { - result.items = processEntitiesResponseItems( - result.items, - expandLegacyCompoundRelationsInEntity, - ); - } - if (!responseStream.send(result.items)) { - // The kernel buffer is full. Create the lock but do not await it - // yet - we can better spend our time going to the next round of - // the loop and read from the database while we wait for it to - // drain. - locks.writeLock = createDeferred(); - res.once('drain', () => { - locks.writeLock?.resolve(); - delete locks.writeLock; - }); + if (await responseStream.send(result.items)) { + return; // Client closed connection } } - signal.throwIfAborted(); - cursor = result.pageInfo?.nextCursor; } while (cursor); diff --git a/plugins/catalog-backend/src/service/response/createEntityArrayJsonStream.ts b/plugins/catalog-backend/src/service/response/createEntityArrayJsonStream.ts index 4762afd2d9..5bdc9d8994 100644 --- a/plugins/catalog-backend/src/service/response/createEntityArrayJsonStream.ts +++ b/plugins/catalog-backend/src/service/response/createEntityArrayJsonStream.ts @@ -16,9 +16,10 @@ import { EntitiesResponseItems } from '../../catalog/types'; import { Response } from 'express'; +import { writeResponseData } from './write'; export interface EntityArrayJsonStream { - send(entities: EntitiesResponseItems): boolean; + send(entities: EntitiesResponseItems): Promise; complete(): void; close(): void; } @@ -33,7 +34,7 @@ export function createEntityArrayJsonStream( let completed = false; return { - send(response) { + async send(response) { if (firstSend) { res.setHeader('Content-Type', 'application/json; charset=utf-8'); res.status(200); @@ -41,13 +42,15 @@ export function createEntityArrayJsonStream( } if (response.type === 'raw') { - let needsDrain = false; for (const item of response.entities) { const prefix = firstSend ? '[' : ','; firstSend = false; - needsDrain ||= !res.write(prefix + item, 'utf8'); + + if (await writeResponseData(res, prefix + item)) { + return true; + } } - return !needsDrain; + return false; } let data: string; @@ -60,7 +63,7 @@ export function createEntityArrayJsonStream( } firstSend = false; - return res.write(data, 'utf8'); + return writeResponseData(res, data); }, complete() { if (firstSend) { diff --git a/plugins/catalog-backend/src/service/response/write.ts b/plugins/catalog-backend/src/service/response/write.ts index c2bca0953a..31f14eea7f 100644 --- a/plugins/catalog-backend/src/service/response/write.ts +++ b/plugins/catalog-backend/src/service/response/write.ts @@ -78,26 +78,40 @@ export async function writeEntitiesResponse( const prefix = first ? '[' : ','; first = false; - const needsDrain = !res.write(prefix + entity, 'utf8'); - if (needsDrain) { - const closed = await new Promise(resolve => { - function onContinue() { - res.off('drain', onContinue); - res.off('close', onClose); - resolve(false); - } - function onClose() { - res.off('drain', onContinue); - res.off('close', onClose); - resolve(true); - } - res.on('drain', onContinue); - res.on('close', onClose); - }); - if (closed) { - return; - } + if (await writeResponseData(res, prefix + entity)) { + return; } } res.end(`${first ? '[' : ''}]${trailing}`); } + +/** + * Writes a data to the response and waits if the response buffer needs draining. + * + * @internal + * @returns true if the response was closed while waiting for the buffer to drain + */ +export async function writeResponseData(res: Response, data: string | Buffer) { + const ok = res.write(data, 'utf8'); + if (!ok) { + if (!res.writableNeedDrain) { + return true; + } + const closed = await new Promise(resolve => { + function onContinue() { + res.off('drain', onContinue); + res.off('close', onClose); + resolve(false); + } + function onClose() { + res.off('drain', onContinue); + res.off('close', onClose); + resolve(true); + } + res.on('drain', onContinue); + res.on('close', onClose); + }); + return closed; + } + return false; +} diff --git a/plugins/scaffolder-backend-module-github/report.api.md b/plugins/scaffolder-backend-module-github/report.api.md index e954384444..60ed9db5ef 100644 --- a/plugins/scaffolder-backend-module-github/report.api.md +++ b/plugins/scaffolder-backend-module-github/report.api.md @@ -80,6 +80,7 @@ export function createGithubBranchProtectionAction(options: { requiredConversationResolution?: boolean | undefined; requireLastPushApproval?: boolean | undefined; requiredCommitSigning?: boolean | undefined; + requiredLinearHistory?: boolean | undefined; token?: string | undefined; }, JsonObject @@ -272,6 +273,7 @@ export function createGithubRepoCreateAction(options: { } | undefined; requireCommitSigning?: boolean | undefined; + requiredLinearHistory?: boolean | undefined; customProperties?: | { [key: string]: string; @@ -319,6 +321,7 @@ export function createGithubRepoPushAction(options: { sourcePath?: string | undefined; token?: string | undefined; requiredCommitSigning?: boolean | undefined; + requiredLinearHistory?: boolean | undefined; requireLastPushApproval?: boolean | undefined; }, JsonObject @@ -432,6 +435,7 @@ export function createPublishGithubAction(options: { } | undefined; requiredCommitSigning?: boolean | undefined; + requiredLinearHistory?: boolean | undefined; customProperties?: | { [key: string]: string; diff --git a/plugins/scaffolder-backend-module-github/src/actions/gitHelpers.ts b/plugins/scaffolder-backend-module-github/src/actions/gitHelpers.ts index 9989a80724..c720690ee6 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/gitHelpers.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/gitHelpers.ts @@ -43,6 +43,7 @@ type BranchProtectionOptions = { enforceAdmins?: boolean; dismissStaleReviews?: boolean; requiredCommitSigning?: boolean; + requiredLinearHistory?: boolean; }; export const enableBranchProtectionOnDefaultRepoBranch = async ({ @@ -62,6 +63,7 @@ export const enableBranchProtectionOnDefaultRepoBranch = async ({ enforceAdmins = true, dismissStaleReviews = false, requiredCommitSigning = false, + requiredLinearHistory = false, }: BranchProtectionOptions): Promise => { const tryOnce = async () => { try { @@ -93,6 +95,7 @@ export const enableBranchProtectionOnDefaultRepoBranch = async ({ require_last_push_approval: requireLastPushApproval, }, required_conversation_resolution: requiredConversationResolution, + required_linear_history: requiredLinearHistory, }); if (requiredCommitSigning) { diff --git a/plugins/scaffolder-backend-module-github/src/actions/github.test.ts b/plugins/scaffolder-backend-module-github/src/actions/github.test.ts index 3af922eb4f..f2f36e263a 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/github.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/github.test.ts @@ -1097,6 +1097,7 @@ describe('publish:github', () => { enforceAdmins: true, dismissStaleReviews: false, requiredCommitSigning: false, + requiredLinearHistory: false, }); await action.handler({ @@ -1126,6 +1127,7 @@ describe('publish:github', () => { enforceAdmins: true, dismissStaleReviews: false, requiredCommitSigning: false, + requiredLinearHistory: false, }); await action.handler({ @@ -1154,6 +1156,7 @@ describe('publish:github', () => { enforceAdmins: true, dismissStaleReviews: false, requiredCommitSigning: false, + requiredLinearHistory: false, }); await action.handler({ @@ -1181,6 +1184,7 @@ describe('publish:github', () => { enforceAdmins: true, dismissStaleReviews: false, requiredCommitSigning: false, + requiredLinearHistory: false, }); }); @@ -1269,6 +1273,7 @@ describe('publish:github', () => { enforceAdmins: true, dismissStaleReviews: false, requiredCommitSigning: false, + requiredLinearHistory: false, }); await action.handler({ @@ -1302,6 +1307,7 @@ describe('publish:github', () => { enforceAdmins: true, dismissStaleReviews: false, requiredCommitSigning: false, + requiredLinearHistory: false, }); await action.handler({ @@ -1335,6 +1341,7 @@ describe('publish:github', () => { enforceAdmins: true, dismissStaleReviews: false, requiredCommitSigning: false, + requiredLinearHistory: false, }); await action.handler({ @@ -1370,6 +1377,7 @@ describe('publish:github', () => { enforceAdmins: true, dismissStaleReviews: false, requiredCommitSigning: false, + requiredLinearHistory: false, }); await action.handler({ @@ -1405,6 +1413,7 @@ describe('publish:github', () => { enforceAdmins: true, dismissStaleReviews: false, requiredCommitSigning: false, + requiredLinearHistory: false, }); await action.handler({ @@ -1440,6 +1449,7 @@ describe('publish:github', () => { enforceAdmins: true, dismissStaleReviews: false, requiredCommitSigning: false, + requiredLinearHistory: false, }); }); it('should call enableBranchProtectionOnDefaultRepoBranch with the correct values of bypassPullRequestAllowances', async () => { @@ -1472,6 +1482,7 @@ describe('publish:github', () => { enforceAdmins: true, dismissStaleReviews: false, requiredCommitSigning: false, + requiredLinearHistory: false, }); await action.handler({ @@ -1503,6 +1514,7 @@ describe('publish:github', () => { enforceAdmins: true, dismissStaleReviews: false, requiredCommitSigning: false, + requiredLinearHistory: false, }); await action.handler({ @@ -1534,6 +1546,7 @@ describe('publish:github', () => { enforceAdmins: true, dismissStaleReviews: false, requiredCommitSigning: false, + requiredLinearHistory: false, }); await action.handler({ @@ -1565,6 +1578,7 @@ describe('publish:github', () => { enforceAdmins: true, dismissStaleReviews: false, requiredCommitSigning: false, + requiredLinearHistory: false, }); await action.handler({ @@ -1600,6 +1614,7 @@ describe('publish:github', () => { enforceAdmins: true, dismissStaleReviews: false, requiredCommitSigning: false, + requiredLinearHistory: false, }); await action.handler({ @@ -1635,6 +1650,7 @@ describe('publish:github', () => { enforceAdmins: true, dismissStaleReviews: false, requiredCommitSigning: false, + requiredLinearHistory: false, }); }); @@ -1664,6 +1680,11 @@ describe('publish:github', () => { defaultValue: false, overrideValue: true, }, + { + inputProperty: 'requiredLinearHistory', + defaultValue: false, + overrideValue: true, + }, { inputProperty: 'protectEnforceAdmins', defaultValue: true, @@ -1712,6 +1733,7 @@ describe('publish:github', () => { enforceAdmins: true, dismissStaleReviews: false, requiredCommitSigning: false, + requiredLinearHistory: false, [octokitParameter || inputProperty]: defaultValue, }); @@ -1740,6 +1762,7 @@ describe('publish:github', () => { enforceAdmins: true, dismissStaleReviews: false, requiredCommitSigning: false, + requiredLinearHistory: false, [octokitParameter || inputProperty]: overrideValue, }); @@ -1768,6 +1791,7 @@ describe('publish:github', () => { enforceAdmins: true, dismissStaleReviews: false, requiredCommitSigning: false, + requiredLinearHistory: false, [octokitParameter || inputProperty]: defaultValue, }); }, diff --git a/plugins/scaffolder-backend-module-github/src/actions/github.ts b/plugins/scaffolder-backend-module-github/src/actions/github.ts index 982c68d80f..6d8e0046c2 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/github.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/github.ts @@ -115,6 +115,7 @@ export function createPublishGithubAction(options: { includeClaimKeys?: string[]; }; requiredCommitSigning?: boolean; + requiredLinearHistory?: boolean; customProperties?: { [key: string]: string }; }>({ id: 'publish:github', @@ -165,6 +166,7 @@ export function createPublishGithubAction(options: { secrets: inputProps.secrets, oidcCustomization: inputProps.oidcCustomization, requiredCommitSigning: inputProps.requiredCommitSigning, + requiredLinearHistory: inputProps.requiredLinearHistory, customProperties: inputProps.customProperties, }, }, @@ -217,6 +219,7 @@ export function createPublishGithubAction(options: { token: providedToken, customProperties, requiredCommitSigning = false, + requiredLinearHistory = false, } = ctx.input; const octokitOptions = await getOctokitOptions({ @@ -289,6 +292,7 @@ export function createPublishGithubAction(options: { gitAuthorEmail, dismissStaleReviews, requiredCommitSigning, + requiredLinearHistory, ); ctx.output('commitHash', commitResult?.commitHash); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.examples.test.ts index 5443d7304c..14ebc9a1cd 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.examples.test.ts @@ -98,6 +98,7 @@ describe('github:branch-protection:create', () => { require_last_push_approval: false, }, required_conversation_resolution: false, + required_linear_history: false, }); expect( mockOctokit.rest.repos.createCommitSignatureProtection, @@ -130,6 +131,7 @@ describe('github:branch-protection:create', () => { require_last_push_approval: false, }, required_conversation_resolution: false, + required_linear_history: false, }); expect( mockOctokit.rest.repos.createCommitSignatureProtection, @@ -162,6 +164,44 @@ describe('github:branch-protection:create', () => { require_last_push_approval: true, }, required_conversation_resolution: true, + required_linear_history: false, + }); + expect( + mockOctokit.rest.repos.createCommitSignatureProtection, + ).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + branch: 'master', + }); + }); + + it('should create branch protection with params and require linear history', async () => { + const input = yaml.parse(examples[3].example).steps[0].input; + const ctx = Object.assign({}, mockContext, { input }); + await action.handler(ctx); + + expect(mockOctokit.rest.repos.updateBranchProtection).toHaveBeenCalledWith({ + mediaType: { + previews: ['luke-cage-preview'], + }, + owner: 'owner', + repo: 'repo', + branch: 'master', + required_status_checks: { + strict: true, + contexts: ['test'], + }, + restrictions: null, + enforce_admins: true, + required_pull_request_reviews: { + required_approving_review_count: 1, + require_code_owner_reviews: true, + bypass_pull_request_allowances: undefined, + dismiss_stale_reviews: true, + require_last_push_approval: true, + }, + required_conversation_resolution: true, + required_linear_history: true, }); expect( mockOctokit.rest.repos.createCommitSignatureProtection, diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.examples.ts b/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.examples.ts index 4d0eb253ef..12761a2b70 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.examples.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.examples.ts @@ -67,4 +67,25 @@ export const examples: TemplateExample[] = [ ], }), }, + { + description: `GitHub Branch Protection and required linear history on default branch.`, + example: yaml.stringify({ + steps: [ + { + action: 'github:branch-protection:create', + name: 'Setup Branch Protection', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requireCodeOwnerReviews: true, + requiredStatusCheckContexts: ['test'], + dismissStaleReviews: true, + requireLastPushApproval: true, + requiredConversationResolution: true, + requiredCommitSigning: true, + requiredLinearHistory: true, + }, + }, + ], + }), + }, ]; diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.test.ts index d4eac30b81..8e672af725 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.test.ts @@ -96,6 +96,7 @@ describe('github:branch-protection:create', () => { require_last_push_approval: false, }, required_conversation_resolution: false, + required_linear_history: false, }); expect( mockOctokit.rest.repos.createCommitSignatureProtection, @@ -132,6 +133,7 @@ describe('github:branch-protection:create', () => { require_last_push_approval: false, }, required_conversation_resolution: false, + required_linear_history: false, }); expect( mockOctokit.rest.repos.createCommitSignatureProtection, @@ -167,6 +169,7 @@ describe('github:branch-protection:create', () => { requiredConversationResolution: true, requireLastPushApproval: true, requiredCommitSigning: true, + requiredLinearHistory: true, }, }); @@ -199,6 +202,7 @@ describe('github:branch-protection:create', () => { require_last_push_approval: true, }, required_conversation_resolution: true, + required_linear_history: true, }); expect( mockOctokit.rest.repos.createCommitSignatureProtection, diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts b/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts index 4ab847c98e..a7555d3af1 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts @@ -62,6 +62,7 @@ export function createGithubBranchProtectionAction(options: { requiredConversationResolution?: boolean; requireLastPushApproval?: boolean; requiredCommitSigning?: boolean; + requiredLinearHistory?: boolean; token?: string; }>({ id: 'github:branch-protection:create', @@ -90,6 +91,7 @@ export function createGithubBranchProtectionAction(options: { inputProps.requiredConversationResolution, requireLastPushApproval: inputProps.requireLastPushApproval, requiredCommitSigning: inputProps.requiredCommitSigning, + requiredLinearHistory: inputProps.requiredLinearHistory, token: inputProps.token, }, }, @@ -109,6 +111,7 @@ export function createGithubBranchProtectionAction(options: { requiredConversationResolution = false, requireLastPushApproval = false, requiredCommitSigning = false, + requiredLinearHistory = false, token: providedToken, } = ctx.input; @@ -147,6 +150,7 @@ export function createGithubBranchProtectionAction(options: { enforceAdmins, dismissStaleReviews, requiredCommitSigning, + requiredLinearHistory, }); }, }); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts index 1c3d035ee3..61dea32c35 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts @@ -100,6 +100,7 @@ export function createGithubRepoCreateAction(options: { includeClaimKeys?: string[]; }; requireCommitSigning?: boolean; + requiredLinearHistory?: boolean; customProperties?: { [key: string]: string }; }>({ id: 'github:repo:create', @@ -140,6 +141,7 @@ export function createGithubRepoCreateAction(options: { secrets: inputProps.secrets, oidcCustomization: inputProps.oidcCustomization, requiredCommitSigning: inputProps.requiredCommitSigning, + requiredLinearHistory: inputProps.requiredLinearHistory, customProperties: inputProps.customProperties, }, }, diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.test.ts index d0ee4bfa12..9ff8cd123c 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.test.ts @@ -331,6 +331,7 @@ describe('github:repo:push', () => { bypassPullRequestAllowances: undefined, requiredApprovingReviewCount: 1, requiredCommitSigning: false, + requiredLinearHistory: false, restrictions: undefined, }); @@ -359,6 +360,7 @@ describe('github:repo:push', () => { bypassPullRequestAllowances: undefined, requiredApprovingReviewCount: 1, requiredCommitSigning: false, + requiredLinearHistory: false, restrictions: undefined, }); @@ -387,6 +389,7 @@ describe('github:repo:push', () => { bypassPullRequestAllowances: undefined, requiredApprovingReviewCount: 1, requiredCommitSigning: false, + requiredLinearHistory: false, restrictions: undefined, }); @@ -415,6 +418,7 @@ describe('github:repo:push', () => { bypassPullRequestAllowances: undefined, requiredApprovingReviewCount: 1, requiredCommitSigning: false, + requiredLinearHistory: false, restrictions: undefined, }); }); @@ -464,6 +468,11 @@ describe('github:repo:push', () => { defaultValue: false, overrideValue: true, }, + { + inputProperty: 'requiredLinearHistory', + defaultValue: false, + overrideValue: true, + }, { inputProperty: 'protectEnforceAdmins', defaultValue: true, @@ -508,6 +517,7 @@ describe('github:repo:push', () => { bypassPullRequestAllowances: undefined, requiredApprovingReviewCount: 1, requiredCommitSigning: false, + requiredLinearHistory: false, restrictions: undefined, [octokitParameter || inputProperty]: defaultValue, }); @@ -536,6 +546,7 @@ describe('github:repo:push', () => { bypassPullRequestAllowances: undefined, requiredApprovingReviewCount: 1, requiredCommitSigning: false, + requiredLinearHistory: false, restrictions: undefined, [octokitParameter || inputProperty]: overrideValue, }); @@ -564,6 +575,7 @@ describe('github:repo:push', () => { bypassPullRequestAllowances: undefined, requiredApprovingReviewCount: 1, requiredCommitSigning: false, + requiredLinearHistory: false, restrictions: undefined, [octokitParameter || inputProperty]: defaultValue, }); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.ts b/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.ts index a4fa7673c6..62624057c8 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.ts @@ -75,6 +75,7 @@ export function createGithubRepoPushAction(options: { sourcePath?: string; token?: string; requiredCommitSigning?: boolean; + requiredLinearHistory?: boolean; requireLastPushApproval?: boolean; }>({ id: 'github:repo:push', @@ -106,6 +107,7 @@ export function createGithubRepoPushAction(options: { sourcePath: inputProps.sourcePath, token: inputProps.token, requiredCommitSigning: inputProps.requiredCommitSigning, + requiredLinearHistory: inputProps.requiredLinearHistory, }, }, output: { @@ -137,6 +139,7 @@ export function createGithubRepoPushAction(options: { requireLastPushApproval = false, token: providedToken, requiredCommitSigning = false, + requiredLinearHistory = false, } = ctx.input; const { owner, repo } = parseRepoUrl(repoUrl, integrations); @@ -185,6 +188,7 @@ export function createGithubRepoPushAction(options: { gitAuthorEmail, dismissStaleReviews, requiredCommitSigning, + requiredLinearHistory, ); ctx.output('remoteUrl', remoteUrl); diff --git a/plugins/scaffolder-backend-module-github/src/actions/helpers.ts b/plugins/scaffolder-backend-module-github/src/actions/helpers.ts index b28c35105c..9bba0ca359 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/helpers.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/helpers.ts @@ -371,6 +371,7 @@ export async function initRepoPushAndProtect( gitAuthorEmail?: string, dismissStaleReviews?: boolean, requiredCommitSigning?: boolean, + requiredLinearHistory?: boolean, ): Promise<{ commitHash: string }> { const gitAuthorInfo = { name: gitAuthorName @@ -416,6 +417,7 @@ export async function initRepoPushAndProtect( enforceAdmins: protectEnforceAdmins, dismissStaleReviews: dismissStaleReviews, requiredCommitSigning: requiredCommitSigning, + requiredLinearHistory: requiredLinearHistory, }); } catch (e) { assertError(e); diff --git a/plugins/scaffolder-backend-module-github/src/actions/inputProperties.ts b/plugins/scaffolder-backend-module-github/src/actions/inputProperties.ts index 4a52c1d69f..2c86cf492e 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/inputProperties.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/inputProperties.ts @@ -270,6 +270,12 @@ const requiredCommitSigning = { description: `Require commit signing so that you must sign commits on this branch.`, }; +const requiredLinearHistory = { + title: 'Require linear history', + type: 'boolean', + description: `Prevent merge commits from being pushed to matching branches.`, +}; + const repoVariables = { title: 'Repository Variables', description: `Variables attached to the repository`, @@ -346,6 +352,7 @@ export { sourcePath }; export { token }; export { topics }; export { requiredCommitSigning }; +export { requiredLinearHistory }; export { repoVariables }; export { secrets }; export { oidcCustomization };