tech.guitarrapc.cóm

Technical updates

GitHub Actions で configure-aws-credentials を使った OIDCではまったこと

GitHub Actions の OpenID Connector と AWS の OIDC Provider を使うことで、IAM Role を Assume できるというのは前回書きました。

tech.guitarrapc.com

構築中によく出るエラーに関しても書いたのですが、いざ実際に使おうとしたら別のエラーではまったので忘れないようにメモしておきます。

tl;dr;

  • OpenID Connect で認証すると、AWS OIDC Provider の認証の上限に引っ掛かりやすい。
  • Composite Action の中で、 configure-aws-credentials を呼び出すときは注意。
  • 2021/12/10 追記: このPR が入れば自動的にリトライしてくれるで記事のプラクティスは不要になる予定。

GitHub Actions で並列実行すると時々失敗する。

エラーメッセージ Error: Couldn't retrieve verification key from your identity provider, please reference AssumeRoleWithWebIdentity documentation for requirements

対策: configure-aws-credentials で OIDC 認証を頻繁に呼び出す場合は、キャッシュかリトライしましょう。

並列でジョブを実行するなど、短時間に頻繁に認証を取得ようとすると、時々認証に失敗します。 IAM Userを使っていると当然でないのですが、OIDC Providerで認証をかけようとするとぐさっと刺さります。

これを回避するには、リトライかキャッシュ作戦をとる必要があります。

github.com

workflow で一回だけ認証ととって、キャッシュを使いまわす場合次のような workflow になります。

gist.github.com

ただ、GitHub Actions の cache は 「パブリックリポジトリのキャッシュには、センシティブな情報を保存しないことをおすすめします。」とある通り取扱いには注意だと思います。トークン自体 は 900sec (min) か 1 hour (default) で切れますが、時間の問題ではないという。

Warning: We recommend that you don't store any sensitive information in the cache of public repositories. For example, sensitive information can include access tokens or login credentials stored in a file in the cache path. Also, command line interface (CLI) programs like docker login can save access credentials in a configuration file. Anyone with read access can create a pull request on a repository and access the contents of the cache. Forks of a repository can also create pull requests on the base branch and access caches on the base branch. https://docs.github.com/en/actions/advanced-guides/caching-dependencies-to-speed-up-workflows

configure-aws-credentials を1 jobで複数回呼び出したときに初回の認証を上書きできない

対策: configure-aws-credentials の呼び出しは一回の composite action で行うか、workflow で呼び出しましょう。

通常 configure-aws-credentials は、呼び出しのたびに job 内の認証を後勝ちで上書きします。 ただし、composite action の中で、configure-aws-credentials を呼び出すと、意図しない結果になるパターンがあります。

configure-aws-credentials を呼び出す処理を composite action に書いて、1 job内で 別の IAM Role の Assume を呼びだすと 2 回目の configure-aws-credentials で上書きできないので避けましょう。

github.com

正常動作例1

次のように workflow 上で連続でconfigure-aws-credentialsを呼び出すと、2回目の configure-aws-credentials で myrole_Bに上書きできていることが確認できます。

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
     # 1st
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@master
        with:
          aws-region: ap-northeast-1
          role-to-assume: arn:aws:iam::123456789012:role/myrole_A
          role-session-name: GitHubActions-${{ github.run_id }}
      - name: get-caller-identity shows myrole_A as expected
        run: aws sts get-caller-identity
      # 2nd
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@master
        with:
          aws-region: ap-northeast-1
          role-to-assume: arn:aws:iam::123456789012:role/myrole_B
          role-session-name: GitHubActions-${{ github.run_id }}
      - name: get-caller-identity shows myrole_B as expected
        run: aws sts get-caller-identity

1回目のget-caller-identity は myrole_A です。

{
    "UserId": "AROASJXUOK5UM7XZKRYTB:GitHubActions-1426675663",
    "Account": "***",
    "Arn": "arn:aws:sts::***:assumed-role/myrole_A/GitHubActions-1426675663"
}

2回目のget-caller-identity は myrole_B です。

{
    "UserId": "AROASJXUOK5UM7XZKRYTB:GitHubActions-1426675663",
    "Account": "***",
    "Arn": "arn:aws:sts::***:assumed-role/myrole_B/GitHubActions-1426675663"
}

意図通りの挙動です。

正常動作例2

Composite Action の中で configure-aws-credentials をまとめて2回呼び出すと、2回目の呼び出しで myrole_B に上書きできていることが確認できます。

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Configure AWS Credentials
        uses: ./.github/actions/aws_oidc_auth_all

aws_oidc_auth_all はこうなっています。

# ./.github/actions/aws_oidc_auth_all/action.yaml
name: aws oidc auth
description: |
  Get aws oidc auth.
runs:
  using: "composite"
  steps:
    # 1st
    - name: Configure AWS Credentials (Role A)
      uses: aws-actions/configure-aws-credentials@master
      with:
        aws-region: ap-northeast-1
        role-to-assume: arn:aws:iam::123456789012:role/myrole_A
        role-session-name: GitHubActions-${{ github.run_id }}
    - name: get-caller-identity shows myrole_A as expected
      run: aws sts get-caller-identity
      shell: bash
    # 2nd
    - name: Configure AWS Credentials (Role B)
      uses: aws-actions/configure-aws-credentials@master
      with:
        aws-region: ap-northeast-1
        role-to-assume: arn:aws:iam::123456789012:role/myrole_B
        role-session-name: GitHubActions-${{ github.run_id }}
    - name: get-caller-identity shows myrole_B as expected
      run: aws sts get-caller-identity
      shell: bash

1回目のget-caller-identity は myrole_A です。

{
    "UserId": "AROASJXUOK5UM7XZKRYTB:GitHubActions-1426687022",
    "Account": "***",
    "Arn": "arn:aws:sts::***:assumed-role/myrole_A/GitHubActions-1426687022"
}

2回目のget-caller-identity は myrole_B です。

{
    "UserId": "AROASJXUOK5UHN4XWD3XF:GitHubActions-1426687022",
    "Account": "***",
    "Arn": "arn:aws:sts::***:assumed-role/myrole_B/GitHubActions-1426687022"
}

意図通りの挙動です。

問題の動作

Composite Action の中で configure-aws-credentials を それぞれのIAM Role について呼び出すと、2回目の呼び出しが myrole_A のままで myrole_B で上書きできていないことが確認できます。

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      # 1st <- Shows myrole_A, expected.
      - name: Configure AWS Credentials (Role A)
        uses: ./.github/actions/aws_oidc_auth_single
        with:
[https://docs.github.com/en/actions/creating-actions/creating-a-composite-action:embed:cite]


          role-to-assume: arn:aws:iam::123456789012:role/myrole_A
      # 2nd <- Shows myrole_A, unexpected!!
      - name: Configure AWS Credentials (Role B)
        uses: ./.github/actions/aws_oidc_auth_single
        with:
          role-to-assume: arn:aws:iam::123456789012:role/myrole_B

aws_oidc_auth_single はこうなっています。

# ./.github/actions/aws_oidc_auth_single/action.yaml
name: aws oidc auth
description: |
  Get aws oidc auth.
inputs:
  role-to-assume:
    description: "AWS IAM Role to assume 1"
    required: true
runs:
  using: "composite" # this is key point
  steps:
    - name: Configure AWS Credentials
      uses: aws-actions/configure-aws-credentials@master
      with:
        aws-region: ap-northeast-1
        role-to-assume: ${{ inputs.role-to-assume }}
        role-session-name: GitHubActions-${{ github.run_id }}
    - name:  get-caller-identity shows myrole_A on both 1st and 2nd run. (2nd run must be myrole_B but incorrect result.)
      run: aws sts get-caller-identity
      shell: bash

1回目のget-caller-identity は myrole_A です。

{
    "UserId": "AROASJXUOK5UM7XZKRYTB:GitHubActions-1426687028",
    "Account": "***",
    "Arn": "arn:aws:sts::***:assumed-role/myrole_A/GitHubActions-1426687028"
}

2回目のget-caller-identity も myrole_A です。myrole_B に上書きできていません。

{
    "UserId": "AROASJXUOK5UHN4XWD3XF:GitHubActions-1426687028",
    "Account": "***",
    "Arn": "arn:aws:sts::***:assumed-role/myrole_A/GitHubActions-1426687028"
}

aws-actions/configure-aws-credentials の中身を見ても、composite action 特有の処理は当然存在しません。 composite action の仕様かと思いつつ、そういった記述もないので謎です。

仕方ないので、正常動作例1,2 のいずれかを用いることになるでしょう。

あるいは、両方の権限をもつ単一Roleでやるのも手でしょう。

GitHub Actions が OpenId Connect に対応したので AWS OIDC Provider と連携する

前々から言われていた GitHub Actions で OpenID Connect 経由で、各種Cloud Provider の認証を得るのが GA しました。 めでたい。

github.blog

これにより、aws-actions/configure-aws-credentials のみで認証が組めるようになったので見てみましょう。

github.com

tl;dr;

  • GitHub Actions でAWS操作をするために、IAM User が不要になるので神。
  • セルフホストな GitHub Actions を EC2 で動かしているなら、素直に Instance Role を使うので十分というのもある。(何か事情があれば OpenId Connector でもいい)
  • AWS アカウントで OpenId Connect Provider は 一意です。1 AWSアカウントで複数環境を持っている & 環境ごとに IAM Role を持っている場合は、環境ごとに OpenId Conect Provider を作ろうとして失敗しないように。
  • 並列度高く認証を取得しようとすると失敗することが多いので注意

動作例

設定がただしければ、必要な IAM Role Arn を role-to-assume に指定するだけで、その Role 権限で 操作ができるようになります。

これで

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@master
        with:
          aws-region: ap-northeast-1
          role-to-assume: "作ったIAM Role の Arn を入れる"
          role-session-name: "適当な名前"

こうなる。

f:id:guitarrapc_tech:20211105022035p:plain
GitHub OpenId Conector と AWS OIDC Provider でAWS の操作ができる

基本

GitHubから公式にドキュメントが公開されたのでこれに従いましょう。先日までuses: aws-actions/configure-aws-credentials@v1 になってたせいでドキュメントが嘘つきでしたが @master に修正されています。

docs.github.com

なお、https://github.com/aws-actions/configure-aws-credentials は、uses: aws-actions/configure-aws-credentials@v1 になっているので動きません。uses: aws-actions/configure-aws-credentials@master に読み替えましょう。

Terraform で AWSを用意する。

用意する必要があるのは、OIDC Provider と IAM Role です。

  • OIDC Provider は、GitHub OIDC と信頼関係を結ぶのに必要です。
  • IAM Role は OIDC Provider 経由で GitHub OIDC でリクエストされたときに、リクエスト元のリポジトリオーナー/リポジトリ名:ブランチ名 を検証し条件にマッチしたらそのRoleを Assume して利用できるようにします。つまり、このRoleに、リポジトリの制約と必要な IAM Policyを振ればok。

全体を示します。

gist.github.com

OIDC Provider を用意する

値は固定なので単純です。terraform ならこうなります。

// oidc provider
resource "aws_iam_openid_connect_provider" "main" {
  url             = "https://token.actions.githubusercontent.com"
  client_id_list  = ["sts.amazonaws.com"]
  thumbprint_list = ["a031c46782e6e6c662c2c87c76da9aa62ccabd8e", "6938fd4d98bab03faadb97b34396831e3780aea1"] // 2022/1/11 に 中間証明書更新された
}

thumbprint に関しては 中間証明書の寿命が1年なので、毎年1/11 に代わっていくはずです。 thumbprint はコマンドでとれるので、更新直後などドキュメントが間に合ってないときは適当に対応しましょう。

Get Thumbprint of GitHub OIDC, updated on 2022/01/13. · GitHub

client_id_list がGA前と後で変わっています。古いバージョンではここで、リクエスト元のリポジトリURL を指定していましたが、今は sts.amazonaws.com といつものになりました。標準に沿ってくれてよかった。

GitHub の Endpoint が確認できますね。

https://token.actions.githubusercontent.com/.well-known/openid-configuration

{"issuer":"https://token.actions.githubusercontent.com","jwks_uri":"https://token.actions.githubusercontent.com/.well-known/jwks","subject_types_supported":["public","pairwise"],"response_types_supported":["id_token"],"claims_supported":["sub","aud","exp","iat","iss","jti","nbf","ref","repository","repository_owner","run_id","run_number","run_attempt","actor","workflow","head_ref","base_ref","event_name","ref_type","environment","job_workflow_ref"],"id_token_signing_alg_values_supported":["RS256"],"scopes_supported":["openid"]}

IAM Role を用意する

順番に解説します。

重要なのは、Assume Role です。 Assume Role で OIDC Provider からのリクエストを検証しています。

  • principal は、認証を委譲されて受けるので type: "Federated" + 先ほど作った OIDC Provider の arn を指定 します。
  • condition で、リクエスト元が repo:<GitHubOwner>/<Repositry>:Branch の条件とマッチするか検証します。

OIDC 経由で、許可した「リポジトリ、かつブランチ」だった場合に IAM Role をsts:AssumeRoleWithWebIdentity としてAssume できるようにします。

さて、複数のリポジトリで同じロールを使いたいケースはどうすればいいでしょうか?

IAM Policy の condition は、ワイルドカード一致するかを StringLike で検証できますが、これは value が1要素 (=単一リポジトリ) のときにしか機能しません。 value が複数要素(=複数リポジトリ)でもワイルドカードで一致するかを見る場合は、ForAnyValue: + StringLike を用います。

今回は 分岐させましたが、別に ForAnyValue で初めから書いてもいいでしょう。

data "aws_iam_policy_document" "github_oid_assume_role_policy" {
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRoleWithWebIdentity"]
    principals {
      type        = "Federated"
      identifiers = [aws_iam_openid_connect_provider.main.arn]
    }
    # aud があるとはじかれてるので、aud の値がおかしいっぽい。 aws-actions/configure-aws-credentials の仕組みからすると、sts.amazonaws.com が来るはず。 <- github.com/aws-actions/configure-aws-credentials ではなしになってる :(
    # condition {
    #   test     = "StringEquals"
    #   variable = "token.actions.githubusercontent.com:aud"
    #   values   = ["https://github.com/${var.github_owner}"]
    # }
    condition {
      test     = length(var.github_oidc_repo_names) == 1 ? "StringLike" : "ForAnyValue:StringLike"
      variable = "token.actions.githubusercontent.com:sub"
      values   = [for item in var.github_oidc_repo_names : "repo:${var.github_owner}/${item}:*"]
    }
  }
}

AssumeRole ができてしまえば IAM Roleを作るだけです。今回は、aws sts get-caller-identity を実行できるようにしてみましょう。

特にいうことはないですね。終わり。

data "aws_iam_policy_document" "github_actions" {
  // allow running `aws sts get-caller-identity`
  statement {
    effect    = "Allow"
    actions   = ["sts:GetCallerIdentity"]
    resources = ["*"]
  }
}

resource "aws_iam_policy" "github_actions" {
  name        = "githubactions_policy"
  path        = "/"
  description = "Policy for GitHubActions"
  policy      = data.aws_iam_policy_document.github_actions.json
}
resource "aws_iam_role" "test_role" {
  name               = "githubactions-oidc-role"
  path               = "/"
  assume_role_policy = data.aws_iam_policy_document.github_oid_assume_role_policy.json
  policy_arns = [
    aws_iam_policy.github_actions.arn
  ]
}

リソースを作成

さて、これで AWS にリソースを作るとこんな感じになります。

今回は私は、guitarrapc/githubactions-lab と guitarrapc/infrastructure の 2リポジトリから GitHub Actions 経由で認証を受けられるようにしました。 IAM Role の Trust relationships > Conditions で2リポジトリが ForAnyValue:StringLike で指定されているのがわかりますね。

f:id:guitarrapc_tech:20211105021103p:plain
IAM > Identity providers > token.actions.githubusercontent.com

f:id:guitarrapc_tech:20211105021210p:plain
IAM > Roles > githubactions-oidc-role

ここまでのどれかにミスがあると Assume Role されません。 うまくいかない場合は何度も見直すことになるでしょう。

GitHub Actions の構成

Workflow を用意します。 キーポイントは3つです。

  • permissions で、id-token: write で書き込み権限が必要です。 permissions: write-all でもいいのですが、現状では permissions を指定しないとうごかないので注意です。
  • role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}secrets.AWS_ROLE_TO_ASSUME は、GitHub Secret に AWS_ROLE_TO_ASSUME という名前で先ほど作った IAM Role の Arn を仕込んであります。Arnのフォーマットは、今回の例なら arn:aws:iam::xxxxxxxxxxxxx:role/githubactions-oidc-role というフォーマットになります。
  • role-session-name で、CloudTrail イベントにユーザー名が出るのでいい感じの名前にしましょう。
  • uses: aws-actions/configure-aws-credentials@master を指定します。 uses: aws-actions/configure-aws-credentials@v1 でないので注意してください。

github.com

name: aws oidc credential

on:
  workflow_dispatch:
  push:
    branches: ["main"]

# allow use id-token
permissions:
  id-token: write # required!
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Configure AWS Credentials
        # must use "master", not "v1". v1 is not yet released to use latest role-to-assume.
        # Error: Credentials could not be loaded, please check your action inputs: Could not load credentials from any providers
        uses: aws-actions/configure-aws-credentials@master
        with:
          aws-region: ap-northeast-1
          role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
          role-session-name: GitHubActions-${{ github.run_id }}
          role-duration-seconds: 900 # minimum: 900sec, maximum: iam role session duration
      - name: get-caller-identity is allowed to run on role.
        run: aws sts get-caller-identity

実行してみると、冒頭のスクショのように成功するはずです。

$ aws sts get-caller-identity
{
    "UserId": "AROASJXUOK5UM7XZKRYTB:GitHubActions-1420393244",
    "Account": "***",
    "Arn": "arn:aws:sts::***:assumed-role/githubactions-oidc-role/GitHubActions-1420393244"
}

うまくできましたか? おめでとうございます!

ダメでした? 私も失敗を繰り返したので、経験したエラーと対処をFAQに載せておきます。

FAQ

GitHub Actionsで実行してみたら動作しない。

エラーメッセージ Credentials could not be loaded, please check your action inputs: Could not load credentials from any providers

この場合、AWS OIDC Provider の設定がおかしい or GitHub Actions の permissions が抜けています。

  • (AWSの設定ミス) OIDC Provider の client_id_list に "sts.amazonaws.com" ではなく、リポジトリのURL を指定している。(古いブログが記事がそうなっている)
  • (GitHub Actions Workflowの設定ミス) Workflow で、permissions がない。デフォルトの GitHub Actionsは write-all のはずですが、まだ id-tokens は含まれていないようです。明示的に permissions: write-all を指定するか、id-token: write を指定しましょう。

GitHub Actionsで実行してみたら動作しない。

エラーメッセージ Not authorized to perform sts:AssumeRoleWithWebIdentity

このケースは、OIDC Provider は問題なく、AssumeRole の設定にミスがあります。

  • (AWSの設定ミス) AssumeRole の Action が "Action": "sts:AssumeRoleWithWebIdentity" になっていない。
  • (AWSの設定ミス) 複数リポジトリなのに ForAnyValue:StringLike ではなく StringLike で判定している。
  • (AWSの設定ミス) AWS の IAM Role の Assume Policy が設定しようとしている GitHub Owner/Repository:ブランチ と内容と一致していない。

過去にある類似記事との差分

GA前の内容で、結構ずれています。GA前の記事の内容でやると失敗するので注意です。(aws-actions/configure-aws-credentials が v1 で OIDC 変更を反映すればいける)

dev.classmethod.jp

zenn.dev

github.com

Git GUIクライアントと GitKraken と Fork

Git の GUI クライアント、いろんなツールがあってそれぞれ使いやすさがあります。

ここしばらくはForkをメインで使っていた中で、私がForkに感じた良さと苦手なことをメモしておこうと思います。

普段私は、GitKraken をメインにしていますが、サイズの大きなリポジトリでは Fork を利用しています。(ここ最近は手になじませるため、Forkをメインにしている)

私がgitで利用するのは、主にコードのコミット (Unity や C#、Go、React 他)、並びに Git LFS です。 バイナリファイルや、PSD や画像ファイルを編集してコミットする機会は少ないので無視します。

コミット頻度は高く、ブランチを切ったり、マージ操作、ファイルごとの差分確認、stageのフラット/ツリーでの確認、コンフリクト解消を重視しています。

tl;dr;

  • SourceTree 使っているなら、 Fork は上位互換。有料なのだけネック。
  • GitKraken 使っているなら、 Fork はgit操作が高速でUIもロックされないのが快適。ただ、UIのコンセプトが違うので一つ一つの操作が手間に感じる。
  • Git GUI クライアント、まだまだ全然決定版がない。

SourceTree使ってた、 あるいは10GB 超えるようなリポジトリだったら、私は Fork 使います。そんなに大きなリポジトリじゃないなら、GitKraken 使っているのが幸せです。

GitKraken が Fork の早さをもって、ファイルロックが起こらなければ最強といえるんですがそんなものはない。あるいは、Forkが GitKraken のように操作ごとの視点の動きを考慮したUIになればいいですが、そんな未来も来ないでしょう。諸行無常。

Fork とは

Fork は、無料で試用ができる有料Git GUIクライアントです。$49.99 (One-time purchase)

試用のみ無料ですが、無料版は現在存在しないと明言されています。その上の発言と一貫性取れてない気がするけど。有料の価値はあるいい製品です。

git-fork.com

OAuth 認証が必要なので、Organizationで使うには Fork をOAuth 許可する必要があります。

SourceTree と非常に似通っているので、SourceTreeを使っていたら違和感なく移行できるでしょう。 Git操作が高速なのが特徴で、例えば10GB越えのリポジトリでUnity で3DモデルやLFSを使っていても、checkout、diff、stage、commit、push、pullのいずれも重いと感じることなく操作ができます。

製品としては、SourceTree の高速版という印象でほぼずれはないと思います。

SourceTree比較のFork

SourceTree を使っていると Fork は気持ちいいぐらい使いやすいと思います。

有料なだけの価値があります。

pros:

  • どの操作でも固まらない
  • ファイルdiff 違和感はなく高速。
  • 操作UI も違和感ほぼなし
  • push/pull が爆速
  • checkout が爆速
  • tree が SourceTree より見やすい

cons:

  • 有料
  • OAuth 認証が必要

特筆

特筆することはないです。 SourceTree 使っているなら使いやすくて軽い、でも有料かー。という印象に落ち着くと思います。

GitKraken比較のFork

GitKraken で重いと感じているならFork は高速にgit操作できるのですごくいいです。 一方で、GitKraken に慣れている場合、Forkは一つ一つの操作で目線を移動する必要があり一貫した操作性がないのが使いにくいと感じます。

有料の価値があるかは、GitKrakenが重いと感じるか次第。

pros:

  • push/pull が爆速。
  • checkout が爆速。
  • リポジトリごとの操作が非同期でロックされないので快適。
  • 素直なgit。hooks など妙な挙動がない。
  • カスタムコマンドができる。(私はいらないけど)

cons:

  • diff をファイルの下に出すのはごみといわざるをえない。
  • 操作していて、視点があっちこっち見ないといけない。
  • 画面内でタブを使っているので、一覧しても見えないものがある。
  • branch作成、ammend など些細な操作が厳しい。
  • tree が厳しい、見やすくはない。
  • repo 初期化はセルフサービス。
  • repo 追加もセルフサービス。検索などない。
  • Conflict 解消がやりにくすぎる、Forkでやるの無理では...
  • SSH 鍵生成などもセルフサービス。
  • PR は Web でどうぞスタイル。
  • ライセンス管理は原始的。

特筆

視点の移動はかなりあって、私がGitKraken に慣れてるときに Fork を久々に触って使いにくく感じた原因はこれです。 コンセプトの違いはあると思いますが、油と水ぐらい違う。

またツリーに関しては、好みがありますが、結構読みやすさが違います。 個人的に Fork のツリーはよくある表示ですが、コミットメッセージに食い込んでて、普段見せたいもの(head/changeの有無/コミット一覧)と、必要に応じて見たいもの(ブランチ名、commit user、commit id、commit日付) の区別がついていないと思います。

f:id:guitarrapc_tech:20211104020627p:plain
上 Fork の ツリー、下 GitKraken の ツリー

GitKraken の評価

Forkだけ評価しても一方的なので GitKraken も書きます。

GitKraken は、個人利用は無料利用が可能な有料Git GUIクライアントです。$4.95 per user/month (paid annually)

無料版だと OSSのみ + VCS が github.com だけだったり制約があります。有料の価値はあるいい製品です。

www.gitkraken.com

OAuth 認証が必要なので、Organizationで使うには GitKraken をOAuth 許可する必要があります。

総じて良く、私は個人的には GitKraken が最も好きな GUI Git クライアントです。 リポジトリが小さい限りは cons に挙げたデメリットはほぼ発生することなく、非常に快適に使えます。 いろいろ GUI Git を使ってきていますが、最も操作しやすく、UIとツリーが洗練されていると感じます。

Fork とは全然違うので、pros/cons を挙げておきます。

pros:

  • git 操作で操作時に視点を移動させないでいい
  • ツリーが見やすい
  • diff を見ながら表示を3パターンに変更できる。(Hunk/Inline/Split)
  • Conflict 解消がしやすい、優秀。
  • Terminal 統合ができるので、CUI 派でも使いやすい。Terminal なら重くもないし。
  • LFS 操作に意識することがない (LFSが記述された .gitattributes がルート配下にある前提)
  • Profile があり、Profileごとにメアド、VCS認証など設定を持つことができます。
  • git terminal の設定や SSH鍵の生成、GitHubへの登録など一通りの操作が完結する。
  • 有料版で各種VCS に対応している。(商用するなら有料版なので自然とそうなるはず)
  • 商用で、ライセンスのシート管理が可能。Proでも個人でライセンス管理しやすい。

cons:

  • カスタムコマンドはない。
  • 商用利用には有料版が必要。
  • .git が大きなリポジトリ (10GB超えぐらい) でpush/pull が重い。(diff/stage/commit は影響なし、push/pull/checkout が影響を受けやすいです)
  • 一つの操作ごとにUIがロックされるので、連続して別のリポジトリタブに行って操作などができない。
  • git hooks に余計な処理を挟んでおり、まれにバグって詰む (どうしようもない)
  • GitKraken がgit内部のファイルをつかんでいることがあり、Windowsでファイルを消せないことがまれによくある。
  • ディレクトリをロックしていてチェックアウト失敗することがまれによくある。
  • background fetch が走っていて、無言で操作できないことがまれによくある。

特筆

Gitkraken で日常的に困る可能性があるのが、リポジトリが大きいと重いこと & 操作のロックでしょう。 また、サイレントにファイルロックしていることによるストレスがたまにあります。

一方で、使いやすさに関してはすごくいい... UI設計者すごい、尊敬します。

基本的に、GitKraken で git の一連の捜査 (clone, fork、pr) が完結することが目指されており、実際実現できています。 目指すゴールとコンセプトがはっきりしているのは良いことです。

UIの秀逸さ

GitKranen は、git 操作における視点をよく考えてUI/UX が設計されていると思っています。 画面内の要素はタブなどで表示が隠されていることがないので、操作によらず一貫した見方ができます。

また、git操作で視点はばらつくことがほぼないのが特徴です。例として、次のような画面を用意しました。

f:id:guitarrapc_tech:20211104000337p:plain
GitKrakenのUI

  • ブランチの操作をしたいときは左ペインを見て、中ペインはツリーを示す。
  • コミットのためのファイル操作をするときは右ペインを見て、中ペインに大きくdiff を示す。
  • よく使うgit操作の Pull/Push/Branch/Stash/Pop/Terminal などは上部にボタンを用意

操作を追ってみましょう。 常に視点が、 中ペイン + 左ペイン or 右ペイン + 中ペイン と動かさなくて済むようになっているのがわかります。

ブランチを切る

まずはブランチを切ります。

やりたいことに必要なのは、ツリーの状態とブランチ操作です。 そのため、視点は中ペイン (ツリー) と左ペイン (ブランチ) に限定されます。

  • ツリーの状態を見る (中ペインのツリーを見る)
  • ブランチ状況を確認する (左のブランチ一覧を見る)
  • ブランチを切る (左のブランチ一覧から追加 -> 中ペインのツリーを見る)

f:id:guitarrapc_tech:20211104000810p:plain
ファイル変更があるときの画面1

コミットをする

ローカルのファイル変更をしたのでコミットをします。

やりたいことに必要なのは、ツリーの状態とファイル一覧とファイルごとのdiff です。 そのため、視点は中ペイン (ツリー/diff) と右ペイン (ファイル一覧) に限定されます。 diff の時に、中ペインがツリーではなくdiff を大きく表示するのがよく考えられていると思います。

  • ローカル変更を確認する (右のファイル一覧、ファイルごとのdiffは中ペインのdiffを見る)
  • stage 操作をする (右のファイル状況で変更を確認)
  • コミットする (右のファイル状況で変更を確認)

f:id:guitarrapc_tech:20211104000858p:plain
ファイル変更があるときの画面 2

f:id:guitarrapc_tech:20211104000952p:plain
ファイル変更があるときの画面3

Push & PR作成

コミットしたので、pushしたり PR を作ったりします。

やりたいことに必要なのは、ツリーの状態とブランチ操作です。 そのため、視点は中ペイン (ツリー、pushボタン) と左ペイン (ブランチ一覧、PR一覧) に限定されます。

  • プッシュする (上の操作ペイン)
  • PR を作る (左のブランチ一覧を見る)

f:id:guitarrapc_tech:20211104001112p:plain
PRを作るときの画面

文章とスクショで言われてもよくわからない場合、無料版を触るといいです。

.git が大きなリポジトリ (10GB超えぐらい) でpush/pull が重い

3GB 程度なら快適ですが、10GB 程度になると push / pull が重くなります。 これは明確に CPU やネットワークにかかわらず起こるので欠点です。

fork ぐらい早くなれば完全にお勧めできるんですが、重いを感じるレベルになるリポジトリでは欠点が目立つ可能性があります。

git hooks のつらさ

めったにないのですが、この間起こって詰みました。 以前は適当に clone しなおしたら行けたんですが今回はダメっぽくて困りました。(今見たらいけてる、謎)

Fork の縦ペインで視点移動は減らせるのか

Fork を水平、縦でペイン表示変更してみて視点移動を減らせるか試してみたので、Twitter にあげたのをぺたり。 結果は、縦なら少しは良くなるけど、diff がどうしようもなく使いにくい。

LinqPad の設定

LinqPad 7 へのアップグレードが可能になっています。 今現状は LinqPad 7 ではなく 6になりますが、すでに LinqPad 6ライセンス持っている人は今なら早期アップグレードでディスカウントが大幅に利くのでお得です。具体的には、Premium 使ってても Pro の新規ライセンスより安くなるので好き。

Windows 11 でクリーンしたついでに LinqPad も入れなおしたので設定ついでにメモ。

tl;dr;

  • LinqPad で C# を書くときに便利にしている設定を公開する
  • 基本デフォルトに沿うようにしているので、最低限しか設定しない。

before & after

f:id:guitarrapc_tech:20211017072515p:plain
before

f:id:guitarrapc_tech:20211017072622p:plain
after

設定一覧

設定は基本的に、Edit > Preference から行えます。

Edit > Preference > Editor

  • Show line numbers in editor: True (行を表示します)

f:id:guitarrapc_tech:20211017070706p:plain
Edit > Preference -> Editor

Edit > Preference > Query

  • Default Query Language: C# Program
  • Enable Nullable Refrence Types in C# queries by default: True

f:id:guitarrapc_tech:20211017070636p:plain
Edit > Preference -> Query

Edit > Preference > Advanced

  • Convert tabs to spaces: True

f:id:guitarrapc_tech:20211017071654p:plain
Edit > Preference -> Advanced

LinqPad はいいぞ

会社だと Enterprise がやばいぐらいお得なのでおすすめ。C# 書くなら福利厚生といえるかもしれません、しらんけど。

Enterprise License (unlimited users, up to 10 locations) が $1390 は安すぎる。

f:id:guitarrapc_tech:20211017072204p:plain

GitHub Actions のローカル Composite Action で歯がゆいこと

GitHub Actions の Composite Action (複合ステップアクション) は便利なのですが、制約や歯がゆいことが多く悩ましいものがあります。

では何が難しいと感じているのか、その対処をどうしているのかメモしておきます。

tl;dr;

  • Composite Action は run のみ使える。uses は使えないからあきらめて。
  • Composite Action は run.if が使えないので bash if で分岐しよう。
  • Composite Action でスクリプト使うならコンテナ実行時にパス狂うから気を付けて
  • Compoiste Action の全 run ステップは Grouping log lines を使おう、絶対だ。

Composite Actions とは

GitHub Actions は、Jobで実際にやる処理一つ一つを step として記述できます。 この step で run: を使っていろいろな処理を書いたり uses: を使ってアクションを呼び出したりしていることでしょう。

さて、プロジェクトでいろいろな workflow を用意していくと、似通った run step を記述していてまとめ上げたくならないでしょうか。 TypeScriptやDockerアクションにするというわけではなく、単純に run step のYAMLを分離して呼び出すことで共通化したい。

こんな時に便利なのが Composite Action です。

docs.github.com

Composite Actions の利用例

例えば、次のように jobA, jobB, jobC それぞれで dotnet build / publish をしているときに、このdotnet 処理を別のYAMLに記述して呼び出せれば便利、みたいな感じです。(これを分離するのに価値があるかはおいておいて、まとめ上げられるというのに注目)

jobs:
  jobA:
    runs-on: ubuntu-latest
    steps:
      - run: dotnet restore
      - run: dotnet build -c Debug
      - run: dotnet publish -c Debug
      - run: nanika yaru

  jobB:
    runs-on: ubuntu-latest
    steps:
      - run: dotnet restore
      - run: dotnet build -c Debug
      - run: dotnet publish -c Debug
      - run: betsu no nanika yaru

  jobC:
    runs-on: ubuntu-latest
    steps:
      - run: dotnet restore
      - run: dotnet build -c Release
      - run: dotnet publish -c Release
      - run: tondemo naikoto yaru

Composite Actions を使うようにしてみましょう。 やることは単純です。ローカルAction として、.github/actions/dotnet_build/actions.yaml を定義して、dotnet build の記述を移します。 外から実行に値を受けるなら、inputs で指定するのは workflow_dispatch などと同じで一貫性が取れています。

name: .NET Build
description: |
  .NET Build
inputs:
  build-config:
    description: "dotnet build config. Debug|Release"
    default: "Debug"
    required: false
runs:
  using: "composite"
  steps:
      - run: dotnet restore
        shell: bash
      - run: dotnet build -c ${{ inputs.build-config }}
        shell: bash
      - run: dotnet publish -c ${{ inputs.build-config }}
        shell: bash

あとは、元の workflow で呼び出すだけです。簡単ですね。

jobs:
  jobA:
    runs-on: ubuntu-latest
    steps:
      - name: .NET Build
        uses: ./.github/actions/dotnet_build
      - run: nanika yaru

  jobB:
    runs-on: ubuntu-latest
    steps:
      - name: .NET Build
        uses: ./.github/actions/dotnet_build
      - run: betsu no nanika yaru

  jobC:
    runs-on: ubuntu-latest
    steps:
      - name: .NET Build
        uses: ./.github/actions/dotnet_build
        with:
          build-config: Release
      - run: tondemo naikoto yaru

Composite Action 利用時の注意

一見すると簡単で便利、最高って感じですが、Composite Actions は微妙に歯がゆいことがいくつかあります。 ということで、使うときはこれだけ気を付けておくといいです。(順次改善されて行ってほしい)

1. 使えるのは run: のみ (制約->改善済み)

感想: uses: 使えるようになってほしいけど無理そう 2021/8/26 に uses が使えるようになりました。 GitHub Actions: Reduce duplication with action composition | GitHub Changelog

Composite Action で使えるのは、 run: のみで uses: は使えません。 そのため、外部 Actions の呼び出しや別の composite action の呼び出しができません。

これが地味につらいところです。 たいがいは uses をいくつか使っているので、結果そのジョブを丸っと Composite Action に移して実行するというのはたいがいできません。

ほぼ毎回、runs: 部分をより分けてどれを composite action にするか検討することになるでしょう。 ただ分離したいだけなのに、というわけにはいかないのです。

2. run.if は使えない (制約)

感想: これはできるようになっていいのでは

run step は、実行するかどうかを決定する if コンディションがありますが、Composite Action の run で if: <expression> は使えません。 このため、元の run が if を使っていた場合、run: 処理の中で bash if を使って分岐することになったりします。なるほどねー。

# これはだめ
      - if: ${{ env.HOGE == 'hoge' }}
        run: do something
        shell: bash

# こうなる
      - run: |
          if [[ "${{ env.HOGE }}" == "hoge" ]]; then
            do something
          fi
        shell: bash

if 分岐を多用していると地味にめんどくさいので、ちまちま bash if にするか shell script に処理を書いてまとめたりします。

3. container で実行すると github.action_path パスが狂う (歯がゆい)

感想: 地味に罠なのでなおして~

Composite Actions の今のパスは github.action_path でとれます。このため、Composite Action で使うスクリプトは同じパスに置いておく、とかできます。

${{ github.action_path }}/prepare_env.sh

しかしコンテナで実行するときは狂うので、仕方ないので ${{ job.container.id }} でコンテナ環境か判定して、${{ github.workspace }}${{ github.action_path }} で修正してあげましょう。 これやらずパス参照で書けばいいやと思うと、actions のフォルダ名を変えるたびに毎回YAMLを修正しないと行けなくてつらいので。

やっておくのオススメです。

4. 1ステップで実行されるのでログの区切りがつかない (歯がゆい)

感想: すべての Composite Cction でやらないとつらいので大変めんどくさい

Composite Actions は、端的に言うと 呼び出し側の1 step で 呼び出した run がすべて実行されます。 つまり、1 step ログに、呼び出したすべての処理の標準出力がでるので、どの処理がどの出力か区別がつきません。

このため、Grouping log lines を使って処理ごとにログ出力をグループ化しましょう。絶対やりましょう。

::group::{title}
::endgroup::

docs.github.com

先ほどのサンプルはやってませんね、ダメな奴です。 アレに適用して次のようにすると、dotnet restore / dotnet build / dotnet publish がそれぞれグループ化されます。(こうなると、name もつけたくなるのでつけてます)

name: .NET Build
description: |
  .NET Build
inputs:
  build-config:
    description: "dotnet build config. Debug|Release"
    default: "Debug"
    required: false
runs:
  using: "composite"
  steps:
      - name: restore packages
        run: |
          ::group::Restore packages
            dotnet restore
          ::endgroup::
        shell: bash
      - name: build
        run: |
          ::group::Build
            dotnet build -c ${{ inputs.build-config }}
          ::endgroup::
        shell: bash
      - name: publish binaries
        run: |
          ::group::Publish binaries
            dotnet publish -c ${{ inputs.build-config }}
          ::endgroup::
        shell: bash

個人的には、name で自動的にグループ化してほしい気もありますが、ユーザーの好きなようにコントロールさせるために何もしていない気もします。

まとめ

Composite Actions は素朴でいいのですが実際使うときはアレってなるので、これらだけ注意すると便利です。

GitHub Actions に本当に欲しいのは、Template 機能な気もするけど ローカルアクションは便利なのでいいものです。 公開されている GitHub Actions を GHE で使うときに GitHub と Connect せずにローカルに展開することもできますし。

だいたいのことは GitHub Actions でできるようになりましたが、パイプライン的な観点がないので、今後はそっちがどうなるのか気になりますね。