[GitHub] いくつかの特定のブランチ(main, staging, develop)にのみ適用されるブランチ保護ルールが作れるのか確認してみた

結論:難しい。保護対象のブランチごとにルールを作成するのが望ましい。
2023.07.01

こんにちは、CX事業本部 Delivery部の若槻です。

GitHub では、リポジトリにブランチ保護ルール(branch protection rule)を作成して、指定したパターンのブランチにルールを適用することができます。

このブランチ保護ルールを利用する上で、mainstagingおよびdevelopなどいくつかの特定のブランチに同じルールを適用したい場合があります。

そこで今回は、上記を実現できるブランチ保護ルールのパターンを作れるのか確認してみました。

はじめに結論

難しそうです。

ただし、次のパターンを設定すれば、"ほぼ"mainstagingおよびdevelopブランチにのみ適用されるブランチ保護ルールを作成できました。

[msd][ate][iav][nge]*

しかしこのパターンも完璧ではなく、maintenanceなど対象としたいブランチ名を含む名前のブランチは対象となってしまうため、保護対象のブランチごとにルールを作成するのが望ましいという結論となりました。

検証

準備

まず、以下のようにしてブランチ保護ルールの ID を取得します。コマンドによるブランチ保護ルールの操作は GraphQL を使用します。

gh api graphql -f query='
  query ($owner: String!, $repo: String!) { 
    repository(owner:$owner, name:$repo) {
      branchProtectionRules(first:100) {
        nodes {
          id
          pattern
        }
      }
    }
  }
' -f owner=${OWNER} -f repo=${REPO}

取得した ID を使用して、ブランチ保護ルールのパターンを変更する関数を作成します。

update_branch_protection_rule() {
  NEW_PATTERN=$1

  gh api graphql -f query='
  mutation ($id: ID!, $pattern: String!) { 
    updateBranchProtectionRule(input: {branchProtectionRuleId: $id, pattern: $pattern}) {
      clientMutationId
    }
  }
' -f id=${RULE_ID} -f pattern=${NEW_PATTERN}
}

また、ブランチ一覧を取得する関数も作成します。結果では保護ブランチと非保護ブランチを分けて表示するようにしています。

get_branches() {

  gh api https://api.github.com/repos/${OWNER}/${REPO}/branches | \
  jq "[group_by(.protected)[] | (\
    if .[0].protected \
    then {protected: map(.name)} \
    else {not_protected: map(.name)} \
    end)]"
}

検証は次の6つのブランチがあるリポジトリで試してみます。

$ get_branches
[
  {
    "not_protected": [
      "develop",
      "feature/hoge",
      "main",
      "staging",
      "temp",
      "v1.2.3"
    ]
  }
]

試行錯誤

その1

ドキュメントによると、ブランチ保護ルールのパターンは fnmatch 構文に従っているようなので、まず {main,staging,develop}というパターンで試してみます。構文通りならこれで行けるはず。

NEW_PATTERN='{main,staging,develop}'
update_branch_protection_rule ${NEW_PATTERN}

しかしどのブランチも保護対象となりません。fnmatch 構文に完全に従っているわけでは無いようです。

$ get_branches
[
  {
    "not_protected": [
      "develop",
      "feature/hoge",
      "main",
      "staging",
      "temp",
      "v1.2.3"
    ]
  }
]

その2

続いて*[main|staging|develop]*というパターン。

NEW_PATTERN='*[main|staging|develop]*'
update_branch_protection_rule ${NEW_PATTERN}

スラッシュ/が含まれるブランチ以外が保護対象となりました。

$ get_branches
[
  {
    "not_protected": [
      "feature/hoge"
    ]
  },
  {
    "protected": [
      "develop",
      "main",
      "staging",
      "temp",
      "v1.2.3"
    ]
  }
]

その3

続いて[main|staging|develop]*というパターン。

NEW_PATTERN='[main|staging|develop]*'
update_branch_protection_rule ${NEW_PATTERN}

すると、先程と同じくスラッシュ/が含まれるブランチ以外が保護対象となりました。

$ get_branches
[
  {
    "not_protected": [
      "feature/hoge"
    ]
  },
  {
    "protected": [
      "develop",
      "main",
      "staging",
      "temp",
      "v1.2.3"
    ]
  }
]

その4

続いて[main,staging,develop]*というパターン。

NEW_PATTERN='[main,staging,develop]*'
update_branch_protection_rule ${NEW_PATTERN}

すると、先程と同じくスラッシュ/が含まれるブランチ以外が保護対象となりました。

$ get_branches
[
  {
    "not_protected": [
      "feature/hoge"
    ]
  },
  {
    "protected": [
      "develop",
      "main",
      "staging",
      "temp",
      "v1.2.3"
    ]
  }
]

その5

続いて*[main,staging,develop]というパターン。

NEW_PATTERN='*[main,staging,develop]'
update_branch_protection_rule ${NEW_PATTERN}

すると、今度は/または.が含まれるブランチ以外が保護対象となりました。

$ get_branches
[
  {
    "not_protected": [
      "feature/hoge",
      "v1.2.3"
    ]
  },
  {
    "protected": [
      "develop",
      "main",
      "staging",
      "temp"
    ]
  }
]

その6

続いて*[main|staging|develop]というパターン。

NEW_PATTERN='*[main|staging|develop]'
update_branch_protection_rule ${NEW_PATTERN}

すると、同様に/または.が含まれるブランチ以外が保護対象となりました。

$ get_branches
[
  {
    "not_protected": [
      "feature/hoge",
      "v1.2.3"
    ]
  },
  {
    "protected": [
      "develop",
      "main",
      "staging",
      "temp"
    ]
  }
]

その7

続いて[main|staging|develop]というパターン。

NEW_PATTERN='[main|staging|develop]'
update_branch_protection_rule ${NEW_PATTERN}

するとすべてのブランチが保護対象となりませんでした。

$ get_branches
[
  {
    "not_protected": [
      "develop",
      "feature/hoge",
      "main",
      "staging",
      "temp",
      "v1.2.3"
    ]
  }
]

一番良さそうなパターン

色々試した結果、[msd][ate][iav][nge]*というパターンを指定すれば、mainstagingおよびdevelopブランチにのみブランチ保護ルールを適用できました。

NEW_PATTERN='[msd][ate][iav][nge]*'
update_branch_protection_rule ${NEW_PATTERN}
$ get_branches
[
  {
    "not_protected": [
      "feature/hoge",
      "temp",
      "v1.2.3"
    ]
  },
  {
    "protected": [
      "develop",
      "main",
      "staging"
    ]
  }
]

この指定方法については下記の内容が参考になりました。

ただし、maintenance のようなブランチ名だとやはり保護対象になってしまうので、完璧なパターンとはなりませんでした。

おわりに

GitHub で、いくつかの特定のブランチ(main, staging, develop)にのみ適用されるブランチ保護ルールを作ってみました。

正規表現ではなく fnmatch とも微妙に異なるパターンを指定する必要があったため、探り探りの検証となりました。

検証の結果、ブランチごとにルールを作成するのが望ましいという結論となりましたが、リポジトリを作成するごとにルールを複数作成するのは大変なので、rulesets を使用して Organization 全体にルールを適用するなどすると良いかと思います。

参考

以上