# Latchkey でワークフローファイルを実行する

> Latchkey のワークフローファイル: 受け付けるキー、名指しで拒否するキーとその理由、ファイルの置き場所、そして push したときに起きること。

Source: https://latchkey.dev/ja/documentation/workflow-files

## Summary

- `.latchkey/workflows/<name>.yml` にワークフローをコミットします。形式は GitHub Actions でおなじみのもので、`uses:` だけがありません。
- Latchkey はキーを無視して実行するのではなく、ファイルを拒否します。push ごとに拒否は 1 件で、ファイル内で最初に拒否されたキーを、あなたが書いたとおりの名前と行番号で示すか、拒否がファイル全体に関するものであればファイルそのものを示します。
- `runs-on:` は Latchkey のサイズを指定します。コピーしてきたファイルの `runs-on: ubuntu-latest` は `runs-on: small` になります。
- チェックアウトは自動で、ランナーイメージには 12 種類の言語ツールチェーンが入っています。`actions/checkout` と `actions/setup-*` のステップはそのまま不要になります。

## お約束

Latchkey はあなたのワークフローファイルを実行します。`.latchkey/workflows/` にワークフローを 1 つ 1 ファイルでコミットしてください。形式は GitHub Actions でおなじみのもので、ジョブ、`run:` を持つステップ、`env:`、Latchkey のランナーサイズを指定する `runs-on:`、`timeout-minutes:`、あるジョブを別のジョブのあとに実行する `needs:`、マトリクス、そして `services:` の下のサービスコンテナです。push すると、Latchkey は少数の固定されたコンテキストを実行レコード上で確定し、それらをブランチ保護がバインドできるチェックとして報告します。コミットしたファイルは、今後のすべてのリリースでそのまま動き続けます。

ブランチ保護では、Latchkey と Latchkey / plan のコンテキストを必須にできますが、まだ必須にしないでください。Latchkey が書き込めないチェックは存在しないままになり、必須のチェックが存在しないとマージはブロックされます。また、Latchkey / plan は Latchkey 自身による停止も失敗として報告するため、これを必須にすると Latchkey の障害によってマージがブロックされます。

ニュートラルの Latchkey チェックは、Latchkey がこのコミットを検証しなかったことを意味します。あなたのコードは成功も失敗もしていません。ジョブが結果を決める前に何かが実行を止めたのであり、何が止めたかはチェックのテキストに書かれています。ニュートラルのチェックはマージをブロックしないため、何も検証されていなくても、ブランチ保護はそのプルリクエストを通します。

## 今リリースで動くもの

このリリースでワークフローファイルが提供するのは、ジョブ、1 つずつ順に実行されるステップ、`env:`、ランナーサイズ、タイムアウト、マトリクス、サービスコンテナ、そして `needs:` でほかのジョブを待つジョブです。ステップは、このリポジトリまたはそのワークスペースに対して Latchkey で設定されたシークレットを読み取れます。そのステップの `env:` で変数の値全体として `${{ secrets.NAME }}` と書くと、Latchkey はそのステップの開始時にその値をステップに渡します。同じ名前のシークレットがリポジトリとワークスペースの両方にある場合はリポジトリのシークレットが優先され、どちらのレベルにも設定されていない名前があると、どのジョブも開始される前に実行が拒否されます。また、フォークからのプルリクエストの実行にはシークレットが一切渡されません。Latchkey が自身のブランチで提案した修正の実行にシークレットが渡されるのは、その修正がワークフローファイルを 1 つも変更しない場合だけです。シークレットが渡されない実行では、シークレットを参照するステップは実行されません。28 バイト未満の値は、ログで隠すことができないため、決して渡されません。

## ファイルの置き場所

`.latchkey/workflows/` にワークフローごとに 1 ファイルを置きます。正式な拡張子は `.yml` ですが `.yaml` も読み取るので、`.github/workflows/` からコピーしたファイルは拡張子をそのままにできます。ワークフローファイルの名前を変更すると新しいパイプラインが始まり、古い名前での実行はその名前のまま残ります。

`.latchkey/workflows/ci.yml`
```yaml
name: CI

on:
  push:
    branches: [main]
  pull_request:

latchkey:
  schema: 1

jobs:
  test:
    runs-on: small
    timeout-minutes: 20
    env:
      NODE_ENV: test
    steps:
      - name: Install dependencies
        run: npm ci
      - name: Run the suite
        run: npm test
```

## ランナーを選ぶ

| `runs-on:` の値 | vCPU | メモリ | ディスク |
| --- | --- | --- | --- |
| `small` or `latchkey-small` | 2 | 8 GB | 100 GB |
| `medium` or `latchkey-medium` | 4 | 16 GB | 100 GB |
| `large` or `latchkey-large` | 8 | 32 GB | 100 GB |
| `xlarge` or `latchkey-xlarge` | 16 | 64 GB | 200 GB |

どのジョブも `runs-on:` を 1 つ指定します。どちらの表記も受け付けられ、同じランナーを指します。Latchkey の Actions ワークフローからコピーした `runs-on:` の行はそのまま動きます。GitHub ホステッドのファイルによくある行は `runs-on: ubuntu-latest` ですが、Latchkey は GitHub ホステッドではなく、その名前を黙ってサイズに読み替えることはしません。`runs-on: small`、あるいはジョブに必要なサイズに書き換えてください。すべてのランナーは x86_64 上の Ubuntu 24.04 LTS です。各サイズの内容は [制限と同時実行数](https://latchkey.dev/ja/documentation/runner-limits) を参照してください。

## エミュレートしないもの

Latchkey は GitHub のアクションを実行しません。`uses:` ステップは、そのステップと Latchkey での代替手段を名指ししたメッセージで拒否されます。チェックアウトは自動 (ランナーがジョブごとのトークンで、保留が解除されたフォークではトークンなしで、そのコミットのリポジトリをクローンします)、`actions/cache` は `latchkey cache save` と `latchkey cache restore`、setup 系アクションはイメージ自身のツールチェーン (Node、Python、Go、Rust、Java が標準ランナーイメージに入っています)、アーティファクトはまだサポートしておらず、サードパーティのアクションは `run:` ステップになります。Actions 互換のランタイムを約束することはありませんし、今後もしません。

## ツールチェーンは最初から入っています

標準ランナーイメージには Python、Node、Go、Java、Rust、Ruby、PHP、.NET、Haskell、Swift、Kotlin、Julia が入っており、この 12 種類のどれかが欠けているとイメージのビルドが失敗します。`uses: actions/setup-*` ステップに書き換える先はありません。削除して、そのままツールを呼び出してください。

## チェックアウト

最初のステップが走る前に、ランナーが push されたコミットでリポジトリをクローンするので、ワークフローファイルにチェックアウトのステップは要りません。フォークからのプルリクエストは保留が解除されてはじめて実行され、そのコミットはトークンを一切使わずに、このリポジトリ自身のアドレスから取得されます。このクローンは shallow でタグを持ちません。これはベンダー自身のチェックアウトアクションと同じ既定値なので、タグからバージョンを導出したり履歴を読むステップは、このリリースではそれらを利用できません。

## ジョブの実行のしかた

Latchkey はジョブのステップを、ファイルに書かれた順に 1 つずつ実行します。各ステップは、ジョブのワークスペースでジョブの環境を引き継いで起動される、独立した `bash --noprofile --norc -e -o pipefail` スクリプトです。ステップは最初に失敗したコマンドで停止し、パイプラインの途中で失敗したコマンドはパイプライン全体を失敗させます。あるステップが失敗すると、それ以降のステップは、その `if:` が `failure()` または `always()` を指定しているときにだけ実行され、ジョブは最初に失敗したステップの終了コードで失敗します。あるステップでの `cd` や `export` は、次のステップには持ち越されません。

各ステップは独自のプロセスグループで動く独立したスクリプトなので、`kill $$` や `kill 0` などでステップが自身のシェルやプロセスグループを終了させても、終了するのはそのステップだけです。そのステップは終了コード 143 で失敗し、それ以降の `failure()` や `always()` のステップはそのまま実行されます。また、各ステップのスクリプトは個別に読み込まれるため、構文エラーのあるステップは終了コード 2 でそのステップだけが失敗し、それ以降のステップのうち `if:` が実行を認めるものはすべてそのまま実行されます。ジョブ自身の `if:` が成り立たないジョブは、構文エラーの有無にかかわらず、そのステップを 1 つも実行しません。さらに、各ステップのスクリプトはそのステップの開始時にジョブの一時ディレクトリ (`$RUNNER_TEMP`) に書き出されるため、あるステップがそのディレクトリやジョブの作業ディレクトリを削除したり、ほかの形で次のステップの開始に必要なものを取り除いたりすると、ジョブは次のステップの時点で停止します。次のステップは開始されず、`always()` のステップも含めてそれ以降のステップは 1 つも実行されず、ジョブは終了コードなしで失敗します。

`if:` が受け付けるのは `success()`、`failure()`、`always()`、そしてマトリクス値または `env` 値の等価比較だけです。その範囲を超える `if:` は、Latchkey が推測して評価するのではなく、名指しで拒否されます。ジョブ自身の `if:` が成り立たないジョブはスキップされます。そのステップは実行されず、実行のチェックはそのジョブを成功でも失敗でもなく、スキップとして数えます。

## ほかのジョブを必要とするジョブ

`needs:` を持つジョブは、そこに挙げたすべてのジョブが成功したあとに開始します。ジョブは 1 つ (`needs: build`) でも複数 (`needs: [build, test]`) でも、同じファイルの `jobs:` の下のキーで指定してください。マトリクスのジョブは、そのすべてのレグが成功したときに成功したことになります。`needs:` を持たないジョブは、これまでどおりほかのジョブを待たずに開始します。必要とするジョブを待ったまま、キューに入ってから 24 時間たったジョブは停止され、そのタイルには「Stopped」と表示されます。それまでに必要とするジョブのいずれかが成功せずに終わっていた場合は、代わりにスキップされます。

待っているあいだ、実行ページのそのジョブのタイルには、まだ待っているジョブの名前が表示されます (「Waiting for build.」)。必要とするジョブが失敗した、キャンセルされた、またはスキップされたときは、そのジョブは開始される前にスキップされます。実行のチェックはそのジョブを成功でも失敗でもなく、スキップとして数え、そのジョブにランナーが使われることはありません。また、そのタイルにはどれが起きたかが表示されます (「Skipped: a job it needs failed.」)。スキップは連鎖します。スキップされたジョブを必要とするジョブも、スキップされます。

`needs:` を持つジョブでは、ジョブレベルの `if:` でマトリクス値または `env` 値を判定できます。そこでの `failure()` と `always()` は名指しで拒否されます。必要とするジョブが成功しなかったジョブは、どの `if:` も読まれる前にスキップされるため、どちらもそのジョブを実行できないからです。

## Latchkey が設定する環境変数

ワークフローファイルから Latchkey が起動したジョブでは、次の名前が設定されます。

| 名前 | 値 |
| --- | --- |
| `LATCHKEY_CI_RUN_ID` | このジョブが属する実行 |
| `LATCHKEY_CI_ATTEMPT` | 試行回数、1 から数えます |
| `LATCHKEY_CI_REPOSITORY_ID` | リポジトリの Latchkey 識別子 |
| `LATCHKEY_CI_PROVIDER` | git プロバイダ、たとえば `github` |
| `LATCHKEY_CI_SHA` | 実行が固定されているコミット |
| `LATCHKEY_CI_REF` | プロバイダから届いたままの ref |
| `LATCHKEY_CI_ACTOR` | 実行を起動した人 |
| `LATCHKEY_CI_PR_NUMBERS` | このコミットを含むプルリクエスト。カンマ区切りで最大 16 件、それより多いときは末尾に `+` が付きます。 |
| `LATCHKEY_CI_FORK` | プルリクエストがフォークからのとき `true`。そのジョブはリポジトリトークン、キャッシュ、シークレット、セルフヒールのいずれもなしで実行されます。そのステップが到達できるのは GitHub と固定された一連の公開パッケージレジストリだけで、sudo、Docker、サービスコンテナは使えません |
| `LATCHKEY_CI_BASE_REF` | 完全な `refs/heads/` パスで表したベース ref。プルリクエストではそのベースブランチ、push では新しいブランチの作成元となった ref です。それ以外の場合は空です。 |
| `LATCHKEY_CI_EVENT_CLASS` | 実行を開始した正規化済みイベント |
| `LATCHKEY_CI_REPOSITORY` | この実行が属するリポジトリ、`owner/name` 形式 |
| `LATCHKEY_CI_STEP_NUMBER` | 実行中のステップの番号、1 から数えます。ステップのコマンドから読めますが、`if:` では読めません |

リポジトリのフルネームは `owner/name` の形式でそのまま公開され、URL ではありません。LATCHKEY_CI_REPOSITORY はリポジトリが登録されたときの名前を保持するため、コードホスト側でリポジトリの名前を変更しても変わりません。`LATCHKEY_` で始まる名前はすべて Latchkey のものなので、ワークフローファイルから設定することはできません。

## `latchkey:` ブロック

トップレベルに置ける任意のブロックで、メンバーはちょうど 1 つです。値はテキストとして読まれるため、`schema: 1` と `schema: "1"` は同じファイルです。`latchkey:` ブロックのないファイルはスキーマバージョン 1 として読まれます。ブロックを書く場合は `schema:` を持ち、それ以外のキーを持たない必要があります。

## ファイルが実行される条件

`on:` は `push` (任意で `branches:` と `tags:`) と `pull_request` (任意で `branches:` と `types:`) を受け付けます。`on: push` だけならブランチへの push でのみ実行され、タグの push は `push` ブロックが `tags:` を指定しているときにだけファイルを実行します。`on: pull_request` では `branches:` はプルリクエストのベースブランチに一致し、head ref には一致しません。

Latchkey はプルリクエストのワークフローを opened、synchronize、reopened の 3 つすべてで常に実行するので、`types:` を書くならその 3 つをちょうど列挙するか、書かないかのどちらかです。`on: schedule` はまだサポートしていません。

> **フォークからのプルリクエスト**
>
> Latchkey がフォークからのプルリクエストを実行するのは、それが作成者自身のフォークからのもので作成者がこのリポジトリへの書き込み権限を持っている場合か、メンテナーがそのコミットを承認した場合だけです。そのため、このコードはまだ検証されていません。実行するには、メンテナーがこのコミットそのものを承認する (新しい push には新しい承認が必要です) か、この変更をこのリポジトリのブランチに新しいコミットとして push してください。

Latchkey のチェックを必須にしている場合、保留中のフォークからのプルリクエストは、メンテナーがそのコミットを承認するか、変更がこのリポジトリのブランチに新しいコミットとして push されるまでマージをブロックします。

## 何も実行されないとき

次の 3 つは、push は読まれたが一致するものがなかったという結果で、4 つ目は Latchkey がそのプルリクエストを実行しないという結果です。どれも失敗ではありません。

| 理由 | 意味 |
| --- | --- |
| `no_workflow_file` | このコミットの .latchkey/workflows/ にワークフローファイルがないため、何も実行されませんでした。 |
| `no_workflow_matched` | .latchkey/workflows/ のどのワークフローもこのイベントに一致しないため、何も実行されませんでした。 |
| `fork_base_no_workflow_file` | このプルリクエストが対象とするベースコミットにワークフローファイルがないため、何も実行されませんでした。フォーク自身のワークフローファイルは実行されません。まずこのリポジトリのブランチにファイルを追加してください。 |
| `fork_private_repository` | Latchkey はプライベートリポジトリのフォークからのプルリクエストを実行しないため、このコードは検証されていません。実行するには、この変更をこのリポジトリのブランチに新しいコミットとして push してください。 |

## マトリクス

マトリクスは組み合わせごとに 1 つのジョブへ展開されます。レグのキーは、ジョブのキーにレグの値をキー順に並べてカンマで連結したものを付け足した形になります。たとえば `os: linux` と `node: 20` を持つ `build` というジョブのレグキーは `build(node=20,os=linux)` です。1 つのジョブが展開できるのは最大 50 レグ、1 つのワークフローファイルに含められるのは全ジョブを合わせて最大 50 レグ、1 回の実行に含められるのは、そのイベントに一致したすべてのワークフローファイルを通して数えて最大 150 ジョブです。

Actions では `fail-fast` の既定値は true で、1 つが失敗すると他のレグをキャンセルします。Latchkey はすべてのレグを実行し、どれもキャンセルしません。そのため `fail-fast: false` は Latchkey の動作そのものなので受け付けられ、`fail-fast: true` は黙って無視されるのではなく拒否され、`max-parallel` も拒否されます。

## サービスコンテナ

ジョブは `services:` の下にサービスコンテナを宣言できます。それぞれはラベルと `image:` の組です。Latchkey はジョブ専用のネットワーク上で、ジョブの最初のステップより前にすべてのサービスを起動します。ステップはサービスのラベルをホスト名として、そのイメージが待ち受けるポートでサービスに接続します。たとえば `redis:7` を実行する `cache` というラベルのサービスは、そのジョブのどのステップからも `cache:6379` です。サービスは `ports:` の下で、`<runner port>:<service port>` の形でランナーのポートを公開することもでき、その場合ステップは `localhost:<runner port>` でもサービスに接続できます。ラベルは英小文字、数字、`-` からなり、英字で始まり、ハイフンで終わらず、63 文字以内で、`localhost` は使えません。1 つのジョブが宣言できるサービスは最大 10 個です。フォークからのプルリクエストのステップは、どのサービスコンテナにも到達できません。

`.latchkey/workflows/ci.yml`
```yaml
jobs:
  test:
    runs-on: small
    services:
      cache:
        image: redis:7
    steps:
      - run: nc -z cache 6379
```

`.latchkey/workflows/ci.yml`
```yaml
jobs:
  test:
    runs-on: small
    services:
      db:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: postgres
        ports:
          - 5432:5432
    steps:
      - run: pg_isready -h localhost -p 5432
```

Latchkey は各サービスの準備ができるまで待ちます。イメージがヘルスチェックを宣言していれば healthy になるまで、宣言していなければイメージが公開するポートのいずれかが接続を受け付けるまで、どちらでもなければ実行中になるまでです。待つのはイメージの取得を含めて、すべてのサービスで合計最大 5 分です。イメージを取得できない、停止した、または時間内に準備ができなかったサービスがあると、ジョブは最初のステップが実行される前に失敗し、ジョブのログにはそのサービス、イメージ、理由が書かれます。ジョブがどのように終わっても、Latchkey はサービスと、そのために作成したものをすべて削除します。いずれにしても、ランナー自体はその 1 つのジョブのあとに破棄されます。

サービスは公開イメージを実行します。サービスの下では、`env:` でイメージが起動時に読む変数 (たとえば `postgres` の `POSTGRES_PASSWORD`) を設定し、`ports:` でランナーのループバックアドレスにポートを公開します。`options:`、`volumes:`、`credentials:` は名指しで拒否されるため、取得に認証情報が必要なイメージはサービスとして実行できません。代わりに `run:` ステップで起動してください。サービスの `image:` や `env:` にはマトリクス値を使えず、サービスの `env:` はシークレットを読めません。

Latchkey はサービスの設定をジョブログに書き込みません。28 バイト以上の設定は、ステップがどこで出力しても、ログでもセルフヒールが読む内容でも隠されます。テスト用データベースのパスワードのような短い設定は隠されないため、本物の認証情報はサービスの設定に入れないでください。認証情報に見える値は拒否され、シークレットはステップの `env:` に置くものです。

## 拒否メッセージの読み方

Latchkey はキーを無視して実行するのではなく、ファイルを拒否します。push ごとに拒否は 1 件で、ファイル内で最初に拒否されたキーを、あなたが書いたとおりの名前と行番号で示すか、拒否がファイル全体に関するものであればファイルそのものを示します。plan コンテキストは実行レコード上でその拒否を確定します。Latchkey / plan チェックは、どこで拒否されたかを示します。

ファイルを移行するときに出会いやすいのは次の 3 つです。

- `uses:` はサポートしていません。ステップは `run:` として書いてください。チェックアウトは自動で、キャッシュは `latchkey cache save` と `latchkey cache restore`、ツールチェーン (Node、Python、Go、Rust、Java) はランナーイメージに入っています。
- サービスの下の `options:`、`volumes:`、`credentials:` はサポートしていません。サービスは `env:` の設定と `ports:` のポートで公開イメージを実行し、ステップはラベルか公開されたポートでサービスに接続します。
- `notify` の `if:` は `always()` を使っていて、`notify` には `needs:` があります。必要とするジョブが成功しなかったジョブは開始される前にスキップされるため、`always()` と `failure()` ではそのジョブを実行できません。`success()`、マトリクスまたは `env` の判定を使うか、`needs:` を削除してください。

## ジョブが失敗したときの意味

失敗したジョブは Latchkey のセルフヒールに渡され、修復はステップ単位ではなくジョブ全体に対して報告されます。フォークからのプルリクエストではセルフヒールは実行されません。すべての失敗があなたのコードによるものとは限りません。Latchkey は、自身の障害、あなたによるキャンセル、請求や接続の問題を、あなたのコマンド自体の結果と区別します。Latchkey チェックが赤になるのは、あなたのコマンドが失敗したか、ジョブ自身が失敗を報告したときです。Latchkey が自身の障害だと確認できる障害によって Latchkey チェックが赤になることはなく、チェックのテキストにはその障害が Latchkey によるものだと書かれます。push またはプルリクエストで開始されたジョブのログは、現在 `latchkey logs <job id>` と MCP の `get_job_logs` ツールで取得でき、そのジョブはジョブ一覧と実行ページにも表示されます。実行ページでは、各ステップのログを実行中に確認できます。Latchkey がランナーに配置できなかったジョブはそのことを示し、課金されることはありません。

終了した実行のジョブをもう一度実行するには、GitHub の Latchkey チェックで Re-run を押すか、ダッシュボードの実行で **Re-run jobs that did not pass** を押します。Latchkey は同じ実行の新しい試行として、成功しなかったすべてのジョブと、そのいずれかを必要とするすべてのジョブを実行します。それ以外の成功したジョブは引き継がれ、再び実行されることも課金されることもありません。ダッシュボードの **Cancel run** は、実行の中でまだ終了していないすべてのジョブをキャンセルし、その後、実行は Cancelled と表示されます。

## 他のリポジトリで変わらないこと

`latchkey-*` ランナー上の GitHub Actions ジョブは、これまでとまったく同じように、同じ組織、同じ請求、同じ無料枠で動き続けます。リポジトリは 1 つずつ Latchkey CI に移せます。

## 前方互換性

このリリースでコミットしたファイルが移行されることはありません。今後のすべてのリリースで、同じコンパイラがそのファイルを読みます。

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
