← ./articles-ja

feature flagがbuild tool configを制御しない時はoverlayを使う

feature flagは、多くの場合application codeを制御します。しかし、buildに参加するすべてのtoolを自動で制御するわけではありません。

compiler、bundler、packager、linker、deploy toolがそれぞれ独自の設定ファイルを読む場合、この違いが問題になります。code上ではfeatureを無効にしていても、build toolがそのfeature用の設定を読んで動くことがあります。

その時は、configurationをbase fileとfeature-specific overlayに分けます。

症状: featureはoffなのにbuild toolが動く

失敗は矛盾して見えます。

  • optional featureはdisabled
  • code pathはcompile-time gateの内側
  • それでもbuildがsecret、signing key、endpoint、asset、plugin settingを要求する
  • errorはcompile後、packagingやdeploymentで出る

よくある形はこうです。

feature OFF
compiled code path removed
build tool config still present
build tool performs feature-specific work

これはfeature flagのbugではありません。境界のbugです。

compile-time gateとtool-level configを分けて考える

compile-time gateはsource codeを制御します。

例:

  • Rust #[cfg(feature = "updater")]
  • C/C++ preprocessor flag
  • bundlerで置換されるTypeScript constant
  • package-level optional import

tool-level configはbuild toolを制御します。

例:

  • bundle target
  • signing setting
  • updater artifact setting
  • deploy destination
  • plugin configuration
  • linker script
  • generated asset option

build toolは、あなたのcode feature flagが何を意味するかを必ず知っているわけではありません。config fileに「このartifactに署名する」「このpluginを有効にする」と書いてあれば、code pathがcompileされたかどうかに関係なく、その作業を試みることがあります。

base configとfeature overlayを使う

base configには、すべてのvariantで共通する設定だけを残します。

{
  "name": "my-app",
  "build": {
    "outDir": "dist"
  },
  "bundle": {
    "targets": ["installer"]
  }
}

optional settingは別のoverlayに置きます。

{
  "bundle": {
    "createUpdateArtifacts": true
  },
  "release": {
    "signing": {
      "required": true
    }
  }
}

build commandも明示します。

# Base build
build-tool --config config.json

# Feature build
build-tool --config config.json --config config.updater.json

複数configを直接mergeできるtoolもあれば、先にcombined configを生成する必要があるtoolもあります。原則は同じです。base settingは常に読み、feature settingはそのvariantでだけ読みます。

overlayに頼る前にmerge semanticsを確認する

すべてのtoolがJSONを同じようにmergeするとは限りません。

RFC 7386で定義されるJSON Merge Patchでは、objectはmergeされ、nullは削除を意味し、arrayはappendではなくreplaceされる値として扱われます。

例えばbaseがこれで:

{
  "bundle": {
    "targets": ["msi", "nsis"],
    "active": true
  }
}

overlayがこれなら:

{
  "bundle": {
    "targets": ["nsis"]
  }
}

結果はこうなり得ます。

{
  "bundle": {
    "targets": ["nsis"],
    "active": true
  }
}

arrayは追加ではなく置換されています。

release processでoverlayを使う前に、次を確認します。

  • objectは再帰的にmergeされるか
  • arrayはreplaceかappendか
  • nullはkey削除か
  • 後のconfigが前のconfigを上書きするか
  • unknown keyをvalidateするか

overlay fileは小さくし、変更するkeyだけを入れます。

build後にvariant-specific artifactを確認する

build successだけでは足りません。overlayを忘れても、間違ったvariantのvalid artifactができることがあります。

artifact checkを入れます。

# Base build should produce the app but not the optional artifact.
test -f dist/my-app.exe
test ! -f dist/my-app.exe.sig

# Updater build should produce both.
test -f dist/my-app.exe
test -f dist/my-app.exe.sig

pathは使っているtoolに合わせます。verificationは「commandがexit 0だった」ではなく、variantのcontractを表すべきです。

よいcheckは具体的です。

  • default buildにはupdater signatureがない
  • updater buildにはupdater signatureがある
  • free buildにはpaid-only assetがない
  • production buildはproduction endpointを使う
  • staging buildはproduction credentialを使わない

目的は、間違ったvariantで成功してしまうことを捕まえることです。

secretをbase configに置かない

config overlayは安全境界でもあります。

base configにoptional release secretを含めないでください。すべてのbuildで必要なpublic settingなら置けますが、local developerにproduction signing keyやdeploy credentialを要求する形にしないほうがよいです。

こちらを選びます。

config.json
config.release.json

こちらは避けます。

config.json with release signing enabled by default

後者はrelease pathを誤ってtriggerしやすくし、development pathを無駄に壊しやすくします。

overlayが過剰なケース

小さなoptionすべてを分割する必要はありません。

overlayが有効なのは、次のような場合です。

  • buildに名前付きvariantがある
  • variantごとにartifactが違う
  • variantごとに必要なsecretが違う
  • featureがlocal developmentではoptional
  • wrong variantでもcommandが成功し得る

その設定がすべてのenvironmentで常に必要なら、base configに置きます。overlayが多すぎると、build全体を理解しにくくなります。

参考

まとめ

feature flagはcodeを制御します。build toolはartifactを制御します。この2つが同じgateを共有していないなら、base configとfeature overlayに分け、merge ruleを確認し、variantを定義するartifactを検証します。