モジュール
モジュールは ritsu.mod を持つディレクトリです。ほかのプロジェクトが、横のディレクトリ、git のリポジトリ、OCI のレジストリのどれかから足して、そのファイルを @<名前>/… のパスで読みます。ritsu のリポジトリから配るモジュールは、モジュールの一覧に、版、ファイルごとのページ、版のあいだの差とともに載せています。
プロジェクトは、依存のファイルを、自分のファイルと一緒に、各言語の検査と言語をまたぐ検査にかけます。そのため、証明はモジュールの境目でも切れません。このページの例は、ritsu のテストの三つのモジュール crates/ritsu/tests/projects/modules.ja です。tax(ディレクトリは 税)は付加価値税の税率を、pricing(価格)は送料と注文の合計を持ちます。注文の合計は tax の税率を準用します。shop(店)は注文のフローとかごを持ち、その値段を pricing で決めます。
モジュールを書く
ritsu mod init <名前> は、走らせたディレクトリに ritsu.mod を書きます。そこから、そのディレクトリはモジュールになります。次は pricing の ritsu.mod です。
# Acme の店の値段。送料と、tax の税率を入れた注文の合計。
module example.com/acme/pricing
version 1.1.0
description "Acme の店の送料と注文の合計"
license "MIT OR Apache-2.0"
require tax path "../税"
ritsu.mod は一行に一つの文を書きます。# から行の終わりまでは注です。最初の文は module です。
| 行 | 意味 |
|---|---|
module <名前> |
モジュールの名前。レジストリに送り出すモジュールは、そのレジストリのリポジトリで名前を付けます(example.com/acme/pricing、ghcr.io/i2y/ritsu-holidays)。git で共有するモジュールの名前は、置いてある場所です(example.com/acme/rates。下)。パスでだけ読むモジュールは、短い名前(shop)でもかまいません。 |
version <版> |
版。SemVer の形で、頭に v を付けず、ビルドのメタデータも付けません(1.4.0、2.0.0-rc.1)。送り出すときのタグになります。git で共有するモジュールのタグは、頭に v を付けたものです(v1.4.0)。 |
ritsu >=<版> |
このモジュールを読める ritsu の下限。古い ritsu は、要る版を言って止まります。 |
description、license、source |
レジストリ、モジュールの一覧、ライブラリが見せるもの。license は SPDX の式です。source はモジュールのリポジトリの https の URL で、公開するものなので、ユーザー、パスワード、クエリは書けません(E302)。 |
require <名前> path "<ディレクトリ>" |
ディレクトリにある依存。パスはモジュールのルートから書きます。 |
require <名前> oci "<リポジトリ>" <版> |
レジストリにある依存。その版か、それより新しい版を使います。 |
require <名前> git "<URL>" <版> [dir "<ディレクトリ>"] |
git のリポジトリのタグにある依存。その版か、それより新しい版を使います。dir は、リポジトリの中のモジュールのディレクトリです(git でモジュールを共有する)。 |
exclude "<パス>" |
ritsu mod push が送らないファイルかディレクトリ。ritsu mod init は、ritsu gen の出力先の exclude "generated" を書きます。 |
signature "<名前>" … |
その名前のモジュールに求める署名(署名)。 |
library typescript\|python\|go "<名前>" |
その言語でのモジュールのライブラリの名前(ライブラリ)。 |
依存の名前は require の次の語で、英小文字、数字、_ で書き、英字で始めます。モジュールのファイルは、依存のファイルを、@ とこの名前に、依存のルートからのパスを続けて指します。pricing の 注文の合計.rule は、tax の税率を準用します。
@ のパスは、ファイルがどのディレクトリにあっても、モジュールのルートから読みます。どの言語も、パスを読むところでは @ のパスを読みます。規則の apply と import、ワークフローの use rule、カレンダーの use calendar、ゲートの use、kaname の use、yuen のリンク、sakai のコンテキストです。モジュールの中でコマンドラインに渡したパスも同じに読みます。ritsu rulec doc @tax/規則/付加価値税率.rule は依存の規則のページを書き、ritsu mod where はそのファイルがディスクのどこにあるかを言います。
- モジュールのルートは、
ritsu.modのあるディレクトリです。ritsu は、--rootが無ければ、渡したファイルの上で.gitかritsu.modを持つ一番近いディレクトリをルートにします。 - パスはモジュールの中に留まります。モジュールのルートの外へ出るパスと、下のモジュールの中へ
@を通らずに入るパスは、そのパスを読む言語のエラーです。ルートに、名前が@で始まるファイルかディレクトリがあるのもエラーです(E307)。 - 下のディレクトリで自分の
ritsu.modを持つものは、別のモジュールです。モジュールを歩くときは、その中に入りません。上から走らせたritsu checkは、それだけを検査する方法を言います。 ritsu.modの無いプロジェクトでは、何も変わりません。@で始まるパスも、ほかのパスと同じです。
モジュールを足す
ritsu mod add <名前> <取り先> で依存を足します。ディレクトリなら、走らせた場所からの ./… か ../… で、レジストリなら <リポジトリ>@<版> か <リポジトリ>@latest で、git のリポジトリなら <URL>@<版> か <URL>@latest で書きます(@latest か、版を書かないときは、プレリリースでない一番新しい版です)。最後の require の行のあとに一行書き、モジュールに lock が要れば ritsu.lock も書きます。
$ ritsu mod add pricing ../価格
$ ritsu mod add holidays ghcr.io/i2y/ritsu-holidays@1.0.0
$ ritsu mod add rates https://example.com/acme/rates@1.2.0
$ ritsu mod get holidays@latest
$ ritsu mod remove holidays
名前は、依存を足す側が決めます。プロジェクトのファイルが @ のあとに書く名前です。このサイトのモジュールの一覧の ritsu mod add の行は、祝日のモジュールをリポジトリの名前から ritsu_holidays と呼びますが、このページは、モジュールの README と同じく holidays と呼びます。
ritsu mod get <名前>@<版> はレジストリか git のリポジトリから取る依存の版を変え、ritsu mod remove <名前> は依存の行を消します。三つのコマンドは、ritsu.mod と ritsu.lock をそろって書くか、どちらも書かないかのどちらかで、ritsu.mod のほかの行と注はそのまま残します。
ritsu.lock は ritsu が書くファイルです。プロジェクトが読むモジュールに、依存の依存まで、一行ずつを持ちます。レジストリのモジュールの行は、選んだ版と二つのダイジェストを持ちます。レジストリと cosign が使う OCI のマニフェストのダイジェストと、モジュールのファイルのダイジェストです。git のモジュールの行は、同じものに加えて、タグが指していたコミットとリポジトリの URL を持ちます。版は最小の版の選び方(Minimal Version Selection)で選びます。モジュールごとに、require の行が求める版のうち一番高いものを選び、一つのモジュールには一つの版だけを使います。そのため、lock は、レジストリに問い合わせずに ritsu.mod だけから決まり、同じファイルを持つ二つの機械は、どのモジュールでも同じファイルを読みます。レジストリからも git のリポジトリからも取る依存の無いモジュールには、lock は要らず、作りません。
- どのコマンドも lock を
ritsu.modと突き合わせ、合わなければ止まります(E304)。ritsu mod lockが lock を書き直し、ritsu mod lock --checkは書き直しになるときに exit 1 で終わります。CI の一段に使えます。 - レジストリと git のリポジトリから取る依存のファイルは、読むたびに lock のダイジェストと突き合わせます(E305)。
- 作業ツリーの
require … pathは、木のどこにある同じモジュールのrequire … ociとrequire … gitよりも優先します。依存を直しながら、使う側のモジュールで確かめるときは、この行を書きます。
ritsu mod list は、プロジェクトが読むモジュールを並べます。
$ cd crates/ritsu/tests/projects/modules.ja/店
$ ritsu mod list --lang ja
モジュール example.com/acme/shop 0.1.0
@pricing example.com/acme/pricing 1.1.0 パス ../価格
@pricing/@tax example.com/acme/tax 1.0.0 パス ../税
ritsu mod list: 依存 2 個(パス 2 個、レジストリ 0 個)。lock は要りません
モジュールの中の ritsu check は、プロジェクトが読むモジュールを全部読み、そのファイルを、言語をまたぐ検査も含めて、自分のファイルと同じに検査します。
$ cd crates/ritsu/tests/projects/modules.ja/店
$ ritsu check . --lang ja
ok 会計/かご.kn: 関数 2 個、total
注文/注文を受ける.flow: 検査を通りました
依存: モジュール 2 個、ファイル 3 個(rulec 3)。どれも検査を通りました
ritsu check: ファイル 2 個(kaname 1、dandori 1)。どれも検査を通りました。言語の境目: 確かめた 0 か所、決められない 0 か所
- 依存のファイルのエラーは、検査を落とします。依存のファイルの警告は依存の行に数え、一つずつは
--deps-warningsで出します。 - 足した依存のファイルを、モジュールのどのファイルも指さなければ W301 です。依存の依存(
@pricing/@tax/…)を指すファイルは W303 です。そのモジュールを自分のritsu.modでrequireしてください。 - 依存の
.geasの主張は走らせません。主張は依存の作者のプログラムについてのもので、そのプログラムは手元の機械にありません。 - 依存の規則への
contributionは、プロジェクトの中でその規則に足されます。ritsu checkは、モジュールの境目を越えるものを一つずつ注で言います。
読むモジュールを取り出す
通信するのは ritsu mod だけです。ritsu check、gen、run、各言語のコマンドは、レジストリと git のリポジトリから取る依存を、ritsu mod fetch が置いたところから読みます。そこに無ければ、ritsu mod fetch を走らせるよう言って止まります。
ritsu mod fetchは、lock のモジュールのうち手元に無いものを取り出し、置き場に置きます。置き場は実行をまたいで使うディレクトリで、RITSU_MODULESが無ければ、macOS では~/Library/Caches/ritsu/modules、ほかでは$XDG_CACHE_HOME/ritsu/modules(無ければ~/.cache/ritsu/modules)です。ダイジェストと、signatureの行が求める署名を確かめ、全部のモジュールが通ったときだけ書きます。--rev <git の版>はその版の lock が固定するものを、<名前>@<版>はその版を取り出します。ritsu mod vendorは、そのコピーをモジュールの中(.ritsu/modules/)に置きます。レジストリやリポジトリに届かないビルドや、依存をリポジトリに入れておくときに使います。ritsu mod vendor --checkは、コピーが lock に合うかを確かめます。どのコマンドも、置き場より先にこのコピーを読みます。ritsu mod cleanは置き場の中身を消します。--keep <lock>…を付ければ、その lock が固定していないものだけを消します。消したものは、ダイジェストでいつでも取り直せます。ritsu mod where @<名前>/<パス>は、依存のファイルがディスクのどこにあるかを言います。エディタで開くときに使います。
レジストリには curl で HTTPS で問い合わせます(平文の HTTP は、この機械の上のレジストリだけです)。最初の問い合わせは資格情報なしで送ります。レジストリが資格情報を求めれば、ritsu は Docker の設定ファイル($DOCKER_CONFIG/config.json、無ければ ~/.docker/config.json)を、docker login と資格情報のヘルパーが書いた形で読み、そこにあるヘルパーを走らせます。資格情報とトークンは、どの出力、ログ、ファイルにも出しません。
npm のパッケージは、置き場と同梱のコピーを、ネイティブの ritsu と同じに読みます。レジストリに問い合わせるコマンドと、git か cosign を走らせるコマンド(add、get、fetch、versions、push、report、site と、取り出しが要るときの lock、remove、diff)は、ネイティブの ritsu を使うよう言って止まります。API の dirs で開くディレクトリを絞るときは、置き場を dirs に足すか、ritsu mod vendor で同梱のコピーをモジュールに置きます。
git でモジュールを共有する
自分たちのリポジトリのあいだで共有するモジュールには、レジストリが要りません。作者がコミットにタグ v<版> を打って push し、使う側はリポジトリの URL で依存に足します。リポジトリの中のディレクトリにあるモジュールは --dir で指し、タグは <ディレクトリ>/v<版> です。
$ ritsu mod add rates https://example.com/acme/rates@1.2.0
$ ritsu mod add tax git@example.com:acme/mono.git@1.0.0 --dir modules/tax
$ ritsu mod versions https://example.com/acme/rates
require rates git "https://example.com/acme/rates" 1.2.0
require tax git "git@example.com:acme/mono.git" 1.0.0 dir "modules/tax"
- URL は
https://…、ssh://…、scp の形の<ユーザー>@<ホスト>:<パス>のどれかです。git://、file://、ほかの機械への平文のhttp://、<transport>::…、パスワードかクエリを含む URL は E302 です。 - モジュールの名前は、置いてある場所です。ホストと、
.gitを除いたリポジトリのパスと、dirをつなぎ、スキーム、ユーザー、ポートは入れません(example.com/acme/rates、example.com/acme/mono/modules/tax)。タグのritsu.modには、この名前のmoduleと、タグの版を書きます。 - 名前は小文字なので、URL も小文字で書いてください。多くのホストは、リポジトリのパスの大文字と小文字を区別しません。区別するホストで、パスに大文字を含むリポジトリは、依存に足せません。
vの無いタグ、+を含むタグ、SemVer の形でないタグは、版になりません。プレリリースは版になりますが、@latestでは選びません。- lock は、タグが指していたコミットを、URL と一緒に固定します。
ritsu mod fetchは、そのコミットを名前で取り、チェックアウトはしません。コミットと木を取り、モジュールのファイルをまとめて取って、git が持つバイトのまま読みます。filter も改行の変換もフックも効きません。モジュールの中にシンボリックリンク、サブモジュール、Git LFS のポインタがあれば止まります。 - タグが動いたときは、レジストリのタグと同じに扱います。
ritsu mod fetchは lock のコミットを取り続け、ritsu mod getはタグがいま指すコミットを取り、ritsu mod reportは動いたことを先頭に出します。リポジトリが lock のコミットをもう渡さなければ、ritsu mod fetchはタグが動いたと言って止まります。 - ritsu は、2.30 以上の git を、手元の設定のまま走らせます。ssh の鍵、資格情報のヘルパー、
url.<base>.insteadOfは、git fetchと同じく効きます。git に端末でパスワードを聞かせないので、資格情報が無ければすぐに止まります。
git で共有するモジュールには、確かめる署名がありません。lock のコミットとダイジェストで固定し、そのモジュールに当たる signature の行は E309 です。git から取るモジュールを依存に持つモジュールは、レジストリに送り出せません。git から取るモジュールの名前は置いてある場所なので、一つのモジュールを配る方法はどちらか一つです。git で共有していたモジュールをレジストリに移すときは、module の行をレジストリのリポジトリに変えて、新しい版を送り出します。使う側は require … git の行を require … oci に書き換えて、ritsu mod lock を走らせます。ファイルは使う側が付けた名前でモジュールを指すので、変わりません。
GitHub Actions では、actions/checkout が持つトークンは、チェックアウトしたリポジトリのためのもので、ほかのリポジトリは読めません。ritsu は git を自分の一時のリポジトリで走らせるので、このトークンを使いません。ほかの非公開のリポジトリのモジュールを依存に持つジョブでは、そのリポジトリを読めるトークンをシークレット(ここでは MODULES_TOKEN)に置き、git の大域の設定で git に渡します。トークンを環境変数から読む資格情報のヘルパーにすれば、トークンはファイルに残りません。ユーザーの名前は、ホストがトークンと一緒に受け付けるものにします。
git config --global credential.https://example.com.username ci
git config --global credential.https://example.com.helper '!f() { test "$1" = get && echo "password=$MODULES_TOKEN"; }; f'
これを ritsu より前のステップで走らせ、ritsu mod fetch を走らせるステップの env に、シークレットを MODULES_TOKEN として渡してください。
レジストリに送り出す
ritsu mod push は、モジュールの名前が指すリポジトリに、版をタグにして送り出します。
$ cd modules/holidays
$ ritsu mod push --dry-run --lang ja
ghcr.io/i2y/ritsu-holidays 1.0.0 の成果物を作りました(`--dry-run` なので、送っていません)
…
- 名前がレジストリの形で、
versionとritsu >=の行があり、require … pathもrequire … gitも無く、lock が合っているモジュールだけを受け付けます。 - モジュールのファイルを集めます。
excludeの行が挙げるもの、名前が.で始まるもの、下のモジュールは入れません。鍵を持つファイルがあれば受け付けません(W901。ファイルと行を言い、鍵そのものは出しません)。 - 集めたファイルのコピーを
ritsu checkにかけ、エラーがあれば受け付けません。モジュール自身の.geasの主張は、主張が走らせるプログラムのある作業ツリーでのritsu checkに任せます。 - OCI の成果物を作ります。同じファイルからは、いつも同じバイトと同じダイジェストができるので、だれでもソースからダイジェストを作って比べられます。
--dry-runはここで止まり、ダイジェストを出します。 - タグに同じファイルがあれば、送り出し済みです。違うファイルがあれば、送り出しを受け付けません。版は動かさないので、版を上げてください。
- レジストリに無いものを送り、参照(
<名前>@sha256:…)と、署名するcosign signのコマンドを出します。
先に docker login でログインしておきます。ritsu も cosign も、Docker が持つ資格情報を読みます。このリポジトリのモジュールを公開するワークフロー .github/workflows/modules.yml は、モジュールを検査し、ghcr.io に送り出し、鍵を持たずに cosign で署名し、使う側のモジュールと同じ形で署名を確かめます。
ritsu mod site <一覧のファイル> --out <ディレクトリ> は、このサイトのモジュールの一覧のような、モジュールを並べるサイトを書きます。モジュールごと、版ごとのページ、ファイルごとのページ、一つ前の版からの差、ritsu mod add の行、版をブラウザで試すページで開くボタンを作ります。一覧のファイルに書く行は、title、description、playground、module(レジストリのモジュール。全部の版を取り出して確かめます)、git(git で共有するモジュール。git "<URL>" か git "<URL>" dir "<ディレクトリ>" と書き、全部のタグを取り出して確かめます)、signature(ritsu.mod と同じ)、local(まだ送り出していない作業ツリー)です。確かめを通らない版があれば exit 1、一覧のファイルが読めないか、レジストリかリポジトリに届かなければ exit 2 で終わります。
署名
ritsu は署名しません。ritsu mod push が出したダイジェストに、cosign で署名します。GitHub Actions で鍵を持たずに署名するなら、権限 id-token: write を付けて、次を走らせます。
鍵で署名するなら cosign sign --key cosign.key <名前>@sha256:… です。依存を足す側のモジュールは、どの署名を受け付けるかを、ritsu.mod の signature の行に書きます。このリポジトリのモジュールなら、次の行です。
signature "ghcr.io/i2y/ritsu-holidays" by matching "^https://github[.]com/i2y/ritsu/" issuer "https://token.actions.githubusercontent.com"
signature "<名前>" … |
cosign に渡すもの |
|---|---|
key "keys/acme.pub" |
--key。鍵のファイルはモジュールのルートから読みます |
key "keys/acme.pub" tlog off |
同じ。透明性ログに載っていない署名も受け付けます |
by "<署名者>" issuer "<発行者>" |
--certificate-identity と --certificate-oidc-issuer |
by matching "<正規表現>" issuer "<発行者>" |
--certificate-identity-regexp と --certificate-oidc-issuer |
signatureの次の文字列は、モジュールの名前か、/で終わる名前の頭の部分("example.com/acme/")です。- モジュールに効くのは、手元のモジュールの行と、そのモジュールを直に
requireしているモジュールの行です。効く行は全部通らなければなりません。 ritsu mod fetchは、モジュールを置き場に置く前に PATH の cosign を走らせ、確かめたことを置き場に記録します。ritsu checkは cosign を走らせません。効く行が記録に無ければ、ritsu mod fetchを走らせるよう言って止まります。- ritsu が署名を確かめるのは、レジストリから取るモジュールだけです。git から取るモジュールに当たる行は E309 です。行の頭の部分を狭めるか、そのモジュールをレジストリから取ってください。
版を上げると何が変わるか
ritsu mod diff <旧> [<新>] は、モジュールの二つの版を比べます。それぞれの側は、<モジュールの名前>@<版>(木が git から取るモジュールなら、その git のリポジトリから取ります)、ディレクトリ、.(作業ツリー。<新> を書かないときはこれ)のどれかで、どちらかの側が読むモジュールが手元に無ければ取り出します。足したファイル、消したファイル、変わったファイルと、require の変わりを並べます。両方の版にある規則、日付のファイル、カレンダー、ゲートのそれぞれについて、答えが変わる入力を、試した点だけでなく入力の全体で求めて出し、ほかの入力は同じ答えになると言います。変わったファイルごとに、使う側のコードに見せる形の差を、使う側を壊しうるものから並べます。次の例では、pricing の 1.1.0 が、2,500g までの荷物の送料を下げています。
$ cd crates/ritsu/tests/projects/modules.ja/価格
$ ritsu mod diff ../版/価格-1.0.0 . --lang ja
ritsu mod diff:example.com/acme/pricing 1.0.0 → 1.1.0
古い版:../版/価格-1.0.0
新しい版:.
…
振る舞い:
規則/注文の合計.rule (rulec):同じ答えです
規則/送料.rule (rulec):答えが変わります
…
地域 = 国内 かつ 重さ >=2001g <=2500g
料金: 695 → 395
当たる行: 表 重さの段 行2 → 表 重さの段 行1
例: 地域=国内, 重さ=2001
…
インターフェース:
規則/送料.rule (rulec、api):差が 10 件、うち使う側を壊しうるもの 0 件
足した:/{python,typescript,javascript,rust,ruby,php,go,swift,java,connect}/enums/地域/values/EU
…
壊しうる差は候補です。使う側を壊すかどうかは、使う側の検査が決めます。使う側の検査によらず壊す差もあります。帳簿の勘定や振替の定義が変わるのは chobo の E050 と E051 で、データベースにすでにある勘定や仮押さえは前の定義のまま残ります。列挙の値やメッセージのフィールドの Connect での番号が変わるのは E310 で、片方の版が書いたメッセージを、もう片方は別の値や別のフィールドとして読み、どちらも失敗しません。rulec は規則に書いた順に番号を振ります。pricing の 1.1.0 が EU をほかの地域の後ろに足しているのは、このためです。ritsu mod diff はこれらをインターフェースの差の中に示し、ritsu mod report はエラーとして数えます。rulec diff、koyomi diff、sekisho diff も、版をこれまでと同じ書き方で指せるうえに、二つの書き方を受け付けます。@<名前>/<パス> は lock が固定する版の依存のファイルで、@<名前>@<版>/<パス> はモジュールのその版のファイルです。
ritsu mod report --base <git の版> は、依存を上げる PR が知りたいことに答えます。手元のファイルは head のままにして、base の依存で一度、head の依存で一度読み、二つの lock が固定するモジュールを取り出します。head で増えた診断と消えた診断を挙げ、変わったモジュールごとに、その差と、手元のファイルの答えのうち変わるもの、変わる答えを読む手元のフローと kaname のモジュールの行を言います。dandori と kaname には振る舞いの差が無いので、その行は比べずに並べます。タグが動いたモジュール(同じ版なのにファイルが違うもの)は、いちばん上に出します。markdown は PR のコメントにする形です。
# .github/workflows/ritsu-mod-report.yml
name: ritsu mod report
on:
pull_request:
paths: [ritsu.mod, ritsu.lock]
permissions:
contents: read
pull-requests: write
jobs:
report:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: i2y/ritsu@v0.37.0
- name: Report what raising the dependencies changes
env:
GH_TOKEN: ${{ github.token }}
PR: ${{ github.event.pull_request.number }}
run: |
code=0
ritsu mod report --base "origin/$GITHUB_BASE_REF" --format markdown > report.md || code=$?
ritsu mod report --base "origin/$GITHUB_BASE_REF" --format json > report.json || true
if [ -s report.md ]; then gh pr comment "$PR" --body-file report.md; fi
exit "$code"
- uses: actions/upload-artifact@v7
if: always()
with:
name: ritsu-mod-report
path: report.json
fetch-depth: 0は、origin/<base>を読めるように履歴を取ってくるためです。ritsu mod reportは、head の検査にエラーがあれば exit 1、変わったモジュールを比べられなければ exit 2 で終わります。答えが変わったときにも落とすなら、--fail-on-changeを付けます。- モジュールがリポジトリのルートに無いときは、そのディレクトリを
pathsと--rootに書きます。 - fork からの PR では、書き込めないトークンになるので、コメントは付きません。
- 報告を日本語で書くなら、
--lang jaを付けます。
ライブラリ
モジュールは、ritsu gen --library で npm、PyPI、Go 向けに公開できます(ライブラリとして公開する)。