# CIからテスト結果を自動記録する

GitHub Actions のテスト結果を、PM on Rails の受入シナリオへ自動で記録できます。

**GitHub Secrets に PM on Rails の token や PAT を登録する必要はありません。**
**専用のライブラリ・CLI・変換スクリプトも要りません。**

やることは3つだけです。

1. PM on Rails と GitHub リポジトリを GitHub App で連携する
2. テスト名に受入シナリオの Scenario ID を `[pmr:<Scenario ID>]` の形で書く
3. テストランナーの **JUnit XML** を `pmr-scenario-report` で始まる artifact としてアップロードする

その後は、GitHub Actions が終わるたびに PM on Rails が自動で結果を取り込みます。

```text
GitHub Actions (JUnit XML)
    ↓
pmr-scenario-report* artifact
    ↓
PM on Rails が GitHub App 経由で取得
    ↓
テスト名の Scenario ID で受入シナリオに「合格 / 不合格」を記録
```

> **CI から PM on Rails の API を直接呼ぶ方式ではありません。** 通常の GitHub Actions 連携では `PM_ON_RAILS_TOKEN` は不要です。

## 1. GitHub リポジトリを連携する

PM on Rails の **設定 → GitHub連携** から対象リポジトリを接続します。

GitHub App には **Actions: Read** 権限が必要です。PM on Rails はこの権限を使って、GitHub Actions が残した artifact を読み取ります。

詳しい接続手順は [GitHub連携](https://pmonrails.com/docs/github.md) を参照してください。

## 2. テスト名に Scenario ID を書く

PM on Rails の受入シナリオには Scenario ID があります。シナリオ画面の「Scenario ID をコピー」、または MCP の `list_scenarios` / `read_scenario` で確認できます。

その ID を、テストの名前に `[pmr:<Scenario ID>]` の形で含めます。**リポジトリに持つのはこれだけ**です。シナリオ名・Gherkin・version をリポジトリへ写す必要はありません (正本は PM on Rails 側にあり、ID から引けます)。

| 言語 / ランナー | 書き方 |
| --- | --- |
| pytest (Python) | テスト名または parametrize の `ids` に `[pmr:<Scenario ID>]` を書くのが推奨。`record_property("pmr_scenario", "<Scenario ID>")` も使えるが、既定の `junit_family=xunit2` では `PytestWarning` が出る (`filterwarnings = error` の CI は落ちる)。使うなら `junit_family=xunit1` を設定する |
| Pest (PHP) | `it('ログインできる [pmr:<Scenario ID>]', ...)` |
| PHPUnit (PHP) | PHPUnit 11 以降なら名前付きデータセット `#[TestWith([], '[pmr:<Scenario ID>]')]` (JUnit の名前が `testLogin with data set "[pmr:...]"` になる)。旧版は data provider の文字列キーに `[pmr:<Scenario ID>]` を付ける |
| Jest / Vitest (TypeScript) | `it("ログインできる [pmr:<Scenario ID>]", ...)` |
| go test (Go) | `t.Run("[pmr:<Scenario ID>]", ...)` のサブテスト名 (gotestsum `--junitfile`) |
| その他 | JUnit XML の `<testcase name="...">` に `[pmr:<Scenario ID>]` が入れば何でも |

テスト名に書けないランナーでは、JUnit XML の `<testcase>` 配下に `<property name="pmr_scenario" value="<Scenario ID>"/>` を出しても同じです。

1 つのテストに複数の Scenario ID を書いても構いません。同じ Scenario に複数テストがある場合は、1件でも失敗すれば不合格、失敗なしで1件以上通れば合格、すべてスキップならスキップとして記録されます。

```xml
<!-- JUnit XML の例。name に ID が入っていれば、それ以外の形式は問いません -->
<testsuite name="Tests\Feature\LoginTest" tests="1">
  <testcase classname="Tests\Feature\LoginTest"
            name="it logs in with valid credentials [pmr:3f2a9c1e-...]" time="0.12"/>
</testsuite>
```

### なぜ Scenario ID だけで足りるのか

PM on Rails のシナリオは **版ごとに別の Scenario ID** を持ちます。受入条件を編集すると新しい版 (新しい ID) が作られ、古い版は「旧版」になります。合意済みの版は書き換えません。

そのため、テストが報告した ID から「どの版を検証したか」が確定します。テスト側にシナリオ名や Gherkin のハッシュを持たせて照合する必要はありません。

## 3. GitHub Actions で JUnit XML をアップロードする

artifact 名は **`pmr-scenario-report` で始めてください。** 中身はテストランナーが出した JUnit XML そのままです。

```yaml
- name: Run tests
  # ランナーの JUnit 出力。pytest: --junitxml / PHPUnit: --log-junit /
  # Jest・Vitest: jest-junit / go: gotestsum --junitfile
  run: vendor/bin/pest --log-junit /tmp/junit.xml

- name: Upload PM on Rails scenario report
  if: always()
  uses: actions/upload-artifact@v4
  with:
    name: pmr-scenario-report
    path: /tmp/junit.xml
    if-no-files-found: ignore
```

unit / integration のようにジョブを分けている場合は、

```text
pmr-scenario-report-unit
pmr-scenario-report-integration
```

のように末尾を変えて構いません。artifact に複数の XML が入っていても、**JUnit 形式** (`testsuites` / `testsuite`) はすべて読みます。`coverage.xml` / `clover.xml` など JUnit でない XML は読み飛ばし、同期履歴にファイル名を残します。壊れた XML が 1 つでもあるとその run は取り込みません。

**artifact 1 つあたり 32 MB まで**です (展開後のレポート合計は 200 MB まで。テストのログ・標準出力が JUnit に入っていても構いません)。分けた artifact のうち **1 つでも上限を超えると、その run は何も記録しません** — 読めた分の合格だけを記録すると、読めなかった側の不合格が消えて「検証済み」に見えてしまうためです。同期履歴に超過した artifact 名が出るので、レポートを小さくする・分けるなどして上限内に収め、再実行 (または再取り込み) してください。

### `if: always()` を付ける理由

テストが失敗したときも artifact をアップロードするためです。

これがないと、CI が落ちたときに「不合格だった」という結果そのものを PM on Rails が受け取れません。

## 4. 取り込まれた結果を確認する

workflow が完了すると、PM on Rails が artifact を取得し、テスト名の Scenario ID で受入シナリオを引きます。

記録された結果は **テスト → シナリオテスト** に表示されます。

- 合格 / 不合格 / スキップ
- 出自「CI自動テスト」
- workflow 名
- GitHub Actions の run へのリンク
- 検証した版と Gherkin (PM on Rails 側のシナリオから埋まります)

取り込み処理の詳細は **設定 → GitHub連携 → 同期履歴** で確認できます。

## 記録できなかったシナリオは PR にコメントされる

取り込みは workflow が終わった後に行うので、**CI そのものは緑のまま**です。旧版の ID を持つテストなど、結果を記録できなかったシナリオがあると、PM on Rails がその run の PR に「CI の結果を記録できなかったシナリオがあります (workflow 名)」というコメントを付けます。表にはテスト名・シナリオ・理由が並びます。

- コメントは **workflow ごとに 1 件**で、新しい実行のたびに書き換わります (新しいコメントは増えません)。同じ実行を取り込み直しただけのときは、コメントを変更しません
- 直してすべて記録されると、同じコメントが「シナリオ別の記録はすべて完了しました」に書き換わります
- PR に紐付かない run (main への push など) や、artifact が無い run にはコメントしません
- 新しい権限は要りません (GitHub App の Issues: Read & write を使います)。コメントに失敗しても記録そのものは行われ、同期履歴に「PR #N へのコメントに失敗した」と残ります

## 記録されるのは「合意済みの版」だけ

TestResult が書かれるのは、その Scenario ID の版が **承認済み / 有効** のときだけです。それ以外は記録されず、理由が同期履歴に残ります。

| 版の状態 | 挙動 |
| --- | --- |
| 承認済み / 有効 | 記録される |
| 下書き / レビュー中 (合意前) | 記録されない。合意してから再実行する |
| 旧版 (編集で新しい版ができた) | 記録されない。履歴と PR のコメントに**現行版の Scenario ID** が出るので、現行版の Then とテストを見直してから ID を貼り替える。新版が下書きに戻されていれば「合意待ち」と出る |
| 却下 / 廃止 / アーカイブ | 記録されない |

合意済みのシナリオを編集すると、旧版は旧版になり、**新しい ID の新版**ができます (新版はそのまま有効です)。テストが旧版の ID を持っている間は、CI の結果は記録されません。

たとえば受入条件を編集して version 4 ができたのに、テストが version 3 の ID を報告している場合、そのテストは古い仕様向けなので現在の検証結果にはしません。テストが version 4 の Then を確かめているか見直し、必要なら直してから ID を貼り替えて、もう一度 CI を実行してください。**ID だけを機械的に置き換えない**でください — 古い Then を確かめたテストが、新しい仕様の合格として記録されてしまいます。

## フォークからの PR は記録されない

フォークリポジトリから送られた PR の workflow は、結果が正しくても記録しません (外部の人が任意のシナリオに「合格」を積めてしまうため)。同じ組織内のブランチから PR を出してください。`head_repository` が取れない run も記録しません。

editor が `POST /api/github/workflow-runs/{run_id}/scenario-report` で再取り込みする場合、このフォーク判定は通りません。フォーク由来の run を再取り込みしないのは呼び出し側の責任です。

## よくあるエラー

| 状態 | 確認すること |
| --- | --- |
| artifact が見つからない | 名前が `pmr-scenario-report` で始まっているか |
| どのシナリオも記録されない | テスト名 (JUnit の `name`) に `[pmr:<Scenario ID>]` が入っているか。XML が JUnit 形式か |
| artifact を取得できない | GitHub App に Actions: Read があるか |
| 「合意前のシナリオには記録しない」 | シナリオを承認したか |
| 「旧版の Scenario ID」 | 現行版の Then に合わせてテストを見直し、履歴 / PR コメントに出た現行版の ID に貼り替えたか |
| 「フォーク由来の run は記録しない」 | フォークではなく同じリポジトリのブランチから PR を出しているか |
| 「取り込み上限 (32 MB) を超えている」 | その artifact を小さくするか分けたか。1 つでも超えると run 全体を記録しない |

理由は **設定 → GitHub連携 → 同期履歴** に残ります。

## CI の合格を、そのまま完了の証拠にする

合否だけの結果 (「1 件合格」) は、**自動テストがどの版に対して通ったか**を記録するだけで、完了ゲートの証拠にはなりません。完了ゲートが見るのは「受入条件の Then を、実際に何を観測して確かめたか」です。

テストがその観測結果を出力すれば、**CI が合格するたびに現行版の証拠が自動で揃います**。人やエージェントが後から TestResult を書き直す必要はありません。シナリオを改版したときも、テストを新しい Then に合わせて ID を貼り替え、CI を通せばそれで済みます。

出力先は JUnit XML の `<testcase>` 配下の `<property name="pmr_observed" value="...">` です。

| 言語 / ランナー | 書き方 |
| --- | --- |
| pytest (Python) | `request.node.user_properties.append(("pmr_observed", f"一覧に予約番号「{number}」が {count} 件表示された"))` (`record_property` も同じ。上の `junit_family` の注意が当てはまる) |
| Playwright (TypeScript) | `test.info().annotations.push({ type: "pmr_observed", description: `...` })`。JUnit reporter に `embedAnnotationsAsProperties: true` を付ける |
| その他 | `<testcase>` の直下に `<properties><property name="pmr_observed" value="..."/></properties>` が出れば何でも |

書き方の決まりは 3 つです。

- **assert が通った後に、その場で観測した値を埋める。** 固定の「通った」「表示された」ではなく、画面に出た文言・件数・状態を入れます
- **Then の言葉で書く。** 完了ゲートは、人が書いた観測と同じ規則でこの文を判定します。Then と関係のない文や「テストが通った」だけの文は、証拠として数えません
- **1 件 1000 文字まで。** ログを貼る場所ではありません

次の場合は、従来どおり合否だけが記録され、完了ゲートの証拠にはなりません。

- テストが `pmr_observed` を出力していない
- 同じシナリオに紐付くテストが 1 件でも不合格だった (合格した側の観測結果も使いません)

画面のスクリーンショットや外部サービスでの確認など、テストから出せない観測が要るときは、これまでどおり人またはエージェントが TestResult を追加で記録します。

詳しくは [テスト結果](https://pmonrails.com/docs/test-results.md) を参照してください。

## CI 由来の結果は書き換えられない

CI から取り込んだ結果 (出自「CI自動テスト」) は、テスト画面や API から出自を変えられません。逆に、手で記録した結果を後から CI 由来に書き換えることもできません (実行者 `github-ci:` は CI 取り込み専用です)。補足のメモなどは通常どおり直せます。

## CIとローカル開発の認証は別

| 用途 | 認証 |
| --- | --- |
| GitHub Actions からテスト結果を記録 | GitHub App。PM on Rails token 不要 |
| Claude Code / ローカル CLI から MCP を使う | `pm-on-rails login` |

**「CI だから PAT が必要」ではありません。**

GitHub Actions 用に、人間のログイントークンを GitHub Secrets へコピーする必要はありません。

---

https://pmonrails.com/docs/ci-test-results
