'Can I add a new branch protection rule via GitHub API?

I know you can update an existing branch protection rule via the API, but I cannot find any references in the v3 API docs WRT creating a new rule. For example, if I want to add a rule to a repo that matches a new branch prefixed with "dev_", I have to add it through the GUI, using the "Apply rule to" field, then I can use the API to update those rule settings. Ideally, I'd like to have a hook that does this automatically if a new branch is introduced to the repo, but does not match an existing rule. I should be able to create that rule through the API. Is there a way to do this?



Solution 1:[1]

It appears that GitHub has decided to add new features via the GraphQL API only, not via the REST API. So while this is impossible with the REST API, you can do it using the GraphQL createBranchProtectionRule mutation.

In case it helps anyone, I wrote a script to do this, using GitHub's gh CLI:

#!/bin/bash
set -ue
err() { echo 1>&2 "$*"; }
die() { err "ERROR: $*"; exit 1; }
mustBool() {
        [[ "${1#*=}" = "true" || "${1#*=}" = "false" ]] ||
                die "bad boolean property value: $1"
}
mustInt() {
        [[ "${1#*=}" =~ [0-9]+ ]] ||
                die "bad integer property value: $1"
}

[ $# -ge 4 ] || {
        err "usage: $0 HOSTNAME ORG REPO PATTERN [PROPERTIES...]"
        err "   where PROPERTIES can be:"
        err "           dismissesStaleReviews=true|false"
        err "           requiresApprovingReviewCount=INTEGER"
        err "           requiresApprovingReviews=true|false"
        err "           requiresCodeOwnerReviews=true|false"
        err "           restrictPushes=true|false"
        exit 1
}
hostname="$1"
org="$2"
repo="$3"
pattern="$4"
shift 4

repoNodeId="$(gh api --hostname "$hostname" "repos/$org/$repo" --jq .node_id)"
[[ -n "$repoNodeId" ]] || die "could not determine repo nodeId"

graphql="
mutation createBranchProtectionRule {
        createBranchProtectionRule(input: {
                repositoryId: \"$repoNodeId\"
                pattern: \"$pattern\""

seen=()
requiredStatusCheckContexts=()
for property in "$@"; do
        for eSeen in "${seen[@]:-}"; do
                [[ "${eSeen%%=*}" = "${property%%=*}" ]] &&
                # Allow duplication of multivalued properties
                [[ "${eSeen%%=*}" != "requiredStatusCheckContexts" ]] &&
                die "Duplicate property: $property"
        done
        seen+=("${property}")

        case "$property" in
        requiredStatusCheckContexts=*)
                requiredStatusCheckContexts+=("${property#*=}")
                ;;
        \
                allowsDeletions=* | \
                allowsForcePushes=* | \
                dismissesStaleReviews=* | \
                isAdminEnforced=* | \
                requiresApprovingReviews=* | \
                requiresCodeOwnerReviews=* | \
                requiresCommitSignatures=* | \
                requiresLinearHistory=* | \
                requiresStatusChecks=* | \
                requiresStrictStatusChecks=* | \
                restrictPushes=* | \
                restrictsPushes=* | \
                restrictsReviewDismissals=* \
        )
                mustBool "$property"
                graphql="$graphql
                ${property%%=*}: ${property#*=}"
                ;;
        requiredApprovingReviewCount=*)
                mustInt "$property"
                graphql="$graphql
                ${property%%=*}: ${property#*=}"
                ;;
        *)
                die "unknown property: $property"
        esac
done

if [ -n "${requiredStatusCheckContexts[*]:-}" ]; then
        graphql="$graphql
                requiredStatusCheckContexts: [
"
        i=0
        for context in "${requiredStatusCheckContexts[@]}"; do
                [ $i -ne 0 ] && graphql="$graphql,
"
                i=$((1+$i))
                graphql="$graphql"$'\t\t\t'"\"$context\""
        done
        graphql="$graphql
                ]
"
fi

graphql="$graphql
        }) {
                branchProtectionRule {
                        allowsDeletions
                        allowsForcePushes
                        creator { login }
                        databaseId
                        dismissesStaleReviews
                        isAdminEnforced
                        pattern
                        repository { nameWithOwner }
                        requiredApprovingReviewCount
                        requiresApprovingReviews
                        requiredStatusCheckContexts
                        requiresCodeOwnerReviews
                        requiresCommitSignatures
                        requiresLinearHistory
                        requiresStatusChecks
                        requiresStrictStatusChecks
                        restrictsPushes
                        restrictsReviewDismissals
                }
                clientMutationId
        }
}"

gh api --hostname "$hostname" graphql -F "query=$graphql" ||
        die "GraphQL update failed: $graphql"
echo ""
echo "SUCCESS: Branch protection rule successfully created"

Here is an example of invoking it it:

./createBranchProtectionRule.sh github.example.com skissane my-repo v* requiresApprovingReviews=true requiresCodeOwnerReviews=true requiredApprovingReviewCount=1 requiresStatusChecks=true requiresStrictStatusChecks=false requiredStatusCheckContexts=continuous-integration/jenkins/pr-merge requiresLinearHistory=true

which produces the following output:

{
  "data": {
    "createBranchProtectionRule": {
      "branchProtectionRule": {
        "allowsDeletions": false,
        "allowsForcePushes": false,
        "creator": {
          "login": "skissane"
        },
        "databaseId": 1729,
        "dismissesStaleReviews": false,
        "isAdminEnforced": false,
        "pattern": "v*",
        "repository": {
          "nameWithOwner": "skissane/my-repo"
        }
        "requiredApprovingReviewCount": 1,
        "requiresApprovingReviews": true,
        "requiredStatusCheckContexts": [
          "continuous-integration/jenkins/pr-merge"
        ],
        "requiresCodeOwnerReviews": true,
        "requiresCommitSignatures": false,
        "requiresLinearHistory": true,
        "requiresStatusChecks": true,
        "requiresStrictStatusChecks": false,
        "restrictsPushes": false,
        "restrictsReviewDismissals": false
      },
      "clientMutationId": null
    }
  }
}

SUCCESS: Branch protection rule successfully created

Solution 2:[2]

This did it for me for GitHub Enterprise.

curl --location --request PUT 'https://github.company.com/api/v3/repos/ORG/REPO-NAME/branches/BRANCH-NAME/protection' \
--header 'Accept: application/vnd.github.v3+json' \
--header 'Accept: application/vnd.github.luke-cage-preview+json' \
--header 'Authorization: Basic .................... \
--header 'Content-Type: text/plain' \
--data-raw '{
    "required_status_checks": {
        "strict": false,
        "contexts": ["continuous-integration/jenkins/BRANCH-NAME"]
    },
    "enforce_admins": true,
    "required_pull_request_reviews": {
        "dismissal_restrictions": {},
        "dismiss_stale_reviews": true,
        "require_code_owner_reviews": true,
        "required_approving_review_count": 1
    },
    "restrictions": {
        "users": ["users"],
        "teams": ["teams"],
        "apps": ["apps"]
    }
}
'

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Filip Nikolov