feat: [CODE-2386] added rules to update branch [ block_branch_update , block_force_push ] (#2710)

* fix: [CODE-2386] updated return for branch rules map logic
* fix: [CODE-2386] fix force update on require PR
* fix: [CODE-2382] disable force-push for require pr
* fix: [CODE-2382] disable force-push and require pr rule if branch update rule is selected
* fix: [CODE-2386] commented sub heading, will be added later
* feat: [CODE-2386] added rules to update branch
This commit is contained in:
Ritik Kapoor 2024-09-23 17:01:46 +00:00 committed by Harness
parent 1725841f67
commit 534f5a8293
8 changed files with 383 additions and 66 deletions

View File

@ -37,7 +37,13 @@ import { useHistory } from 'react-router-dom'
import { useGet, useMutate } from 'restful-react' import { useGet, useMutate } from 'restful-react'
import { BranchTargetType, MergeStrategy, SettingTypeMode, SettingsTab, branchTargetOptions } from 'utils/GitUtils' import { BranchTargetType, MergeStrategy, SettingTypeMode, SettingsTab, branchTargetOptions } from 'utils/GitUtils'
import { useStrings } from 'framework/strings' import { useStrings } from 'framework/strings'
import { REGEX_VALID_REPO_NAME, getErrorMessage, permissionProps, rulesFormInitialPayload } from 'utils/Utils' import {
REGEX_VALID_REPO_NAME,
RulesFormPayload,
getErrorMessage,
permissionProps,
rulesFormInitialPayload
} from 'utils/Utils'
import type { import type {
RepoRepositoryOutput, RepoRepositoryOutput,
OpenapiRule, OpenapiRule,
@ -147,7 +153,7 @@ const BranchProtectionForm = (props: {
} }
const history = useHistory() const history = useHistory()
const initialValues = useMemo(() => { const initialValues = useMemo((): RulesFormPayload => {
if (editMode && rule) { if (editMode && rule) {
const minReviewerCheck = const minReviewerCheck =
((rule.definition as ProtectionBranch)?.pullreq?.approvals?.require_minimum_count as number) > 0 ? true : false ((rule.definition as ProtectionBranch)?.pullreq?.approvals?.require_minimum_count as number) > 0 ? true : false
@ -197,8 +203,16 @@ const BranchProtectionForm = (props: {
rebaseMerge: isRebasePresent, rebaseMerge: isRebasePresent,
autoDelete: (rule.definition as ProtectionBranch)?.pullreq?.merge?.delete_branch, autoDelete: (rule.definition as ProtectionBranch)?.pullreq?.merge?.delete_branch,
blockBranchCreation: (rule.definition as ProtectionBranch)?.lifecycle?.create_forbidden, blockBranchCreation: (rule.definition as ProtectionBranch)?.lifecycle?.create_forbidden,
blockBranchUpdate:
(rule.definition as ProtectionBranch)?.lifecycle?.update_forbidden &&
(rule.definition as ProtectionBranch)?.pullreq?.merge?.block,
blockBranchDeletion: (rule.definition as ProtectionBranch)?.lifecycle?.delete_forbidden, blockBranchDeletion: (rule.definition as ProtectionBranch)?.lifecycle?.delete_forbidden,
requirePr: (rule.definition as ProtectionBranch)?.lifecycle?.update_forbidden, blockForcePush:
(rule.definition as ProtectionBranch)?.lifecycle?.update_forbidden ||
(rule.definition as ProtectionBranch)?.lifecycle?.update_force_forbidden,
requirePr:
(rule.definition as ProtectionBranch)?.lifecycle?.update_forbidden &&
!(rule.definition as ProtectionBranch)?.pullreq?.merge?.block,
targetSet: false, targetSet: false,
bypassSet: false bypassSet: false
} }
@ -218,7 +232,7 @@ const BranchProtectionForm = (props: {
[space] [space]
) )
return ( return (
<Formik <Formik<RulesFormPayload>
formName="branchProtectionRulesNewEditForm" formName="branchProtectionRulesNewEditForm"
initialValues={initialValues} initialValues={initialValues}
enableReinitialize enableReinitialize
@ -265,7 +279,8 @@ const BranchProtectionForm = (props: {
}, },
merge: { merge: {
strategies_allowed: stratArray, strategies_allowed: stratArray,
delete_branch: formData.autoDelete delete_branch: formData.autoDelete,
block: formData.blockBranchUpdate
}, },
status_checks: { status_checks: {
require_identifiers: formData.statusChecks require_identifiers: formData.statusChecks
@ -274,7 +289,8 @@ const BranchProtectionForm = (props: {
lifecycle: { lifecycle: {
create_forbidden: formData.blockBranchCreation, create_forbidden: formData.blockBranchCreation,
delete_forbidden: formData.blockBranchDeletion, delete_forbidden: formData.blockBranchDeletion,
update_forbidden: formData.requirePr update_forbidden: formData.requirePr || formData.blockBranchUpdate,
update_force_forbidden: formData.blockForcePush && !formData.requirePr && !formData.blockBranchUpdate
} }
} }
} }
@ -304,7 +320,7 @@ const BranchProtectionForm = (props: {
const requireStatusChecks = formik.values.requireStatusChecks const requireStatusChecks = formik.values.requireStatusChecks
const filteredUserOptions = userOptions.filter( const filteredUserOptions = userOptions.filter(
(item: SelectOption) => !bypassList.includes(item.value as string) (item: SelectOption) => !bypassList?.includes(item.value as string)
) )
return ( return (
@ -394,7 +410,7 @@ const BranchProtectionForm = (props: {
if (formik.values.target !== '') { if (formik.values.target !== '') {
formik.setFieldValue('targetSet', true) formik.setFieldValue('targetSet', true)
targetList.push([BranchTargetType.INCLUDE, formik.values.target]) targetList.push([BranchTargetType.INCLUDE, formik.values.target ?? ''])
formik.setFieldValue('targetList', targetList) formik.setFieldValue('targetList', targetList)
formik.setFieldValue('target', '') formik.setFieldValue('target', '')
} }
@ -409,7 +425,7 @@ const BranchProtectionForm = (props: {
if (formik.values.target !== '') { if (formik.values.target !== '') {
formik.setFieldValue('targetSet', true) formik.setFieldValue('targetSet', true)
targetList.push([BranchTargetType.EXCLUDE, formik.values.target]) targetList.push([BranchTargetType.EXCLUDE, formik.values.target ?? ''])
formik.setFieldValue('targetList', targetList) formik.setFieldValue('targetList', targetList)
formik.setFieldValue('target', '') formik.setFieldValue('target', '')
} }
@ -473,7 +489,7 @@ const BranchProtectionForm = (props: {
<BypassList bypassList={bypassList} setFieldValue={formik.setFieldValue} /> <BypassList bypassList={bypassList} setFieldValue={formik.setFieldValue} />
</Container> </Container>
<ProtectionRulesForm <ProtectionRulesForm
setFieldValue={formik.setFieldValue} formik={formik}
requireStatusChecks={requireStatusChecks} requireStatusChecks={requireStatusChecks}
minReviewers={minReviewers} minReviewers={minReviewers}
statusOptions={statusOptions} statusOptions={statusOptions}

View File

@ -14,36 +14,40 @@
* limitations under the License. * limitations under the License.
*/ */
import React from 'react' import React from 'react'
import cx from 'classnames' import cx from 'classnames'
import { Container, FlexExpander, FormInput, Layout, SelectOption, Text } from '@harnessio/uicore' import { Container, FlexExpander, FormInput, Layout, SelectOption, Text } from '@harnessio/uicore'
import { Icon } from '@harnessio/icons' import { Icon } from '@harnessio/icons'
import { Color, FontVariation } from '@harnessio/design-system'
import { FontVariation } from '@harnessio/design-system' import type { FormikProps } from 'formik'
import { Classes, Popover, PopoverInteractionKind, PopoverPosition } from '@blueprintjs/core'
import { useStrings } from 'framework/strings' import { useStrings } from 'framework/strings'
import type { RulesFormPayload } from 'utils/Utils'
import css from '../BranchProtectionForm.module.scss' import css from '../BranchProtectionForm.module.scss'
const ProtectionRulesForm = (props: { const ProtectionRulesForm = (props: {
requireStatusChecks: boolean requireStatusChecks: boolean
minReviewers: boolean minReviewers: boolean
statusOptions: SelectOption[] statusOptions: SelectOption[]
statusChecks: string[] statusChecks: string[]
limitMergeStrats: boolean // eslint-disable-next-line @typescript-eslint/no-explicit-any limitMergeStrats: boolean // eslint-disable-next-line @typescript-eslint/no-explicit-any
setFieldValue: (field: string, value: any, shouldValidate?: boolean | undefined) => void
setSearchStatusTerm: React.Dispatch<React.SetStateAction<string>> setSearchStatusTerm: React.Dispatch<React.SetStateAction<string>>
formik: FormikProps<RulesFormPayload>
}) => { }) => {
const { const {
setFieldValue,
statusChecks, statusChecks,
setSearchStatusTerm, setSearchStatusTerm,
minReviewers, minReviewers,
requireStatusChecks, requireStatusChecks,
statusOptions, statusOptions,
limitMergeStrats limitMergeStrats,
formik
} = props } = props
const { getString } = useStrings() const { getString } = useStrings()
const setFieldValue = formik.setFieldValue
const filteredStatusOptions = statusOptions.filter( const filteredStatusOptions = statusOptions.filter(
(item: SelectOption) => !statusChecks.includes(item.value as string) (item: SelectOption) => !statusChecks.includes(item.value as string)
) )
const { values } = formik
return ( return (
<Container margin={{ top: 'medium' }} className={css.generalContainer}> <Container margin={{ top: 'medium' }} className={css.generalContainer}>
<Text className={css.headingSize} padding={{ bottom: 'medium' }} font={{ variation: FontVariation.H4 }}> <Text className={css.headingSize} padding={{ bottom: 'medium' }} font={{ variation: FontVariation.H4 }}>
@ -68,16 +72,76 @@ const ProtectionRulesForm = (props: {
<Text padding={{ left: 'xlarge' }} className={css.checkboxText}> <Text padding={{ left: 'xlarge' }} className={css.checkboxText}>
{getString('branchProtection.blockBranchDeletionText')} {getString('branchProtection.blockBranchDeletionText')}
</Text> </Text>
<hr className={css.dividerContainer} /> <hr className={css.dividerContainer} />
<FormInput.CheckBox <FormInput.CheckBox
className={css.checkboxLabel} className={css.checkboxLabel}
label={getString('branchProtection.requirePr')} label={getString('branchProtection.blockBranchUpdate')}
name={'requirePr'} name={'blockBranchUpdate'}
onChange={() => {
setFieldValue('blockForcePush', !(values.blockBranchUpdate && values.blockForcePush))
setFieldValue('requirePr', false)
}}
/> />
<Text padding={{ left: 'xlarge' }} className={css.checkboxText}> <Text padding={{ left: 'xlarge' }} className={css.checkboxText}>
{getString('branchProtection.requirePrText')} {getString('branchProtection.blockBranchUpdateText')}
</Text> </Text>
<hr className={css.dividerContainer} />
<Popover
interactionKind={PopoverInteractionKind.HOVER}
position={PopoverPosition.TOP_LEFT}
popoverClassName={Classes.DARK}
disabled={!(values.blockBranchUpdate || values.requirePr)}
content={
<Container padding="medium">
<Text font={{ variation: FontVariation.FORM_HELP }} color={Color.WHITE}>
{values.requirePr ? getString('pushBlockedMessage') : getString('ruleBlockedMessage')}
</Text>
</Container>
}>
<>
<FormInput.CheckBox
disabled={values.blockBranchUpdate || values.requirePr}
className={css.checkboxLabel}
label={getString('branchProtection.blockForcePush')}
name={'blockForcePush'}
/>
<Text padding={{ left: 'xlarge' }} className={css.checkboxText}>
{getString('branchProtection.blockForcePushText')}
</Text>
</>
</Popover>
<hr className={css.dividerContainer} />
<Popover
interactionKind={PopoverInteractionKind.HOVER}
position={PopoverPosition.TOP_LEFT}
popoverClassName={Classes.DARK}
disabled={!values.blockBranchUpdate}
content={
<Container padding="medium">
<Text font={{ variation: FontVariation.FORM_HELP }} color={Color.WHITE}>
{getString('ruleBlockedMessage')}
</Text>
</Container>
}>
<>
<FormInput.CheckBox
disabled={values.blockBranchUpdate}
className={css.checkboxLabel}
label={getString('branchProtection.requirePr')}
name={'requirePr'}
onChange={() => {
setFieldValue('blockForcePush', !values.requirePr)
}}
/>
<Text padding={{ left: 'xlarge' }} className={css.checkboxText}>
{getString('branchProtection.requirePrText')}
</Text>
</>
</Popover>
<hr className={css.dividerContainer} /> <hr className={css.dividerContainer} />
<FormInput.CheckBox <FormInput.CheckBox
className={css.checkboxLabel} className={css.checkboxLabel}

View File

@ -39,7 +39,16 @@ import { useHistory } from 'react-router-dom'
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata' import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
import { useQueryParams } from 'hooks/useQueryParams' import { useQueryParams } from 'hooks/useQueryParams'
import { usePageIndex } from 'hooks/usePageIndex' import { usePageIndex } from 'hooks/usePageIndex'
import { getErrorMessage, LIST_FETCHING_LIMIT, permissionProps, type PageBrowserProps } from 'utils/Utils' import {
getErrorMessage,
LIST_FETCHING_LIMIT,
permissionProps,
type PageBrowserProps,
Rule,
RuleFields,
BranchProtectionRulesMapType,
createRuleFieldsMap
} from 'utils/Utils'
import { SettingTypeMode } from 'utils/GitUtils' import { SettingTypeMode } from 'utils/GitUtils'
import { ResourceListingPagination } from 'components/ResourceListingPagination/ResourceListingPagination' import { ResourceListingPagination } from 'components/ResourceListingPagination/ResourceListingPagination'
import { NoResultCard } from 'components/NoResultCard/NoResultCard' import { NoResultCard } from 'components/NoResultCard/NoResultCard'
@ -50,6 +59,7 @@ import { OptionsMenuButton } from 'components/OptionsMenuButton/OptionsMenuButto
import type { OpenapiRule, ProtectionPattern } from 'services/code' import type { OpenapiRule, ProtectionPattern } from 'services/code'
import { useAppContext } from 'AppContext' import { useAppContext } from 'AppContext'
import { useGetSpaceParam } from 'hooks/useGetSpaceParam' import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
import Include from '../../icons/Include.svg?url' import Include from '../../icons/Include.svg?url'
import Exclude from '../../icons/Exclude.svg?url' import Exclude from '../../icons/Exclude.svg?url'
import BranchProtectionForm from './BranchProtectionForm/BranchProtectionForm' import BranchProtectionForm from './BranchProtectionForm/BranchProtectionForm'
@ -73,6 +83,7 @@ const BranchProtectionListing = (props: { activeTab: string }) => {
const { const {
data: rules, data: rules,
refetch: refetchRules, refetch: refetchRules,
loading: loadingRules,
response response
} = useGet<OpenapiRule[]>({ } = useGet<OpenapiRule[]>({
path: `/api/v1/repos/${repoMetadata?.path}/+/rules`, path: `/api/v1/repos/${repoMetadata?.path}/+/rules`,
@ -87,6 +98,89 @@ const BranchProtectionListing = (props: { activeTab: string }) => {
lazy: !repoMetadata || !!editRule lazy: !repoMetadata || !!editRule
}) })
const branchProtectionRules = {
requireMinReviewersTitle: {
title: getString('branchProtection.requireMinReviewersTitle'),
requiredRule: {
[RuleFields.APPROVALS_REQUIRE_MINIMUM_COUNT]: true
}
},
reqReviewFromCodeOwnerTitle: {
title: getString('branchProtection.reqReviewFromCodeOwnerTitle'),
requiredRule: {
[RuleFields.APPROVALS_REQUIRE_CODE_OWNERS]: true
}
},
reqResOfChanges: {
title: getString('branchProtection.reqResOfChanges'),
requiredRule: {
[RuleFields.APPROVALS_REQUIRE_NO_CHANGE_REQUEST]: true
}
},
reqNewChangesTitle: {
title: getString('branchProtection.reqNewChangesTitle'),
requiredRule: {
[RuleFields.APPROVALS_REQUIRE_LATEST_COMMIT]: true
}
},
reqCommentResolutionTitle: {
title: getString('branchProtection.reqCommentResolutionTitle'),
requiredRule: {
[RuleFields.COMMENTS_REQUIRE_RESOLVE_ALL]: true
}
},
reqStatusChecksTitle: {
title: getString('branchProtection.reqStatusChecksTitle'),
requiredRule: {
[RuleFields.STATUS_CHECKS_ALL_MUST_SUCCEED]: true
}
},
limitMergeStrategies: {
title: getString('branchProtection.limitMergeStrategies'),
requiredRule: {
[RuleFields.MERGE_STRATEGIES_ALLOWED]: true
}
},
autoDeleteTitle: {
title: getString('branchProtection.autoDeleteTitle'),
requiredRule: {
[RuleFields.MERGE_DELETE_BRANCH]: true
}
},
blockBranchCreation: {
title: getString('branchProtection.blockBranchCreation'),
requiredRule: {
[RuleFields.LIFECYCLE_CREATE_FORBIDDEN]: true
}
},
blockBranchDeletion: {
title: getString('branchProtection.blockBranchDeletion'),
requiredRule: {
[RuleFields.LIFECYCLE_DELETE_FORBIDDEN]: true
}
},
blockBranchUpdate: {
title: getString('branchProtection.blockBranchUpdate'),
requiredRule: {
[RuleFields.MERGE_BLOCK]: true,
[RuleFields.LIFECYCLE_UPDATE_FORBIDDEN]: true
}
},
requirePr: {
title: getString('branchProtection.requirePr'),
requiredRule: {
[RuleFields.LIFECYCLE_UPDATE_FORBIDDEN]: true,
[RuleFields.MERGE_BLOCK]: false
}
},
blockForcePush: {
title: getString('branchProtection.blockForcePush'),
requiredRule: {
[RuleFields.LIFECYCLE_UPDATE_FORCE_FORBIDDEN]: true
}
}
}
const columns: Column<OpenapiRule>[] = useMemo( const columns: Column<OpenapiRule>[] = useMemo(
() => [ () => [
{ {
@ -137,48 +231,35 @@ const BranchProtectionListing = (props: { activeTab: string }) => {
</Text> </Text>
) )
type Rule = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any
}
const fieldsToCheck = {
'pullreq.approvals.require_minimum_count': getString('branchProtection.requireMinReviewersTitle'),
'pullreq.approvals.require_code_owners': getString('branchProtection.reqReviewFromCodeOwnerTitle'),
'pullreq.approvals.require_no_change_request': getString('branchProtection.reqResOfChanges'),
'pullreq.approvals.require_latest_commit': getString('branchProtection.reqNewChangesTitle'),
'pullreq.comments.require_resolve_all': getString('branchProtection.reqCommentResolutionTitle'),
'pullreq.status_checks.all_must_succeed': getString('branchProtection.reqStatusChecksTitle'),
'pullreq.status_checks.require_identifiers': getString('branchProtection.reqStatusChecksTitle'),
'pullreq.merge.strategies_allowed': getString('branchProtection.limitMergeStrategies'),
'pullreq.merge.delete_branch': getString('branchProtection.autoDeleteTitle'),
'lifecycle.create_forbidden': getString('branchProtection.blockBranchCreation'),
'lifecycle.delete_forbidden': getString('branchProtection.blockBranchDeletion'),
'lifecycle.update_forbidden': getString('branchProtection.requirePr')
}
type NonEmptyRule = { type NonEmptyRule = {
field: string // eslint-disable-next-line @typescript-eslint/no-explicit-any field: string // eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any value: any
} }
const checkFieldsNotEmpty = (rulesArr: Rule, fields: { [key: string]: string }): NonEmptyRule[] => { const checkAppliedRules = (rulesData: Rule, rulesList: BranchProtectionRulesMapType): NonEmptyRule[] => {
const nonEmptyFields: NonEmptyRule[] = [] const nonEmptyFields: NonEmptyRule[] = []
for (const field in fields) { const rulesDefinitionData: Record<RuleFields, boolean> = createRuleFieldsMap(rulesData)
const keys = field.split('.') for (const [key, rule] of Object.entries(rulesList)) {
let value = rulesArr const { title, requiredRule } = rule
for (const key of keys) { const isApplicable = Object.entries(requiredRule).every(([ruleField, requiredValue]) => {
value = value[key] const ruleFieldEnum = ruleField as RuleFields
if (value == null) break const actualValue = rulesDefinitionData[ruleFieldEnum]
} if (requiredValue) return actualValue
if (value !== undefined && (Array.isArray(value) ? value.length > 0 : true)) { return !actualValue
nonEmptyFields.push({ field, value: fields[field] }) // Use value from fieldsToCheck })
if (isApplicable) {
nonEmptyFields.push({
field: key,
value: title
})
} }
} }
return nonEmptyFields return nonEmptyFields
} }
const nonEmptyRules = checkFieldsNotEmpty(row.original.definition as Rule, fieldsToCheck) const nonEmptyRules = checkAppliedRules(row.original.definition as Rule, branchProtectionRules)
const { hooks, standalone } = useAppContext() const { hooks, standalone } = useAppContext()
const space = useGetSpaceParam() const space = useGetSpaceParam()
@ -346,7 +427,7 @@ const BranchProtectionListing = (props: { activeTab: string }) => {
{nonEmptyRules.map((rule: { value: string }) => { {nonEmptyRules.map((rule: { value: string }) => {
return ( return (
<Text <Text
key={`${row.original.identifier}-${rule}`} key={`${row.original.identifier}-${rule.value}`}
className={css.appliedRulesTextContainer}> className={css.appliedRulesTextContainer}>
{rule.value} {rule.value}
</Text> </Text>
@ -386,6 +467,7 @@ const BranchProtectionListing = (props: { activeTab: string }) => {
) )
return ( return (
<Container> <Container>
<LoadingSpinner visible={loadingRules} />
{repoMetadata && !newRule && !editRule && ( {repoMetadata && !newRule && !editRule && (
<BranchProtectionHeader <BranchProtectionHeader
activeTab={activeTab} activeTab={activeTab}

View File

@ -70,6 +70,10 @@ export interface StringsMap {
'branchProtection.blockBranchCreationText': string 'branchProtection.blockBranchCreationText': string
'branchProtection.blockBranchDeletion': string 'branchProtection.blockBranchDeletion': string
'branchProtection.blockBranchDeletionText': string 'branchProtection.blockBranchDeletionText': string
'branchProtection.blockBranchUpdate': string
'branchProtection.blockBranchUpdateText': string
'branchProtection.blockForcePush': string
'branchProtection.blockForcePushText': string
'branchProtection.bypassList': string 'branchProtection.bypassList': string
'branchProtection.commitDirectlyAlertBtn': string 'branchProtection.commitDirectlyAlertBtn': string
'branchProtection.commitDirectlyAlertText': string 'branchProtection.commitDirectlyAlertText': string
@ -169,6 +173,8 @@ export interface StringsMap {
'changesSection.noReviewsReq': string 'changesSection.noReviewsReq': string
'changesSection.pendingAppFromCodeOwners': string 'changesSection.pendingAppFromCodeOwners': string
'changesSection.pendingLatestApprovalCodeOwners': string 'changesSection.pendingLatestApprovalCodeOwners': string
'changesSection.prMergeBlockedMessage': string
'changesSection.prMergeBlockedTitle': string
'changesSection.pullReqWithoutAnyReviews': string 'changesSection.pullReqWithoutAnyReviews': string
'changesSection.reqChangeFromCodeOwners': string 'changesSection.reqChangeFromCodeOwners': string
'changesSection.someChangesWereAppByCodeOwner': string 'changesSection.someChangesWereAppByCodeOwner': string
@ -872,6 +878,7 @@ export interface StringsMap {
pullRequestNotFoundforFilter: string pullRequestNotFoundforFilter: string
pullRequestalreadyExists: string pullRequestalreadyExists: string
pullRequests: string pullRequests: string
pushBlockedMessage: string
quote: string quote: string
reTriggeredExecution: string reTriggeredExecution: string
reactivate: string reactivate: string
@ -940,6 +947,7 @@ export interface StringsMap {
reviewerNotFound: string reviewerNotFound: string
reviewers: string reviewers: string
role: string role: string
ruleBlockedMessage: string
run: string run: string
running: string running: string
samplePayloadUrl: string samplePayloadUrl: string

View File

@ -243,6 +243,8 @@ webhookAllEventsSelected: 'All Events'
branchTagCreation: 'Branch or tag creation' branchTagCreation: 'Branch or tag creation'
branchTagDeletion: 'Branch or tag deletion' branchTagDeletion: 'Branch or tag deletion'
branchProtectionRules: 'Branch protection rules' branchProtectionRules: 'Branch protection rules'
ruleBlockedMessage: 'All branch updates are blocked'
pushBlockedMessage: 'All branch pushes are blocked'
checkRuns: 'Check runs' checkRuns: 'Check runs'
checkSuites: 'Check suites' checkSuites: 'Check suites'
scanAlerts: 'Code scanning alerts' scanAlerts: 'Code scanning alerts'
@ -1002,9 +1004,13 @@ branchProtection:
autoDeleteTitle: Auto delete branch on merge autoDeleteTitle: Auto delete branch on merge
autoDeleteText: Automatically delete the source branch of a pull request after it is merged autoDeleteText: Automatically delete the source branch of a pull request after it is merged
blockBranchCreation: Block branch creation blockBranchCreation: Block branch creation
blockBranchUpdate: Block branch update
blockBranchUpdateText: Only allow users with bypass permission to update matching branches
blockBranchCreationText: Only allow users with bypass permission to create matching branches blockBranchCreationText: Only allow users with bypass permission to create matching branches
blockBranchDeletion: Block branch deletion blockBranchDeletion: Block branch deletion
blockBranchDeletionText: Only allow users with bypass permission to delete matching branches blockBranchDeletionText: Only allow users with bypass permission to delete matching branches
blockForcePush: Block force push
blockForcePushText: Only allow users with bypass permission to force push to matching branches
editRule: Edit Rule editRule: Edit Rule
saveRule: Save Rule saveRule: Save Rule
deleteRule: Delete Rule deleteRule: Delete Rule
@ -1098,6 +1104,8 @@ checkStatus:
pending: Pending... pending: Pending...
error: Errored in {time} error: Errored in {time}
changesSection: changesSection:
prMergeBlockedTitle: Base branch does not allow updates
prMergeBlockedMessage: Read about Protected Branches
reqChangeFromCodeOwners: Changes requested by code owner reqChangeFromCodeOwners: Changes requested by code owner
codeOwnerReqChanges: Code owner requested changes codeOwnerReqChanges: Code owner requested changes
pendingAppFromCodeOwners: Approvals pending from code owners pendingAppFromCodeOwners: Approvals pending from code owners

View File

@ -104,6 +104,7 @@ const PullRequestOverviewPanel = (props: PullRequestOverviewPanelProps) => {
const [minReqLatestApproval, setMinReqLatestApproval] = useState(0) const [minReqLatestApproval, setMinReqLatestApproval] = useState(0)
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const [resolvedCommentArr, setResolvedCommentArr] = useState<any>() const [resolvedCommentArr, setResolvedCommentArr] = useState<any>()
const [mergeBlockedRule, setMergeBlockedRule] = useState<boolean>(false)
const [PRStateLoading, setPRStateLoading] = useState(isClosed ? false : true) const [PRStateLoading, setPRStateLoading] = useState(isClosed ? false : true)
const { pullRequestSection } = useGetRepositoryMetadata() const { pullRequestSection } = useGetRepositoryMetadata()
const mergeable = useMemo(() => pullReqMetadata.merge_check_status === MergeCheckStatus.MERGEABLE, [pullReqMetadata]) const mergeable = useMemo(() => pullReqMetadata.merge_check_status === MergeCheckStatus.MERGEABLE, [pullReqMetadata])
@ -199,9 +200,13 @@ const PullRequestOverviewPanel = (props: PullRequestOverviewPanelProps) => {
useEffect(() => { useEffect(() => {
if (ruleViolationArr) { if (ruleViolationArr) {
const requireResCommentRule = extractSpecificViolations(ruleViolationArr, 'pullreq.comments.require_resolve_all') const requireResCommentRule = extractSpecificViolations(ruleViolationArr, 'pullreq.comments.require_resolve_all')
const mergeBlockedViaRule = extractSpecificViolations(ruleViolationArr, 'pullreq.merge.blocked')
if (requireResCommentRule) { if (requireResCommentRule) {
setResolvedCommentArr(requireResCommentRule[0]) setResolvedCommentArr(requireResCommentRule[0])
} }
setMergeBlockedRule(mergeBlockedViaRule.length > 0)
} else {
setMergeBlockedRule(false)
} }
}, [ruleViolationArr, pullReqMetadata, repoMetadata, data, ruleViolation]) }, [ruleViolationArr, pullReqMetadata, repoMetadata, data, ruleViolation])
@ -232,7 +237,7 @@ const PullRequestOverviewPanel = (props: PullRequestOverviewPanelProps) => {
}, [unchecked, pullReqMetadata?.source_sha, activities]) }, [unchecked, pullReqMetadata?.source_sha, activities])
const rebasePossible = useMemo( const rebasePossible = useMemo(
() => pullReqMetadata.merge_target_sha !== pullReqMetadata.merge_base_sha, () => pullReqMetadata.merge_target_sha !== pullReqMetadata.merge_base_sha && !pullReqMetadata.merged,
[pullReqMetadata] [pullReqMetadata]
) )
@ -278,6 +283,7 @@ const PullRequestOverviewPanel = (props: PullRequestOverviewPanelProps) => {
minReqLatestApproval={minReqLatestApproval} minReqLatestApproval={minReqLatestApproval}
reqCodeOwnerLatestApproval={reqCodeOwnerLatestApproval} reqCodeOwnerLatestApproval={reqCodeOwnerLatestApproval}
refetchCodeOwners={refetchCodeOwners} refetchCodeOwners={refetchCodeOwners}
mergeBlockedRule={mergeBlockedRule}
/> />
</Render> </Render>
), ),

View File

@ -59,6 +59,7 @@ interface ChangesSectionProps {
reviewers: TypesPullReqReviewer[] | null reviewers: TypesPullReqReviewer[] | null
minReqLatestApproval: number minReqLatestApproval: number
reqCodeOwnerLatestApproval: boolean reqCodeOwnerLatestApproval: boolean
mergeBlockedRule: boolean
loadingReviewers: boolean loadingReviewers: boolean
refetchReviewers: () => void refetchReviewers: () => void
refetchCodeOwners: () => void refetchCodeOwners: () => void
@ -76,6 +77,7 @@ const ChangesSection = (props: ChangesSectionProps) => {
reqCodeOwnerLatestApproval, reqCodeOwnerLatestApproval,
minReqLatestApproval, minReqLatestApproval,
loadingReviewers, loadingReviewers,
mergeBlockedRule,
refetchReviewers, refetchReviewers,
refetchCodeOwners refetchCodeOwners
} = props } = props
@ -91,7 +93,7 @@ const ChangesSection = (props: ChangesSectionProps) => {
const reviewers = useMemo(() => { const reviewers = useMemo(() => {
refetchCodeOwners() refetchCodeOwners()
return currReviewers // eslint-disable-next-line react-hooks/exhaustive-deps return currReviewers // eslint-disable-next-line react-hooks/exhaustive-deps
}, [currReviewers, refetchReviewers]) }, [currReviewers, refetchReviewers, mergeBlockedRule])
const codeOwners = useMemo(() => { const codeOwners = useMemo(() => {
return currCodeOwners // eslint-disable-next-line react-hooks/exhaustive-deps return currCodeOwners // eslint-disable-next-line react-hooks/exhaustive-deps
@ -144,9 +146,15 @@ const ChangesSection = (props: ChangesSectionProps) => {
reqCodeOwnerApproval || reqCodeOwnerApproval ||
minApproval > 0 || minApproval > 0 ||
reqCodeOwnerLatestApproval || reqCodeOwnerLatestApproval ||
minReqLatestApproval > 0 minReqLatestApproval > 0 ||
mergeBlockedRule
) { ) {
if (codeOwnerChangeReqEntries.length > 0 && (reqCodeOwnerApproval || reqCodeOwnerLatestApproval)) { if (mergeBlockedRule) {
title = getString('changesSection.prMergeBlockedTitle')
// statusMessage = getString('changesSection.prMergeBlockedMessage')
statusColor = Color.RED_700
statusIcon = 'warning-icon'
} else if (codeOwnerChangeReqEntries.length > 0 && (reqCodeOwnerApproval || reqCodeOwnerLatestApproval)) {
title = getString('changesSection.reqChangeFromCodeOwners') title = getString('changesSection.reqChangeFromCodeOwners')
statusMessage = getString('changesSection.codeOwnerReqChanges') statusMessage = getString('changesSection.codeOwnerReqChanges')
statusColor = Color.RED_700 statusColor = Color.RED_700
@ -258,7 +266,9 @@ const ChangesSection = (props: ChangesSectionProps) => {
reqCodeOwnerLatestApproval, reqCodeOwnerLatestApproval,
minReqLatestApproval, minReqLatestApproval,
refetchReviewers, refetchReviewers,
refetchCodeOwners refetchCodeOwners,
mergeBlockedRule,
approvedEvaluations
]) ])
function renderCodeOwnerStatus() { function renderCodeOwnerStatus() {
@ -376,13 +386,14 @@ const ChangesSection = (props: ChangesSectionProps) => {
) )
} }
const viewBtn = const viewBtn =
minApproval > minReqLatestApproval || !mergeBlockedRule &&
(!isEmpty(approvedEvaluations) && minReqLatestApproval === 0) || (minApproval > minReqLatestApproval ||
(minApproval > 0 && minReqLatestApproval === undefined) || (!isEmpty(approvedEvaluations) && minReqLatestApproval === 0) ||
minReqLatestApproval > 0 || (minApproval > 0 && minReqLatestApproval === undefined) ||
!isEmpty(changeReqEvaluations) || minReqLatestApproval > 0 ||
!isEmpty(codeOwners) || !isEmpty(changeReqEvaluations) ||
false !isEmpty(codeOwners) ||
false)
return ( return (
<Render when={!loading && !loadingReviewers && status}> <Render when={!loading && !loadingReviewers && status}>
<Container className={cx(css.sectionContainer, css.borderContainer)}> <Container className={cx(css.sectionContainer, css.borderContainer)}>
@ -399,7 +410,7 @@ const ChangesSection = (props: ChangesSectionProps) => {
)} )}
<Layout.Vertical padding={{ left: 'medium' }}> <Layout.Vertical padding={{ left: 'medium' }}>
<Text <Text
padding={{ bottom: 'xsmall' }} padding={contentText ? { bottom: 'xsmall' } : undefined}
className={css.sectionTitle} className={css.sectionTitle}
color={ color={
headerText === getString('changesSection.noReviewsReq') headerText === getString('changesSection.noReviewsReq')

View File

@ -333,7 +333,7 @@ export interface Violation {
violation: string violation: string
} }
export const rulesFormInitialPayload = { export const rulesFormInitialPayload: RulesFormPayload = {
name: '', name: '',
desc: '', desc: '',
enable: true, enable: true,
@ -357,11 +357,44 @@ export const rulesFormInitialPayload = {
autoDelete: false, autoDelete: false,
blockBranchCreation: false, blockBranchCreation: false,
blockBranchDeletion: false, blockBranchDeletion: false,
blockBranchUpdate: false,
blockForcePush: false,
requirePr: false, requirePr: false,
bypassSet: false, bypassSet: false,
targetSet: false targetSet: false
} }
export type RulesFormPayload = {
name?: string
desc?: string
enable: boolean
target?: string
targetDefault?: boolean
targetList: string[][]
allRepoOwners?: boolean
bypassList?: string[]
requireMinReviewers: boolean
minReviewers?: string | number
requireCodeOwner?: boolean
requireNewChanges?: boolean
reqResOfChanges?: boolean
requireCommentResolution?: boolean
requireStatusChecks: boolean
statusChecks: string[]
limitMergeStrategies: boolean
mergeCommit?: boolean
squashMerge?: boolean
rebaseMerge?: boolean
autoDelete?: boolean
blockBranchCreation?: boolean
blockBranchDeletion?: boolean
blockBranchUpdate?: boolean
blockForcePush?: boolean
requirePr?: boolean
bypassSet: boolean
targetSet: boolean
}
/** /**
* Make any HTML element as a clickable button with keyboard accessibility * Make any HTML element as a clickable button with keyboard accessibility
* support (hit Enter/Space will trigger click event) * support (hit Enter/Space will trigger click event)
@ -851,3 +884,92 @@ export const getScopeData = (space: string, scope: number, standalone: boolean)
return { scopeRef: space, scopeIcon: 'nav-project' as IconName, scopeId: scope } return { scopeRef: space, scopeIcon: 'nav-project' as IconName, scopeId: scope }
} }
} }
export enum RuleFields {
APPROVALS_REQUIRE_MINIMUM_COUNT = 'pullreq.approvals.require_minimum_count',
APPROVALS_REQUIRE_CODE_OWNERS = 'pullreq.approvals.require_code_owners',
APPROVALS_REQUIRE_NO_CHANGE_REQUEST = 'pullreq.approvals.require_no_change_request',
APPROVALS_REQUIRE_LATEST_COMMIT = 'pullreq.approvals.require_latest_commit',
COMMENTS_REQUIRE_RESOLVE_ALL = 'pullreq.comments.require_resolve_all',
STATUS_CHECKS_ALL_MUST_SUCCEED = 'pullreq.status_checks.all_must_succeed',
STATUS_CHECKS_REQUIRE_IDENTIFIERS = 'pullreq.status_checks.require_identifiers',
MERGE_STRATEGIES_ALLOWED = 'pullreq.merge.strategies_allowed',
MERGE_DELETE_BRANCH = 'pullreq.merge.delete_branch',
LIFECYCLE_CREATE_FORBIDDEN = 'lifecycle.create_forbidden',
LIFECYCLE_DELETE_FORBIDDEN = 'lifecycle.delete_forbidden',
MERGE_BLOCK = 'pullreq.merge.block',
LIFECYCLE_UPDATE_FORBIDDEN = 'lifecycle.update_forbidden',
LIFECYCLE_UPDATE_FORCE_FORBIDDEN = 'lifecycle.update_force_forbidden'
}
export type RuleFieldsMap = Record<RuleFields, boolean>
export type Rule = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any
}
export type BranchProtectionRule = {
title: string
requiredRule: {
[key in RuleFields]?: boolean
}
}
export type BranchProtectionRulesMapType = Record<string, BranchProtectionRule>
export function createRuleFieldsMap(ruleDefinition: Rule): RuleFieldsMap {
const ruleFieldsMap: RuleFieldsMap = {
[RuleFields.APPROVALS_REQUIRE_MINIMUM_COUNT]: false,
[RuleFields.APPROVALS_REQUIRE_CODE_OWNERS]: false,
[RuleFields.APPROVALS_REQUIRE_NO_CHANGE_REQUEST]: false,
[RuleFields.APPROVALS_REQUIRE_LATEST_COMMIT]: false,
[RuleFields.COMMENTS_REQUIRE_RESOLVE_ALL]: false,
[RuleFields.STATUS_CHECKS_ALL_MUST_SUCCEED]: false,
[RuleFields.STATUS_CHECKS_REQUIRE_IDENTIFIERS]: false,
[RuleFields.MERGE_STRATEGIES_ALLOWED]: false,
[RuleFields.MERGE_DELETE_BRANCH]: false,
[RuleFields.LIFECYCLE_CREATE_FORBIDDEN]: false,
[RuleFields.LIFECYCLE_DELETE_FORBIDDEN]: false,
[RuleFields.MERGE_BLOCK]: false,
[RuleFields.LIFECYCLE_UPDATE_FORBIDDEN]: false,
[RuleFields.LIFECYCLE_UPDATE_FORCE_FORBIDDEN]: false
}
if (ruleDefinition.pullreq) {
if (ruleDefinition.pullreq.approvals) {
ruleFieldsMap[RuleFields.APPROVALS_REQUIRE_CODE_OWNERS] = !!ruleDefinition.pullreq.approvals.require_code_owners
ruleFieldsMap[RuleFields.APPROVALS_REQUIRE_LATEST_COMMIT] =
!!ruleDefinition.pullreq.approvals.require_latest_commit
ruleFieldsMap[RuleFields.APPROVALS_REQUIRE_MINIMUM_COUNT] =
typeof ruleDefinition.pullreq.approvals.require_minimum_count === 'number'
ruleFieldsMap[RuleFields.APPROVALS_REQUIRE_NO_CHANGE_REQUEST] =
!!ruleDefinition.pullreq.approvals.require_no_change_request
}
if (ruleDefinition.pullreq.comments) {
ruleFieldsMap[RuleFields.COMMENTS_REQUIRE_RESOLVE_ALL] = !!ruleDefinition.pullreq.comments.require_resolve_all
}
if (ruleDefinition.pullreq.merge) {
ruleFieldsMap[RuleFields.MERGE_BLOCK] = !!ruleDefinition.pullreq.merge.block
ruleFieldsMap[RuleFields.MERGE_DELETE_BRANCH] = !!ruleDefinition.pullreq.merge.delete_branch
ruleFieldsMap[RuleFields.MERGE_STRATEGIES_ALLOWED] =
Array.isArray(ruleDefinition.pullreq.merge.strategies_allowed) &&
ruleDefinition.pullreq.merge.strategies_allowed.length > 0
}
if (ruleDefinition.pullreq.status_checks) {
ruleFieldsMap[RuleFields.STATUS_CHECKS_REQUIRE_IDENTIFIERS] =
Array.isArray(ruleDefinition.pullreq.status_checks.require_identifiers) &&
ruleDefinition.pullreq.status_checks.require_identifiers.length > 0
}
}
if (ruleDefinition.lifecycle) {
ruleFieldsMap[RuleFields.LIFECYCLE_CREATE_FORBIDDEN] = !!ruleDefinition.lifecycle.create_forbidden
ruleFieldsMap[RuleFields.LIFECYCLE_DELETE_FORBIDDEN] = !!ruleDefinition.lifecycle.delete_forbidden
ruleFieldsMap[RuleFields.LIFECYCLE_UPDATE_FORBIDDEN] = !!ruleDefinition.lifecycle.update_forbidden
ruleFieldsMap[RuleFields.LIFECYCLE_UPDATE_FORCE_FORBIDDEN] = !!ruleDefinition.lifecycle.update_force_forbidden
}
return ruleFieldsMap
}