Alternative Architecture DOJO

オルターブースのクラウドネイティブ特化型ブログです。

dotnet-formatを実行したプルリクエスト自動作成のGitOps

こんにちは、MLBお兄さんこと松村です。
MLB はトレードデッドライン (TDL) も過ぎ、プレーオフに向けて本気出すチームによる補強が目まぐるしく行われました。


みなさんソースコードのフォーマットはちゃんとやっていますか?
ここでいうフォーマットとは「整形」のことで、一定のルールに基づいてファイル内のインデントやスペース等を整えることを指します。

dotnet format コマンド

.NET アプリケーションのコードフォーマットを行うために、 dotnet format というコマンドラインがあります。
このコマンドでは .editorconfig ファイルに沿ってフォーマットが行われます。( .editorconfig ファイルが無ければ既定の設定でフォーマットされます)

github.com

例えばフォーマットがガタガタの C# ファイルに対して dotnet format コマンドを実行すると、Visual Studio で行うようなフォーマットが行われることが分かります。

$ dotnet format consoleapp/consoleapp.csproj --no-restore --verbosity diag
  The dotnet runtime version is '8.0.0-preview.6.23329.7'.
  dotnet CLI バージョンは '8.0.100-preview.6.23330.14' です。
  'C:\Program Files\dotnet\sdk\8.0.100-preview.6.23330.14\' にある MSBuild.exe を使用しています。
  ワークスペース 'C:\Users\yuta\source\github\github-actions-samples\dotnet\net8.0\consoleapp\consoleapp.csproj' でコード ファイルを書式設定します。
  ワークスペースを読み込んでいます。
  Project consoleapp is using configuration from 'C:\Users\yuta\source\github\github-actions-samples\.editorconfig'.
  Project consoleapp is using configuration from 'C:\Users\yuta\source\github\github-actions-samples\dotnet\net8.0\consoleapp\obj\Debug\net8.0\consoleapp.GeneratedMSBuildEditorConfig.editorconfig'.
  Project consoleapp is using configuration from 'C:\Program Files\dotnet\sdk\8.0.100-preview.6.23330.14\Sdks\Microsoft.NET.Sdk\analyzers\build\config\analysislevel_8_default.globalconfig'.
  1860 ミリ秒で完了します。
  書式設定可能なファイルを判定しています。
  178 ミリ秒で完了します。
  フォーマッタを実行しています。
  コード スタイル 分析を実行しています。
  診断を決定しています...
  Running 3 analyzers on consoleapp.
  1478 ミリ秒で完了します。
  診断を修正しています...
  2 ミリ秒で完了します。
  1480 ミリ秒で分析が完了します。
  アナライザー参照 分析を実行しています。
  診断を決定しています...
  Running 144 analyzers on consoleapp.
  224 ミリ秒で完了します。
  診断を修正しています...
  0 ミリ秒で完了します。
  225 ミリ秒で分析が完了します。
  2289 ミリ秒で完了します。
  コード ファイル 'C:\Users\yuta\source\github\github-actions-samples\dotnet\net8.0\consoleapp\Class1.cs' が書式設定さ
れました。
  コード ファイル 'C:\Users\yuta\source\github\github-actions-samples\dotnet\net8.0\consoleapp\Program.cs' が書式設定されました。
  2 個中 5 個のファイルが書式設定されました。
  4351 ミリ秒で書式設定が完了します。

$ git diff
diff --git a/dotnet/net8.0/consoleapp/Class1.cs b/dotnet/net8.0/consoleapp/Class1.cs
index 1ef4b1a..1c7620c 100644
--- a/dotnet/net8.0/consoleapp/Class1.cs
+++ b/dotnet/net8.0/consoleapp/Class1.cs
@@ -1,13 +1,14 @@
-    namespace consoleapp;
+namespace consoleapp;
 
 internal class Class1
-    {
-        private     string      field1  =   "hoge";
+{
+    private string field1 = "hoge";
 
-    public  string  Property1
-        =>  field1  ;
+    public string Property1
+        => field1;
 
-    public string   Greet     ()  {
-            return  $"Hello, World {  Property1  }!"     ;   // comment
-        }
+    public string Greet()
+    {
+        return $"Hello, World {Property1}!";   // comment
     }
+}
diff --git a/dotnet/net8.0/consoleapp/Program.cs b/dotnet/net8.0/consoleapp/Program.cs
index 55b1765..13b1e2c 100644
--- a/dotnet/net8.0/consoleapp/Program.cs
+++ b/dotnet/net8.0/consoleapp/Program.cs
@@ -1,6 +1,6 @@
-        // See https://aka.ms/new-console-template for more information
-    using consoleapp;
+// See https://aka.ms/new-console-template for more information

開発中にこまめに Visual Studio でフォーマットを行えばよいのですが、忘れたりすることもあるので自動化できるといいなと思っていました。
そこで GitHub Actions で実施する CI の一作業としてフォーマットを行うワークフローを作ってみました。

ワークフローの流れ

流れをざっと図にするとこちらのようになります。

プルリクエストが作成された際にそのプルリクエストのベースブランチに対して dotnet format コマンドを実行し、差分があれば(=整形されれば)自動的にコミットしてベースブランチに対してプルリクエストを作成するという構成です。
dotnet format コマンドを実行した結果、差分がなければ整形された箇所がないということになるので、プルリクエストを作成しません。

ではここから GitHub Actions のワークフローの YAML ファイルを解説します。
なお YAML ファイル全体はこちらを参照してください。

github.com

ジョブ1 : フォーマットしてブランチにコミット&プッシュ

作業ブランチに対して dotnet format を実行します。
フォーマットするだけなので C# のリストアやビルドは行う必要はありません。(--no-restore)

run-dotnet-format:
  runs-on: ubuntu-latest
  outputs:
    base-branch-name: ${{ steps.commit.outputs.base-branch-name }}
    head-branch-name: ${{ steps.commit.outputs.head-branch-name }}
    changed: ${{ steps.verify-diff.outputs.changed }}
  steps:
    - uses: actions/checkout@v3
    - name: Setup .NET
      uses: actions/setup-dotnet@v3
      with:
        dotnet-version: '8.0.x'
        dotnet-quality: 'preview'
    - name: Format
      run: |
        dotnet format consoleapp/consoleapp.csproj --no-restore --verbosity $DOTNET_FORMAT_VERBOSITY
    - name: Check if there are any changes
      id: verify-diff
      run: |
        git diff --quiet . || echo "changed=true" >> $GITHUB_OUTPUT
    - name: Commit
      if: steps.verify-diff.outputs.changed == 'true'
      id: commit
      run: |
        git checkout -b $HEAD_BRANCH
        git config user.name github-actions[bot]
        git config user.email 41898282+github-actions[bot]@users.noreply.github.com
        git add .
        git commit -m "[bot] Auto formatted"
        git push --set-upstream origin $HEAD_BRANCH
        echo "base-branch-name: $BASE_BRANCH"
        echo "base-branch-name=$BASE_BRANCH" >> $GITHUB_OUTPUT
        echo "head-branch-name: $HEAD_BRANCH"
        echo "head-branch-name=$HEAD_BRANCH" >> $GITHUB_OUTPUT
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

dotnet format コマンドによる差分の有無を判別する処理は以下のコマンドです。
git diff --quiet . || echo "changed=true" >> $GITHUB_OUTPUT

stackoverflow.com

そして差分がある場合は、独自のブランチを作成しフォーマット後のコードをコミット&プッシュしています。
このときの Git 認証は perssions かつ secrets.GITHUB_TOKEN で成立します。

ジョブ2 : プルリクエストを作成

フォーマット後のコードを含んでいるブランチは、マージまで行わないと意味がないですよね。
そのため、フォーマット後ブランチから作業ブランチへのプルリクエストを作成します。

call-pull-request-workflow:
  name: Call pull request workflow
  needs: run-dotnet-format
  if: needs.run-dotnet-format.outputs.changed == 'true'
  uses: ./.github/workflows/github-pull-request-creation.yml
  with:
    base-branch-name: ${{ needs.run-dotnet-format.outputs.base-branch-name }}
    head-branch-name: ${{ needs.run-dotnet-format.outputs.head-branch-name }}

処理かいてないじゃん?と思われるかもしれませんが、「プルリクエストを作成するワークフロー」は別途作成し再利用しています。(=Reusable workflow)
Reusable workflow については、過去に弊社ブログに登場しています。

docs.github.com

aadojo.alterbooth.com

プルリクエストを作成するワークフローを抜粋すると、このような構成になります。私は GitHub CLI を利用しています。

jobs:
  create-pull-request:
    runs-on: ubuntu-latest
    outputs:
      pull-request-url: ${{ steps.pr.outputs.url }}
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Create a pull request
        id: pr
        run: |
          url=$(gh pr create \
            --base $BASE_BRANCH \
            --head $HEAD_BRANCH \
            --title "$TITLE" \
            --body "")
          echo "url=$url" >> $GITHUB_OUTPUT
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

github.com

ジョブ3 : プルリクエストURLの出力

最後はおまけですが、自動作成されたプルリクエストの URL を出力します。
URL はジョブ2で出力(output)された URL を使用します。

show-pull-request-url:
  name: Show pull request url
  needs: call-pull-request-workflow
  runs-on: ubuntu-latest
  steps:
    - run: echo ${{ needs.call-pull-request-workflow.outputs.pull-request-url }}

このワークフロー全体をデモした動画も載せておきますので、流れをイメージしていただければと思います。
※動画は 1.4 倍速にしています(Xのアップロードサイズに収まらなかった!)

twitter.com

長らく実現したかったワークフローができたので、個人的にとても重宝しています。
このワークフローを作る過程で Reusable workflow や、ワークフロー内での Git 操作なども改めて学ぶことができたので良かったです。

似たようなことをやってみたい方はぜひ参考にしてください。

www.alterbooth.com

cloudpointer.tech

www.alterbooth.com