Skip to content

Instantly share code, notes, and snippets.

@flipdazed
Last active March 26, 2023 07:29
Show Gist options
  • Save flipdazed/587dd0069f683aee77f01f0b9dfb21f6 to your computer and use it in GitHub Desktop.
Save flipdazed/587dd0069f683aee77f01f0b9dfb21f6 to your computer and use it in GitHub Desktop.
Create Branch Protection with GitHub API
#!/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 " allowsDeletions=true|false"
err " allowsForcePushes=true|false"
err " blocksCreations=true|false"
err " bypassForcepushActorIds=USER_ID,TEAM_ID,APP_ID"
err " bypassPullRequestActorIds=USER_ID,TEAM_ID,APP_ID"
err " dismissesStaleReviews=true|false"
err " isAdminEnforced=true|false"
err " lockAllowsFetchAndMerge=true|false"
err " lockBranch=true|false"
err " pattern=PATTERN"
err " pushActorIds=USER_ID,TEAM_ID,APP_ID"
err " requireLastPushApproval=true|false"
err " requiredApprovingReviewCount=INTEGER"
err " requiredDeploymentEnvironments=ENVIRONMENT"
err " requiredStatusCheckContexts=CONTEXT"
err " requiredStatusChecks={strict:true,contexts:[CONTEXT]}"
err " requiresApprovingReviews=true|false"
err " requiresCodeOwnerReviews=true|false"
err " requiresCommitSignatures=true|false"
err " requiresConversationResolution=true|false"
err " requiresDeployments=true|false"
err " requiresLinearHistory=true|false"
err " requiresStatusChecks=true|false"
err " requiresStrictStatusChecks=true|false"
err " restrictsPushes=true|false"
err " restrictsReviewDismissals=true|false"
err " reviewDismissalActorIds=USER_ID,TEAM_ID,APP_ID"
exit 1
}
hostname="$1"
org="$2"
repo="$3"
pattern="$4"
shift 4
mustId() {
# Set the name to search for (passed in as the first argument to the function)
name="$1"
# Search Teams
teams=$(gh api --paginate "/orgs/$org/teams" --jq ".[].name" 2>/dev/null)
for team in $teams; do
if [ "$team" = "$name" ]; then
nodeId=$(gh api --paginate "/orgs/$org/teams" --jq ".[] | select(.name == \"$name\") | .node_id" 2>/dev/null)
printf '"%s"' "$nodeId"
return
fi
done
# Search Users
users=$(gh api --paginate "/orgs/$org/members" --jq ".[].login" 2>/dev/null)
for user in $users; do
if [ "$user" = "$name" ]; then
nodeId=$(gh api --paginate "/users/$user" --jq ".node_id" 2>/dev/null)
printf '"%s"' "$nodeId"
return
fi
done
# Search Apps
apps=$(gh api --paginate "/orgs/$org/installations" --jq ".[].account.login" 2>/dev/null)
for app in $apps; do
if [ "$app" = "$name" ]; then
nodeId=$(gh api --paginate "/orgs/$org/installations" --jq ".[] | select(.account.login == \"$name\") | .account.node_id" 2>/dev/null)
printf '"%s"' "$nodeId"
return
fi
done
echo "No match found"
}
# Add function for creating actorIds CSV
create_actor_ids_csv() {
IFS=',' read -r -a actors <<< "${1#*=}"
actor_ids=()
for actor in "${actors[@]}"; do
actor_ids+=("$(mustId "$actor")")
done
actor_ids_csv=$(IFS=','; echo "${actor_ids[*]}")
printf '%s' "$actor_ids_csv"
}
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=()
requiredStatusChecks=()
for property in "$@"; do
for eSeen in "${seen[@]:-}"; do
[[ "${eSeen%%=*}" = "${property%%=*}" ]] &&
# Allow duplication of multivalued properties
[[ "${eSeen%%=*}" != "requiredStatusCheckContexts" ]] &&
[[ "${eSeen%%=*}" != "requiredStatusChecks" ]] &&
die "Duplicate property: $property"
done
seen+=("${property}")
case "$property" in
allowsDeletions=* | \
allowsForcePushes=* | \
dismissesStaleReviews=* | \
isAdminEnforced=* | \
lockAllowsFetchAndMerge=* | \
lockBranch=* | \
restrictsPushes=* | \
restrictsReviewDismissals=*)
mustBool "$property"
graphql="$graphql
${property%%=*}: ${property#*=}"
;;
bypassForcePushActorIds=*)
actor_ids_csv=$(create_actor_ids_csv "$property")
graphql="$graphql
${property%%=*}: [$actor_ids_csv]"
;;
bypassPullRequestActorIds=*)
actor_ids_csv=$(create_actor_ids_csv "$property")
graphql="$graphql
${property%%=*}: [$actor_ids_csv]"
;;
pushActorIds=*)
actor_ids_csv=$(create_actor_ids_csv "$property")
graphql="$graphql
${property%%=*}: [$actor_ids_csv]"
;;
reviewDismissalActorIds=*)
actor_ids_csv=$(create_actor_ids_csv "$property")
graphql="$graphql
${property%%=*}: [$actor_ids_csv]"
;;
requireLastPushApproval=*)
mustBool "$property"
graphql="$graphql
${property%%=*}: ${property#*=}"
;;
requiredApprovingReviewCount=*)
mustInt "$property"
graphql="$graphql
${property%%=*}: ${property#*=}"
;;
requiredStatusCheckContexts=* | \
requiredStatusChecks=* )
if [[ "${property%%=*}" == "requiredStatusCheckContexts" ]]; then
requiredStatusCheckContexts+=("${property#*=}")
elif [[ "${property%%=*}" == "requiredStatusChecks" ]]; then
requiredStatusChecks+=("${property#*=}")
else
mustInt "$property"
graphql="$graphql
${property%%=*}: ${property#*=}"
fi
;;
requiredDeploymentEnvironments=* )
graphql="$graphql
${property%%=*}: [${property#*=}]"
;;
requiresApprovingReviews=* | \
requiresCodeOwnerReviews=* | \
requiresCommitSignatures=* | \
requiresConversationResolution=* | \
requiresDeployments=* | \
requiresLinearHistory=* | \
requiresStatusChecks=* | \
requiresStrictStatusChecks=*)
mustBool "$property"
graphql="$graphql
${property%%=*}: ${property#*=}"
;;
*)
die "unknown property: $property"
;;
esac
done
# Add requiredStatusCheckContexts to the GraphQL mutation
if [ -n "${requiredStatusCheckContexts[*]:-}" ]; then
graphql="$graphql
requiredStatusCheckContexts: ["
for context in "${requiredStatusCheckContexts[@]}"; do
graphql="$graphql
\"$context\","
done
# Remove the trailing comma and add closing bracket
graphql="${graphql%,}
]"
fi
# Add requiredStatusChecks to the GraphQL mutation
if [ -n "${requiredStatusChecks[*]:-}" ]; then
graphql="$graphql
requiredStatusChecks: ["
for check in "${requiredStatusChecks[@]}"; do
graphql="$graphql
{ context: \"$check\" },"
done
# Remove the trailing comma and add closing bracket
graphql="${graphql%,}
]"
fi
# Complete the GraphQL mutation and execute it using the GitHub API
graphql="$graphql
}) {
branchProtectionRule {
allowsDeletions
allowsForcePushes
blocksCreations
creator { login }
databaseId
dismissesStaleReviews
isAdminEnforced
lockAllowsFetchAndMerge
lockBranch
pattern
pushAllowances(first: 100) {
edges {
node {
actor {
__typename
... on User {
id
}
... on Team {
id
}
... on App {
id
}
}
}
}
}
bypassForcePushAllowances(first: 100) {
edges {
node {
actor {
__typename
... on User {
id
}
... on Team {
id
}
... on App {
id
}
}
}
}
}
bypassPullRequestAllowances(first: 100) {
edges {
node {
actor {
__typename
... on User {
id
}
... on Team {
id
}
... on App {
id
}
}
}
}
}
requireLastPushApproval
repository { nameWithOwner }
requiredApprovingReviewCount
requiredDeploymentEnvironments
requiredStatusCheckContexts
requiredStatusChecks {
context
}
requiresApprovingReviews
requiresCodeOwnerReviews
requiresCommitSignatures
requiresConversationResolution
requiresDeployments
requiresLinearHistory
requiresStatusChecks
requiresStrictStatusChecks
restrictsPushes
restrictsReviewDismissals
reviewDismissalAllowances(first: 100) {
edges {
node {
actor {
__typename
... on User {
id
}
... on Team {
id
}
... on App {
id
}
}
}
}
}
}
clientMutationId
}
}"
# Execute the GraphQL mutation using the GitHub API
response=$(gh api --hostname "$hostname" graphql -F "query=$graphql" 2>&1) || {
if echo "$response" | grep -q "Name already protected: $pattern"; then
echo "SUCCESS: Branch protection rule already exists"
exit 0
else
die "GraphQL update failed: $graphql"
fi
}
# Print success message
echo ""
echo "SUCCESS: Branch protection rule successfully created"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment