【完全ガイド】Gitワークフローはちゃんと決めないと厳しいって！

## はじめに

現代のソフトウェア開発において、コードの品質、セキュリティ、一貫性を維持することは非常に重要です。
プレコミットフックは、コードがコミットされる前に自動チェックを行い、標準を適用する強力な方法を提供します。このガイドでは、Gitleaks、Commitlint、Lint-stagedの3つのツールの実装方法を詳しく説明します。






## 第一に秘密情報を守る！

それを担保するようなツールはいくつかありますが、git-secrets、Gitleaks、Secretlintを比較してみてみましょう。

| **ツール**         | **説明**        | **できること**         | **できないこと**     |
|---------------------|------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------|
| **git-secrets**     | AWSが開発したツールで、コミットやプッシュの前に秘密情報（例: AWSキー）が含まれていないかをローカルで検出。                                   | - 秘密情報（例: AWSキー）の検出。<br>- コミット前・プッシュ前のスキャン。<br>- 正規表現ベースのカスタムパターン追加。                                              | - Gitの履歴全体のスキャンが標準機能ではない。<br>- デフォルトでAWS関連のパターンに特化しているため、他の秘密情報の検出には設定が必要。               |
| **Gitleaks**        | オープンソースツールで、Gitリポジトリ全体や履歴から機密情報を検出し、CI/CD環境でも使用可能。                                              | - Gitリポジトリ全体、履歴のスキャン。<br>- 機密情報（APIキーやトークン）の検出。<br>- CI/CDパイプラインへの統合。<br>- 高速スキャン。                       | - 機密情報の削除（削除は別途ツールや手動操作が必要）。<br>- 正規表現の誤検出や設定の調整が必要な場合がある。                                    |
| **Secretlint**      | JavaScript/Node.jsベースの静的解析ツールで、ソースコードやテキストファイル内の秘密情報を検出。                                            | - ファイル全体の秘密情報検出。<br>- JavaScriptやJSONファイルなど、テキストファイルへの適用が得意。<br>- カスタムルールの設定。<br>- VSCode拡張機能としても使用可能。 | - Gitリポジトリ全体の履歴スキャンが非対応（ソースコードの現在の状態のみスキャン）。<br>- リポジトリ操作との連携が標準機能にはない。                  |


### **結論: 状況に応じた選択が必要**
- **ローカル開発中心なら**: `git-secrets` 、`Gitleaks`
- **リポジトリ全体のセキュリティ監査なら**: `Gitleaks`  
- **エディタでの即時チェックが必要なら**: `Secretlint`  

#### **最強の選択肢**
セキュリティを徹底したい場合、 **複数のツールを組み合わせる** のが最善です。  
例:
1. 開発者環境で `git-secrets` や `Gitleaks`を使用してコミット前にチェック。  
2. CI/CDで `Gitleaks` を実行して履歴や全体の漏洩チェック。  
3. コード編集中は `Secretlint` を使い、リアルタイムで問題を検出。

これらを踏まえ、より安全なプロジェクトを作っていくために、`Gitleaks`を使い、開発環境やCI/CDでプロジェクト全体を監視するようなケースを想定するのが良いでしょう。


## その他ツールとの連携

秘密情報だけではなく、コードの品質を担保することも、より良いプロジェクトにしていくためには非常に重要です。
- コミットする時点で、コードを静的解析し、整形したい
- 丁寧なコミットメッセージがあった方がデバッグする際やレビューする際などに役立つ

CommitlintやLint-stagedは、それを実現するために大変便利なツールです。

| **ツール**      | **説明**        | **重要度**            | **その他の情報**     |
|--------------------|-------------------------------------------------------------------------------------------|--------------------|-----------------------------------------------------------------------------------------------|
| **Commitlint** | Gitのコミットメッセージが一定の規則（例: Conventional Commits）に従っているかをチェックするツール。               | ★★★★☆（高い）         | - コードの変更履歴をより読みやすく整理。<br> - 自動リリースやChangelog生成との連携が容易。                   |
| **Lint-staged**| Gitのステージングされたファイルにのみコードリントやフォーマットを適用し、開発フローを効率化するツール。              | ★★★★☆（高い）         | - コードレビューの負担を軽減。<br> - 特定ファイルへのリントやフォーマットをスピーディに適用可能。                |

前段が長くなりましたが、これらの理由から、Gitleaks、Commitlint、Lint-stagedを利用したGitワークフローを組んでいきたいと思います。

## 前提条件

始める前に、以下のツールがインストールされていることを確認してください：
- Homebrew (macOS用)
- Node.js と npm
- Git


## 1. Gitleaks: ハードコードされたシークレットの防止

### Gitleaksとは？
Gitleaksは、リポジトリ内のハードコードされたシークレット、APIキー、パスワード、その他の機密情報をスキャンするツールです。

### インストール
```bash
brew install python
brew install pre-commit
brew install gitleaks
gitleaks --version # 動作確認します
```

### 設定
[デフォルトの設定](https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml)が充実しているため、これだけでも十分gitleaksの機能が使えますが、`.gitleaks.toml`ファイルを作成して、カスタムシークレット検出ルールを定義できます：

```.gitleaks.toml
[[rules]]
description = "カスタムシークレット検出"
id = "custom_secret"
regex = '''P@ssw0rd[0-9]{4}'''
keywords = [
    "P@ssw0rd",
]
```

上記のように作った`.gitleaks.toml`を、他のどのプロジェクトでも共通の設定とする場合は、以下コマンドでシェルに書き込んで設定します：

```zsh
export GITLEAKS_CONFIG=/your-path/.gitleaks.toml
```

今回のように特定のプロジェクトのみとするときは、`.git/hooks/pre-commit`を以下の様にgitleaks.tomlのパスの設定を書き込みます：

```.git/hooks/pre-commit
#!/usr/bin/env bash
# File generated by pre-commit: https://pre-commit.com
# ID: 138fd403232d2ddd5efb44317e38bf03

# start templated
INSTALL_PYTHON=/opt/homebrew/opt/pre-commit/libexec/bin/python3.13
ARGS=(hook-impl --config=.pre-commit-config.yaml --hook-type=pre-commit)
# end templated

HERE="$(cd "$(dirname "$0")" && pwd)"
ARGS+=(--hook-dir "$HERE" -- "$@")

export GITLEAKS_CONFIG=$(pwd)/.gitleaks.toml

if [ -x "$INSTALL_PYTHON" ]; then
    exec "$INSTALL_PYTHON" -mpre_commit "${ARGS[@]}"
elif command -v pre-commit > /dev/null; then
    exec pre-commit "${ARGS[@]}"
else
    echo '`pre-commit` not found.  Did you forget to activate your virtualenv?' 1>&2
    exit 1
fi

```

### プレコミットフックの設定
`.pre-commit-config.yaml`ファイルを作成します：

```.pre-commit-config.yaml
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v5.0.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-added-large-files

  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.21.2
    hooks:
      - id: gitleaks

```
pre-commitを使っていてせっかくなので、今回はtrailing-whitespaceなどのルールのフックも入れてみました。
他にもいろんなルールを設定できるので、[公式リポジトリ](https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml)を元に、適宜適切にカスタムルールを追加しましょう。

上記のプレコミットフックは以下のコマンドで自動的にバージョンを更新できます：
```bash
pre-commit autoupdate
pre-commit install
```

コミットの前に以下コマンドでテストファイルを検査します：

```pasword.txt
P@ssw0rd1234
```

```bash
% gitleaks detect -c .gitleaks.toml
```
↓ この時failすればオッケー。

![for added.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3131509/f9e7d1dd-df95-9233-d84f-298560ebe406.png)

↓ 成功すると以下のようになります。

![スクリーンショット 2024-11-17 17.49.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3131509/1f402d44-fedf-f379-c0aa-248d6fb97176.png)







## 2. Commitlint: コミットメッセージ規約の強制

### Commitlintとは？
Commitlintは、コミットメッセージが一貫した、事前に定義された形式に従うことを確認します。

### インストール
```zsh
npm install --save-dev @commitlint/{cli,config-conventional}
```

### 設定
`commitlint.config.js`を作成：

```commitlint.config.js
module.exports = {
  extends: ["@commitlint/config-conventional"],
};
```

### Huskyフックの設定
```zsh
npx husky init
echo "npx --no -- commitlint --edit \$1" > .husky/commit-msg
npm pkg set scripts.commitlint="commitlint --edit"
echo "npm run commitlint \${1}" > .husky/commit-msg
```

失敗コメントで確認してみます：
```zsh
git commit -m "foo: this will fail"
```
↓

![スクリーンショット 2024-11-17 19.11.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3131509/9a561273-e96f-e548-b399-b111f139f36b.png)

この時、commitlint.config.jsで設定した@commitlint/config-conventionalのルールに従って、featなどの接頭辞が不足していることを検出しました。
こちらも[公式リポジトリ](https://github.com/conventional-changelog/commitlint)を参考に適宜適切に設定しましょう。






## 3. Lint-staged: ステージされたファイルのフォーマット

### Lint-stagedとは？
Lint-stagedは、コミット前にステージされたファイルに対してリンターとフォーマッターを実行します。

### インストール
```zsh
npm install --save-dev prettier eslint husky lint-staged && npx husky init && echo "npx lint-staged" > .husky/pre-commit
```

### 設定
`package.json`を更新：

```package.json
{
  "lint-staged": {
    "**/*.{ts,tsx}": [
      "eslint --fix"
    ],
    "**/*.{css,ts,tsx,json}": [
      "prettier --write"
    ]
  }
}
```

設定ファイルを作成：
- `.eslintrc.js`
- `.prettierrc`
- `eslint.config.js`

```.eslintrc.js
const { resolve } = require('node:path');

const project = resolve(process.cwd(), 'tsconfig.json');

module.exports = {
  extends: [
    'next/core-web-vitals',
    'airbnb-base',
    'airbnb-typescript/base',
    'plugin:@typescript-eslint/recommended-type-checked',
    'plugin:@typescript-eslint/stylistic-type-checked',
    'plugin:prettier/recommended',
  ],
  root: true,
  env: {
    browser: true,
    es2021: true,
    node: true,
  },
  plugins: ['@typescript-eslint', 'simple-import-sort'],
  parserOptions: {
    project,
  },
  rules: {
    // increase the severity of rules so they are auto-fixable
    'simple-import-sort/imports': 'error',
    'simple-import-sort/exports': 'error',
    'prettier/prettier': ['error'],
    'react/no-array-index-key': 'error',
    'react/destructuring-assignment': 'off', // Vscode doesn't support automatically destructuring, it's a pain to add a new variable
    'react/require-default-props': 'off', // Allow non-defined react props as undefined
    'react/jsx-props-no-spreading': 'off', // _app.tsx uses spread operator and also, react-hook-form
    'react-hooks/exhaustive-deps': 'off', // Incorrectly report needed dependency with Next.js router
    '@next/next/no-img-element': 'off', // We currently not using next/image because it isn't supported with SSG mode
    '@next/next/link-passhref': 'off', // Only needed when the child of Link wraps an <a> tag
    '@typescript-eslint/comma-dangle': 'off', // Avoid conflict rule between Eslint and Prettier
    '@typescript-eslint/consistent-type-imports': 'error', // Ensure `import type` is used when it's necessary
    'no-restricted-syntax': ['error', 'ForInStatement', 'LabeledStatement', 'WithStatement'], // Overrides Airbnb configuration and enable no-restricted-syntax
    'import/prefer-default-export': 'off', // Named export is easier to refactor automatically
    'jsx-a11y/click-events-have-key-events': 'off',
    'jsx-a11y/no-static-element-interactions': 'off',
  },
  settings: {
    'import/resolver': {
      typescript: { project },
    },
  },
  overrides: [
    // override "simple-import-sort" config
    {
      files: ['src/**/*.{js,jsx,ts,tsx}'],
      rules: {
        'simple-import-sort/imports': [
          'error',
          {
            groups: [
              // Packages `react` related packages come first.
              ['^react', '^@?\\w'],
              // Internal packages.
              ['^(@|components)(/.*|$)'],
              // Side effect imports.
              ['^\\u0000'],
              // Parent imports. Put `..` last.
              ['^~.', '^\\.\\.(?!/?$)', '^\\.\\./?$'],
              // Other relative imports. Put same-folder imports and `.` last.
              ['^\\./(?=.*/)(?!/?$)', '^\\.(?!/?$)', '^\\./?$'],
              // Style imports.
              ['^.+\\.?(css)$'],
            ],
          },
        ],
      },
    },
  ],
  ignorePatterns: ['**/*.config*', '**/*.setup*', '.eslintrc.js', './src/_libs/drawjs/index.js'],
};

```

```.prettierrc
{
    "tabWidth": 4,
    "useTabs": false
}
```

```eslint.config.js
const path = require("path");

const buildEslintCommand = (filenames) =>
  `next lint --fix --file ${filenames
    .map((f) => path.relative(process.cwd(), f))
    .join(" --file ")}`;

module.exports = {
  "*.{js,jsx,ts,tsx}": [buildEslintCommand],
  "**/*.ts?(x)": () => "yarn check-types",
  "*.json": ["prettier --write"],
};
```

↓上記のように設定した後、commitすると、以下のようにコードがフォーマット化されたことがわかります。

![スクリーンショット 2024-11-17 18.28.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3131509/00789eaa-b22c-2161-0de0-3f892f6f32ac.png)


## 注意
これでgitleaksとcommitlint、lint-stagedを一通り設定しましたが、実はこのままではgitleaksが動きませんでした。

そこで以下の2つのやり方を試して、2つ目の方法で全てうまく動くようになりました。

## 1：.git/hooks/pre-commitにcommitlintとlint-stageの機能を移植（失敗例）

```.git/hooks/pre-commit
#!/usr/bin/env bash
# File generated by pre-commit: https://pre-commit.com
# ID: 138fd403232d2ddd5efb44317e38bf03

# start templated
INSTALL_PYTHON=/opt/homebrew/opt/pre-commit/libexec/bin/python3.13
ARGS=(hook-impl --config=.pre-commit-config.yaml --hook-type=pre-commit)
# end templated

HERE="$(cd "$(dirname "$0")" && pwd)"
ARGS+=(--hook-dir "$HERE" -- "$@")

export GITLEAKS_CONFIG=$(pwd)/.gitleaks.toml

npm run commitlint ${1}

# If commitlint fails, abort the commit
if [ $? -ne 0 ]; then
    echo "commitlint failed, aborting commit."
    exit 1
fi

# Run lint-staged first
npx lint-staged

# If lint-staged fails, exit early and do not run gitleaks
if [ $? -ne 0 ]; then
    echo "lint-staged failed, aborting commit."
    exit 1
fi



# If lint-staged passes, run gitleaks
gitleaks detect -c .gitleaks.toml

if [ -x "$INSTALL_PYTHON" ]; then
    exec "$INSTALL_PYTHON" -mpre_commit "${ARGS[@]}"
elif command -v pre-commit > /dev/null; then
    exec pre-commit "${ARGS[@]}"
else
    echo '`pre-commit` not found.  Did you forget to activate your virtualenv?' 1>&2
    exit 1
fi

```

この場合、それぞれ動いたように見えましたが、commitlintは1回目失敗した後に、2回目に成功するコメントしても失敗するようでした。

## 2：.husky/pre-commitにgitleaksの機能を移植（成功）

```.husky/pre-commit
npx lint-staged

INSTALL_PYTHON=/opt/homebrew/opt/pre-commit/libexec/bin/python3.13
ARGS=(hook-impl --config=.pre-commit-config.yaml --hook-type=pre-commit)
# end templated

HERE="$(cd "$(dirname "$0")" && pwd)"
ARGS+=(--hook-dir "$HERE" -- "$@")

export GITLEAKS_CONFIG=$(pwd)/.gitleaks.toml

if [ -x "$INSTALL_PYTHON" ]; then
    exec "$INSTALL_PYTHON" -mpre_commit "${ARGS[@]}"
elif command -v pre-commit > /dev/null; then
    exec pre-commit "${ARGS[@]}"
else
    echo '`pre-commit` not found.  Did you forget to activate your virtualenv?' 1>&2
    exit 1
fi
```

逆にこのコードを`.git/hooks/pre-commit`に移動しても動きません。
これは推測になりますが、`husky`は`.git/hooks`以下のフックを自動生成・管理しており、`.git/hooks/pre-commit`に移動すると`husky`の管轄外になるため正しく動作しない可能性があります。
また`husky`のフックスクリプトは、自動生成される際に実行権限が付与されています。手動で .git/hooks/pre-commit に移動した場合、スクリプトに実行権限がないとフックが正しく動作しない、というのも原因として考えられます。
これらを踏まえると、手動で `.git/hooks`を編集するのではなく、`husky`の仕組みを使って管理するのが良いと考えます。
また`.git/hook`はGitリポジトリに含まれず、リモートリポジトリにプッシュされないため、`husky`を利用した方が、プロジェクトの他の開発者とも一貫性を保つことができるでしょう。

## おわりに
プレコミットフックの設定方法を詳細な説明、設定例、ベストプラクティスとともに段階的に説明してみました。
目的は、自動チェックを通じて開発者のコード品質、セキュリティ、一貫性を向上させることです。
これらの他にもgit-secrets、Secretlintなどのツールがありますが、適宜適切に選定してgitの堅牢なワークフローを作成しましょう。

## 参考
- [Gitleaks GitHub](https://github.com/gitleaks/gitleaks)
- [Commitlint ドキュメンテーション](https://commitlint.js.org/)
- [Lint-staged GitHub](https://github.com/lint-staged/lint-staged)