コマンドラインの機能を強化しましたGo先日、自分の作っているバイナリエディタbedがhomebrew/coreに入ったことをご報告しました。その後、コードを眺めていると色々と直したいところが出てきたり、欲しい機能の実装イメージが沸いたりして、また活発に開発するようになりました。今でもそれなりに使われていることを意識すると、急にメンテする気力が湧いてくるんですよね。個人OSSの継続にはモチベーションが大事です。フィードバックは大歓迎です。最近、コマンドライン周りの機能強化をやっています。辿れるようにしました。別の履歴として保存しないといけないことです。コマンドラインで環境変数の補完に対応しました。まで補完して、さらにその中のファイルを候補として表示してくれます。:cdは作業中のディレクトリを変更するコマンドで、:pwdはそれを表示するコマンドです。機能も実装しています。作業ディレクトリのことを考えていて、bedでもVimみたいにディレクトリを移動できればファイルを開くのが楽になるなと思い:cdの実装を考え始めたのがきっかけでした。初めはos.Chdirを呼ぶだけだと思っていたのですが、これまでコマンド実行中にディレクトリが移動することがなかったので、既存の機能に影響して苦労しました。同じファイルを上書きします。このtestが作業中のディレクトリからの相対パスになっていたので、:cdを作ったことで挙動が変わってしまいました。各ウィンドウで絶対パスを管理することで、この問題は解決しました。コードを読み返していると、当時勢いで書いた部分が多く、色々な箇所をリファクタリングしたくなってきました。今回は、コマンドラインのパーサーを大きく書き直しました。昔は[]runeを引き回して次のオフセットを返すようなパーサーをよく書いていたのですが、Goのプラクティス的には文字列を渡してパースしたものと残りの文字列を返す方が良いということがわかってきました。strings.CutPrefixはとても便利ですね。他にもやたら状態遷移するコードを好んで書いていたのですが、よほど複雑なものでない限りはコードが読みにくくなるだけなのでやめました。ここ数年でもだいぶコーディングの好みが変わってきているのを感じます。テストもだいぶ書き直して、読みやすく安定したものになりました。bedのテストを見返すと、UIが送ったイベントをウィンドウが処理するのを待つためにsleepするみたいなテストが存在していました。UIが:wと:qのイベントを送った時に、ファイル保存が完了するよりも前に:qを処理してしまうと、保存がうまくいきません。テストの中でsleepするのは良くないですよね。何を待っているのかわからなくなりますし、その時間で処理が完了する保証もなく、不安定になってしまいます。UIが送ったイベントに対しては再描画のリクエストがUIに返ってくるので、そのイベントの受信を待つよう修正することでsleepを撲滅することができました。テストで何かを待ちたくなったら、sleepするより良い待ち方はないのか、本当にsleepする時間内に完了する保証があるのか考える必要があります。Vimの操作に慣れていて、バイナリファイルを軽く確認したい、軽く編集もしたいという人に、bedはとてもオススメできるツールです。入りましたGo数年前に、趣味でbedというバイナリエディタを作ったことがありました。当時はメモリーに乗らないような大きなファイルを描画したり、編集できるようにするロジックを考えるのが楽しく、とても熱中していました。基本的な機能をある程度作ってからは急速にやる気がなくなってしまい、細々とリファクタリングやパフォーマンス改善などをしてメンテナンスをしていました。formulaを配信していたのですが、最近homebrew/coreに入りました。これで、次のコマンドでbedをインストールできるようになりました。bed私の作ったものでhomebrew/coreに入ったプロダクトが二つになりました。一つ目は、もちろんgojqです。誰かが自分のプロダクトを良いものだと思ってくれて、より多くの人に使われるように動いてくれるのは嬉しいことですね。ActionsのワークフローにはYAMLファイルを使いますが、Vimのシンタックスハイライトがうまく効かなくて困ることがよくありました。その中の一部がYAMLのフロースカラースタイルの文字列として認識されてしまい、ハイライトが壊れることがあるのです。broken!'はてなブログも全く同じ問題があることから、中ではVimのシンタックスハイライトが使われているようです。プレインスタイルの文字列ですブロックスカラースタイルのコードの中のシェルスクリプトとしての文字列に色がつくのは便利な気もしますが、YAMLのフロースカラースタイルの文字列は改行も含めるなど自由度が高いので、ブロックスタイルの文字列の中でフロースタイルの解釈をするのはおかしいですね。(上のjoinの例)。(色々とあって取り込まれてからブログを書くまで時間がかかってしまいました...)。github.comこのパッチの中でも、特に重要なのは以下の部分です。contained|-や>1-といったブロックスカラースタイルのヘッダーを正しく認識し、その後に続くブロックスカラースタイルの文字列を認識するようになりました。これによって、文字列の中に複雑なシェルスクリプトを書いてもYAMLとしてのハイライトが壊れにくくなりました。Vimのシンタックス定義の詳しいドキュメントはこちらです。ここでは軽くシンタックスを作る時の考え方を書いておこうと思います。Vimのシンタックス定義の二つの大事な考え方は、nextgroupによる状態遷移と、containsによる構文アイテムの入れ子構造です。シンタックスハイライトの構文アイテムには、トップレベルのアイテムとそうではないものがあります。トップレベルの構文アイテムでもって、そのパターンがあればファイルのどこでも認識したいものを定義し、そこからnextgroupを使って次に続くアイテムを指定していくというのが基本的な考え方です。マッチした場合は基本的にファイルのどこでもリストのマーカーとして認識させたいので、トップレベルのアイテムとして定義します。トップレベルのアイテムがあることで、ファイルの途中からでも構文を認識できるようになっています。多くのプログラミング言語(PietやBefungeといった例外を除く)の文法は、木構造で表現できます。構文要素の隣接関係と包含関係によって表現できるということです。これらに対応するのが、nextgroupとcontainsです。隣接関係とは、functionというキーワードの後には識別子が続き、さらに開き括弧、引数、閉じ括弧、ブロックの開始が続くといった感じです。Vimのシンタックス定義では、nextgroupというオプションで次に続く構文アイテムを指定することができます。ここにはトップレベルではないアイテム、containedというオプションを指定したアイテムを指定します。clusterを使うと便利です。トップレベルのアイテムとnextgroupを使えばある程度の文法は表現できるのですが、これだけでは表現できない文法もあります。4"みたいな文字列補間は、隣接関係だけでは表現できません。他にも、文字列の中の\uXXXXのようなエスケープシーケンスとか、コメントの中のTODOとか、そういうものに色をつけたいことがあります。いった包含関係を表現するために、containsというオプションで構文アイテムの入れ子構造を表現します。状態遷移と入れ子構造を組み合わせることで、Vimのシンタックス定義は複雑な構文に対応しているのです。今回、YAMLのシンタックスハイライトを改善したことで、Vimのシンタックス定義の仕組みを改めて自分の中で整理できました。Vimのシンタックスを改善したい人に、この記事が参考になれば幸いです。一気に解決できたので、とても満足しています。Actionsでは依存パッケージやビルド結果などをうまくキャッシュすることで、テストやビルドの時間を短縮できます。actions/setup-nodeやactions/setup-javaなどの各言語のオフィシャルアクションは各パッケージマネージャーのためのキャッシュ機構を提供していますし、actions/cacheを使って任意のファイルをキャッシュすることもできます。これらは内部で@actions/cacheパッケージを使っており、キャッシュの機構はGitHub自身の機能と密に結びついています。Actionsのキャッシュはリポジトリごとに10GBまでという制限があり、開発者の多いリポジトリではsetup-nodeのキャッシュだけでもすぐに上限に達してしまいます。(参考)、それでも一日に数十GB以上利用してしまう日もあり、効果的にキャッシュを利用できているとは言えません。S3にキャッシュするアクションをフルスクラッチで作りました。二週間前から作り始めてようやく形になってきたので、タグを打ってMarketplaceにも公開しました。}}基本的にactions/cacheとほぼ同じように使えますが、いくつかの違いがあります。ブランチによるスコープ分離がありません。安全だとは思いますが、私のチームでは同じキーのキャッシュが大量に作られることもあり、とても不便に感じています。私のアクションにはブランチによるスコープは実装していません。必要であればkeysやrestore-keysにブランチ名を含めると良いでしょう。actions/cacheはWindowsとそれ以外のOSでキャッシュが混ざらないようになっていますが、同じことはkeyにrunner.osを含めることで実現できるので、私のアクションでは実装していません。そのためenableCrossOsArchiveというオプションはありません。簡単に言うと、キャッシュのpathが異なるキャッシュは別のバージョンとして扱われます。これは重要な機能で、単純にkeyだけでキャッシュをマッチさせてしまうとpathだけを変えた時に意図しないキャッシュをリストアしてしまいます。キャッシュのバージョンがあるおかげで、pathに新しいディレクトリを追加したとしても新しいkeyを考えなくてもよくなっているのです。私の作ったアクションでも、pathに基づいたハッシュをオブジェクトのキーに付与することで同じような挙動を実装しています。actions/cacheのpathが違えば別のキャッシュとして扱われるというこの挙動は、実装を追っていくと納得できる挙動でもあります。(-P)オプションを使って絶対パスを含めたアーカイブにして保存しています。展開時も同じオプションで展開するだけで、例えばpathに相当する場所に移動するという処理はありません。そのため、仮にpathが一つ指定されているだけであったとしても、パスが違えば別のキャッシュとして扱われるのです。actions/cache/saveで保存したファイルを別のパスにactions/cache/restoreできないのも、この実装によるものです。今回、アクションを実装する前に既存のアクションが使えるかをかなり調査しましたが、自分のユースケースでまともに動きそうなアクションは一つも見つけられませんでした。S3にオブジェクトがたくさんある時にうまく動かなかったり、キャッシュのバージョンに相当する挙動を実装していなくてpathを変えてもリストアしてしまったりしました。creatorでない作者のアクションを導入するにはソースコードの精査が義務付けられています)。actions/cacheの中でも特に重要な機能を抽出しつつ、キャッシュをS3に保存するだけのシンプルなアクションが欲しかったので、自分で作ることにしました。actions/cacheはネイティブのtarコマンドを実行していますが、s3-cache-actionはnode-tarを使っています。bindingを使っています。この実装で十分に速度が出ているので、特に問題はないと思っています。Brotliも検証しましたが、npmパッケージの保存を試したところ圧縮処理がとても遅くなり、キャッシュサイズもgzipと大差なかったのでやめました。globパターンの展開はactions/cacheと同じく@actions/globを使っているので、ここの挙動の際はありません。Actionsでのキャッシュにお困りの方は、ぜひ使ってみてください。Actionsなどでテストやリリースを自動化していると、様々なトリガーによって異なる処理を行うことはよくあることです。pushのトリガーではテストやlintを行い、tagをpushしたときはクロスビルドしてリリースする、などです。これらの処理は内容も頻度も異なるので、ワークフローのファイルを分けることはよくあることだと思います。私もかつてはこのようにトリガーや実行したい頻度が異なるのだから分けるのは自然なことだと考えていました。一つのワークフローの中でトリガーによって処理を分岐させるとワークフローが複雑になるし、面倒なことが多いからです。最近はリリース時の処理をテストと同じワークフローにまとめる方が良いと考えるようになりました。リリースする時になって初めて動く処理が多いほど、リリースのワークフローが壊れるリスクが高まるからです。Actionsになって、依存するactionの更新や実行するrunnerの更新など、ジョブが外的要因によって壊れることが増えたように感じます。クロスビルドやDockerイメージのビルドなどリリースする時しか使っていないツールやactionがあると、それらの更新を取り込んだ後の最初のリリースで動かないことに初めて気が付くというリスクがあります。よってテストが落ちるようになったのにリリースされてしまうという懸念もあります。テストもリリースも一つのワークフローにまとめた上で、リリース時にのみ行う処理はできるかぎり減らすというのが良いと思います。リリースかどうかの分岐をあちこちに書く羽目になったとしても、です。クロスビルドは普段からビルドすれば良いですし、Dockerイメージのビルドも常に行っておくべきです。(docker/build-push-actionならpushオプションでオンオフできる)、そういう処理に限ることが望ましいです。要するに、リリースのためのビルド処理は、テストと同じ頻度で行いましょうということだと思います。Actionsがワークフロー間の依存を定義しにくいという事情もあるでしょう。workflow_runってみなさん使ってます?)。私はjqのメンテナをやっているのですが、jqのCIもテストとリリースを一つのワークフローにまとめています。それでもなお、dependabotによるactions/upload-artifactとactions/download-artifactの更新PRが別々に来た時に、他のメンテナが前者のみをマージしてしまい一時的にリリースジョブが壊れてしまったことがあります。ビルドした実行ファイルをダウンロードしてDockerイメージを作るためにdownload-artifactを使っていたのですが、そのジョブはリリースの時にしか実行していませんでした。今は、PRの作成時もDockerイメージのビルドを行うようにしています。github.comCIリソースを心配されるかもしれませんが、大体のケースではキャッシュを活用すれば節約できますし、リリースジョブが壊れにくくするための必要なコストかなと思っています。Environmentsの都合などで常には実行できない処理は諦めざるを得ない場面もあるでしょう。できるだけリリースする時のリスクを減らすための考え方の一つとしてご紹介しました。(経験上、どんなワークフローでもworkflow_dispatchをつけておいて損はないと思います)。ブログを報告する引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。