窓を作っては壊していた人のブログ

この謎のブログタイトルの由来を知るものはもういないだろう

SwiftLintをCIで使う時はCI上のSwift Toolkitのバージョンを気にかけよう

タイトル通りです。

最近iOSアプリを作るプロジェクトに参加しまして、SwiftLintの導入が行われました。 CI上でformatterをかけてdiffが出ていたらCIをfailedにするというTaskを追加した所、無事にfailedして手元でformatterかけたはずでは……?となって原因調査をしたところCI上のSwiftのバージョンと手元のバージョンが異なっていることが原因でした。

要点

  • Swift Toolkitのバージョンが違うと異なる結果になることがある
  • CIではdefaultのバージョンを使わずに指定する

環境

GitHub Actions macOS Host 10.15 - Swift 5.2.4, 5.3.0 - Xcode 11.7, 12.0

Local - Swift 5.3.0 - Xcode 12.0

どうなったか

Xcode 12.0 両方の結果が見たいのでfail-fastじゃなくていいです · yamachu/swift-5.2x-5.3-swiftlint-different@d164e32 · GitHub

Xcode 11.7 両方の結果が見たいのでfail-fastじゃなくていいです · yamachu/swift-5.2x-5.3-swiftlint-different@d164e32 · GitHub

手元がXcode 12.0の時CIが12.0であればdiffは出ていませんが、CIが11.7を使用している時はdiffが出ています。

対策

  1. CI上のXcodeのバージョンを指定する

今回はGitHub Actionsを使用しました。

virtual-environments/macos-10.15-Readme.md at ce5572effe3843a3397fa1ea2e015d48d0c1994d · actions/virtual-environments · GitHub

検証時はXcodeのdefaultが11.7だったため

swift-5.2x-5.3-swiftlint-different/swift.yml at d164e32d11bd803bf90724e223055197820b0d67 · yamachu/swift-5.2x-5.3-swiftlint-different · GitHub

    - name: Switch Xcode version
      run: sudo xcode-select -s /Applications/Xcode_12.app

のようにして使用するXcodeをローカルと同じバージョンになるように指定しました。

www.jessesquires.com

の記事中にあるように DEVELOPER_DIR という環境変数に設定することでXcodeのバージョンを指定するのも良さそうです。

  1. swiftenvで対象バージョンのSwiftをインストールし、それを使うようにする

ビルド時に同じバージョンを使ってくれるかと言われると怪しいので、Xcodeのバージョンを変えることを推奨しますが、仮にローカルでmacOSのバージョン問題があり、CI上と異なってしまうみたいなことがある場合は、swiftenvでCIで使用されているSwiftのバージョンをインストールし、

$ TOOLCHAIN_DIR=/Library/Developer/Toolchains/swift-5.3.0-RELEASE.xctoolchain ./Pods/SwiftLint/swiftlint

のような形でswiftlintを実行すると、ローカル、CI上共に同じ結果を得ることができます。

この複数のSwiftバージョンを使うことに関しては

github.com

SwiftLintのREADMEに書いてあります。

Durable Functions を JavaScript + Azure Functions v3 環境で使う時にハマる点について(2020年7月現在の話)

タイトル通りです。

最近ちょっとしたプロダクトを作る時にDurable Functionsを採用したのですが、Azure Functions v3環境下において、 Azure Functions Core Tools@3 で生成されるTemplateでは期待通りの動作をしませんでした。 時間が経てば解決しそうな事象ではありますが、それまでに使う場合に辛いことにならないようにメモとして残しておきます。

ちなみに作ったプロダクトがこちら

github.com

環境

  • Azure Functions Core Tools 3.0.2630
  • Azure Functions Runtime ~3
  • durable-functions 1.4.3 (JSのPackage)

entityTrigger が動作しない

原因

JavaScriptのDurableFunctions packageで提供されているentityメソッドを発火するためのentityTriggerですが、こちらが実行できません。 原因としては func init を実行した際に出力されるhost.jsonの中身のextensionBundle の値が古いために、バンドルされる Microsoft.Azure.WebJobs.Extensions.DurableTask のバージョンが古すぎて entityTrigger のbindingがサポートされていないためです。

$ func --version
3.0.2630

な環境で作られる host.json の中身がこちらになります。

{
  "version": "2.0",
  "logging": {
    "applicationInsights": {
      "samplingSettings": {
        "isEnabled": true,
        "excludedTypes": "Request"
      }
    }
  },
  "extensionBundle": {
    "id": "Microsoft.Azure.Functions.ExtensionBundle",
    "version": "[1.*, 2.0.0)"
  }
}

このバージョンレンジの中での現時点での最新は

azure-functions-extension-bundles/extensions.json at bd7c5b8adfb740dd8aaa1e60ec9f78b3c3c739cb · Azure/azure-functions-extension-bundles · GitHub

こちらで確認できるのですが、ここにバンドルされている Microsoft.Azure.WebJobs.Extensions.DurableTask のバージョンが 1.8.6 となっています。 entityのTriggerは2.0.0以降でのサポートとなっているため、利用が出来ない、というわけです。

解決策

host.jsonから extensionBundle の記述を削除し、必要なExtensionを自分で追加していくのが一番安心かと思います。 私はextensionBundleの記述を削除した後、

$ func extensions install -p Microsoft.Azure.WebJobs.Extensions.DurableTask -v 2.2.2

で最新のDurableTaskを導入し動作が確認できました。 この場合 .NET Coreがローカルに入っていないとextensionのビルドが出来ないため、インストールしておくのが良いでしょう。

ちなみにextensionBundleの記述がある状態で最新のDurableTaskをインストールしようとすると

No action performed. Extension bundle is configured in HERE_IS_AZURE_FUNCTIONS_FILE_PATH/host.json.

みたいな感じでインストールが出来ません。

continueAsNew でループを行えない

docs.microsoft.com

whileループなどを用いずに無限ループのようなものを作るパターンで利用するメソッドです。 2020年7月28日現在のドキュメントに記載されている

const df = require("durable-functions");
const moment = require("moment");

module.exports = df.orchestrator(function*(context) {
    yield context.df.callActivity("DoCleanup");

    // sleep for one hour between cleanups
    const nextCleanup = moment.utc(context.df.currentUtcDateTime).add(1, "h");
    yield context.df.createTimer(nextCleanup.toDate());

    context.df.continueAsNew(undefined);
});

だと2度しかこのOrchestratorが実行されません。

解決策

すでにIssueとして報告されています。

github.com

context.df.continueAsNew()yieldreturnなしで実行すると起こってしまうようです。

例えば

const df = require("durable-functions");
const moment = require("moment");

module.exports = df.orchestrator(function*(context) {
    yield context.df.callActivity("DoCleanup");

    // sleep for one hour between cleanups
    const nextCleanup = moment.utc(context.df.currentUtcDateTime).add(1, "h");
    yield context.df.createTimer(nextCleanup.toDate());

    yield context.df.continueAsNew(undefined);
    return '';
});

こんな感じでorchestrator関数で明示的に値を返すようにしてcontinueAsNewにyieldを付けてあげることで正常に動作しました。

これに関しては durable-functions パッケージにパッチが当たればいずれなくなる問題なので待つか、上記のような対処法で解決しましょう。

おわりに

C# とかでのDurableFunctionsの情報は調べれば割と出てくるのですが、JSのハマってしまった情報とかはあまり出てこなくて解決するのが大変でした… この記事を見た方はぜひJSでDurableFunctions使って色々とハマりそうな点を見つけて共有していってください。

おまけとか追記

JavaScript + Webpack なファイルを Azure Functions で起動する時の function.json とwebpack.config.js の libraryTarget の対応 · GitHub

そう言えば今年もMicrosoft MVP Developer Technologiesを受賞できました。 更新頻度は低いですが、誰かの役に立てるような情報を発信したり出来るよう頑張りたいと思います。