こんにちは。ご無沙汰しております。 @53able です。
今回は、静的コードチェックを強化したことについてお話しようと思います。
概要
環境変数定義のチェックを自動化し、変数定義の不足があればPull Requestに警告を出して不備に気づけるようにしました。
環境変数の構成
現在GDS (GameWithDesignSystem) では、プロジェクトで環境ごとに参照されるファイルが5つあります。
- .env.development
- .env.staging
- .env.sandbox
- .env.production
- .env.local
- .env-local-sample
VueCLI を採用しているので、ビルドオプションで指定するモードによって参照される環境変数ファイルが切り替わります。
.env.local
は、git 管理から除外しており各自ローカルで自由に編集して利用します。
.env-local-sample
は、どの環境からも直接参照されないですが、 .env.local
に定義を写して正常動作可能なデフォルト値を記述しています。
GDS の記事はこちらから御覧ください
環境変数をスケール
通常の開発では環境ごとに変わる変数を追加する場合に、まず .env.local
に変数を追加して実装します。
仕上げに、各環境向けのファイル .env.development
, .env.staging
, .env.sandbox
, .env.production
にも環境変数を追加し、それぞれの環境に対応した値を代入します。
課題点
定義追加ミスが分かるのは各環境で実行時に参照不可となって、変数追加漏れに気づくのが遅れがちです。
また、コードレビュー時に環境変数の定義漏れを細かくチェックするのも煩雑でした。
解決策
環境変数定義ファイルに定義されている変数名を全て抽出し、各ファイルで環境変数の定義が存在するかどうかを判定する処理を CI で行うようします。
ファイルごとに定義が見つからない環境変数があれば Pull Request に書き込まれ、変数の追加または削除が漏れているかどうか確認できるようにします。
| .env-local-sample
だけに環境変数 HOGE を追加
実装
今回は DangerJS を使い CI 上で Dangerfile のスクリプトが実行されるようになっていることが前提です。
本プロジェクトは、Dangerfile を TypeScript で実装しています。
全ての環境変数ファイルで定義されている変数名を列挙する
環境変数ファイルの内容を読み取り文字列として処理していきます。
環境変数の記述パターン
ファイルに書かれている記述フォーマットは、 変数名=値 です。
一行ずつ変数が定義されています。
イレギュラー要素
変数名=値 で統一されていれば変数名を列挙するのは簡単になるので、ファイル文字列からイレギュラー要素を正規表現で特定し除去していきます。
- コメントアウト(
#
) - 空行
- スペース
- 行頭
- 行末
=
の前後
const getVariableNames = (file: string) => file .replace(/(^\s+)/gm, "") // ① .replace(/(^#.*$)|(^\s*$)/gm, "") // ② .replace(/(\r?\n)+/g, "\n") // ③ .split("\n") // ④ .map((line) => line.split("=")[0].replace(/\s+/g, "")) // ⑤ .filter((line) => line !== "");
正規表現はデフォルトでは改行も含めて一行の文字列として扱います。
改行ごとにパターンマッチ判定を行うために、 正規表現の m
オプションを用いています。
m
オプションを用いることによって行単位で文字列を処理していきます。
①各行頭のスペースを削除します。
②各行頭の #
で始める行、スペースのみの行を空行にします。
③2回連続の改行を1つにします。
④ここまでで 1行が 変数名=値 のテキストへ整形されたので、 .split("\n")
で、変数定義ごとの配列へ変換します。
⑤変数名のみが必要なので、 =
の左辺から余分なスペースを除去します。
環境変数ファイルのループ判定処理用のオブジェクト
環境変数ファイルから環境変数を抽出する関数を使い、各環境変数ファイルの定義済み変数を抽出します。
環境変数は複数あるので、ループ中に警告メッセージを組み立てやすくさせるため、各ファイルの環境変数のオブジェクトを予め作っておきます
// .env.xxx ファイルの種類 const envs = ["staging", "production", "development", "sandbox"] as const; // ループさせて変数定義不足を判定させるために const envMap = await Promise.all( envs.map(async (env) => { const file = `.env.${env}`; const fileString = await fs.promises.readFile(file, "utf-8"); const variables = getVariableNames(fileString); return { env, path: file, variables, }; }), );
サンプル環境変数ファイル
環境変数ファイルとは別に .local.env
に転載させる サンプル環境変数も定義済みの変数を抽出します。
const sampleFileString = await fs.promises.readFile( ".env-local-sample", "utf-8" ); const sampleFileVariables = getVariableNames(sampleFileString);
変数名の重複除外
4つの環境変数ファイル .env.staging
, .env.production
, .env.development
, .env.sandbox
と .env-local-sample
で定義済みの変数をユニークな配列に変換します。
const allEnvVariables = Array.from( new Set([ ...envMap.reduce((acc, cur) => { return [...acc, ...cur.variables]; }, [] as string[]), ...sampleFileVariables, ]) );
サンプル環境変数ファイル .env-local-sample
内の変数定義チェック
ユニーク化した環境変数名配列をループし .env-local-sample 内に定義済みの環境変数名を抽出した配列に含まれているか判定していきます。
const warnMsgsBySample = allEnvVariables.reduce((acc, target) => { if (sampleFileVariables.includes(target)) return acc; return [...acc, `**.env-local-sample**, *${target}* has not been defined.`]; }, [] as string[]);
環境変数ファイル内の変数定義チェック
ユニーク化した環境変数名配列をループし、さらに各環境変数ファイル毎にもループして定義済みの環境変数名を抽出した配列に含まれているか判定していきます。
const warnMsgsByEnv = allEnvVariables.reduce((acc, target) => { const _warnMsgs = envMap.reduce((acc, env) => { if (env.variables.includes(target)) return acc; return [...acc, `**${env.path}**, *${target}* has not been defined.`]; }, [] as string[]); return [...acc, ..._warnMsgs]; }, [] as string[]);
警告文出力
最後に警告が1件以上あれば警告を表示させて環境変数のチェックが完了です。
警告文は、環境変数ファイル名から始まっています。
環境変数ファイル毎に並び替えしてあると警告文を読みやすいので .sort()
でファイルごとに警告メッセージを並び替えます。
const warnMsgs = [...warnMsgsBySample, ...warnMsgsByEnv].sort(); if (warnMsgs.length < 1) return; warn(warnMsgs.join("\n"));
まとめ
環境変数の不備は、ビルド時ではなく実行時に不備に気づくことがあり対応が後手になりがちです。
事前にコードの静的チェックがあることで事故リスクが減ります。
コードレビューでは、環境変数の追加漏れチェックをする手間が省けるのが良かったです。
また、追加だけではなく一部で消し忘れがある環境変数についても気付けるようになりました。
その場合は、他のファイルに消してあるべき環境変数が定義があることで先に消したファイルで変数定義がない警告が発生します。
補足説明
VueCLI はメンテナンスモードになっており、新しくプロジェクトは Vite ベースで作ることが推奨されています。