Mike Goddard
Mike Goddard

Reputation: 606

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?

Upvotes: 13

Views: 11654

Answers (3)

Josh Johanning
Josh Johanning

Reputation: 1235

FYI for those finding this now - there is an API so you don't have to use GraphQL if you don't want.

https://docs.github.com/en/enterprise-cloud@latest/rest/branches/branch-protection#update-branch-protection

curl \
  -X PUT \
  -H "Accept: application/vnd.github+json" \
  -H "Authorization: token <TOKEN>" \
  https://api.github.com/repos/OWNER/REPO/branches/BRANCH/protection \
  -d '{"required_status_checks":{"strict":true,"contexts":["continuous-integration/travis-ci"]},"enforce_admins":true,"required_pull_request_reviews":{"dismissal_restrictions":{"users":["octocat"],"teams":["justice-league"]},"dismiss_stale_reviews":true,"require_code_owner_reviews":true,"required_approving_review_count":2,"bypass_pull_request_allowances":{"users":["octocat"],"teams":["justice-league"]}},"restrictions":{"users":["octocat"],"teams":["justice-league"],"apps":["super-ci"]},"required_linear_history":true,"allow_force_pushes":true,"allow_deletions":true,"block_creations":true,"required_conversation_resolution":true}'

Edit: Even though the API says this is for update, it does create new branch protection rules as well.

Upvotes: 7

Simon Kissane
Simon Kissane

Reputation: 5258

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

Upvotes: 6

Filip Nikolov
Filip Nikolov

Reputation: 1897

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"]
    }
}
'

Upvotes: 2

Related Questions