Last active
March 26, 2023 07:29
-
-
Save flipdazed/587dd0069f683aee77f01f0b9dfb21f6 to your computer and use it in GitHub Desktop.
Create Branch Protection with GitHub API
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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