Skip to content

Commit 30e2b8f

Browse files
authored
feat(ci): cache heavy test artifacts and compare trends
## Summary - cache/restore heavy test artifacts (mutation/property/MBT) with trend comparison and history export - add Slack alerts + automatic critical issue creation - document visualization workflow and helper scripts ## Testing - node scripts/pipelines/render-heavy-trend-summary.mjs --limit 2 - node scripts/pipelines/render-heavy-trend-summary.mjs --limit 1 --json-output reports/heavy-test-trends-history/summary.json
1 parent e292858 commit 30e2b8f

18 files changed

+1423
-12
lines changed

.github/workflows/ci-extended.yml

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ permissions: read-all
1313

1414
jobs:
1515
extended:
16+
permissions:
17+
contents: read
18+
issues: write
19+
env:
20+
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
1621
runs-on: ubuntu-latest
1722
if: ${{ github.event_name != 'pull_request' || (github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork) }}
1823
steps:
@@ -100,6 +105,44 @@ jobs:
100105
if: ${{ steps.flags.outputs.should_run == 'true' }}
101106
run: pnpm install --frozen-lockfile || pnpm install --no-frozen-lockfile
102107

108+
- name: Determine heavy test cache key
109+
if: ${{ steps.flags.outputs.should_run == 'true' }}
110+
id: heavy-cache-key
111+
shell: bash
112+
run: |
113+
set -euo pipefail
114+
prefix="ci-heavy-${{ runner.os }}"
115+
if [ "${{ github.event_name }}" = "schedule" ]; then
116+
key="${prefix}-schedule"
117+
restore_keys="${prefix}-schedule"$'\n'"${prefix}-"
118+
else
119+
key="${prefix}-${GITHUB_SHA}"
120+
restore_keys="${prefix}-"
121+
fi
122+
{
123+
echo "key=${key}"
124+
echo "restore_keys<<__RESTORE__"
125+
printf '%s\n' "${restore_keys}"
126+
echo "__RESTORE__"
127+
} >> "$GITHUB_OUTPUT"
128+
129+
- name: Restore heavy test cache
130+
if: ${{ steps.flags.outputs.should_run == 'true' }}
131+
id: restore-heavy
132+
uses: actions/cache/restore@v4
133+
with:
134+
path: .cache/test-results
135+
key: ${{ steps.heavy-cache-key.outputs.key }}
136+
restore-keys: ${{ steps.heavy-cache-key.outputs.restore_keys }}
137+
138+
- name: Rehydrate cached test artifacts
139+
if: ${{ steps.flags.outputs.should_run == 'true' && steps.restore-heavy.outputs.cache-hit == 'true' }}
140+
run: node scripts/pipelines/sync-test-results.mjs --restore
141+
142+
- name: Snapshot heavy test baseline
143+
if: ${{ steps.flags.outputs.should_run == 'true' }}
144+
run: node scripts/pipelines/sync-test-results.mjs --snapshot
145+
103146
- name: Run integration tests
104147
if: ${{ steps.flags.outputs.run_integration == 'true' }}
105148
run: pnpm run test:int
@@ -247,3 +290,128 @@ jobs:
247290
echo "- run_mbt: ${RUN_MBT}"
248291
echo "- run_mutation: ${RUN_MUTATION}"
249292
} >> "$GITHUB_STEP_SUMMARY"
293+
294+
- name: Compare heavy test trends
295+
if: ${{ always() && steps.flags.outputs.should_run == 'true' }}
296+
run: node scripts/pipelines/compare-test-trends.mjs
297+
298+
- name: Archive heavy test trend history
299+
if: ${{ always() && steps.flags.outputs.should_run == 'true' && github.event_name == 'schedule' }}
300+
shell: bash
301+
run: |
302+
set -euo pipefail
303+
if [ ! -f reports/heavy-test-trends.json ]; then
304+
echo "Trend report not generated; skipping archive."
305+
exit 0
306+
fi
307+
mkdir -p reports/heavy-test-trends-history
308+
timestamp="$(date -u +'%Y-%m-%dT%H-%M-%SZ')"
309+
cp reports/heavy-test-trends.json "reports/heavy-test-trends-history/${timestamp}.json"
310+
printf 'Archived heavy test trend snapshot: %s\n' "${timestamp}"
311+
312+
- name: Render heavy test trend summary
313+
if: ${{ always() && steps.flags.outputs.should_run == 'true' && github.event_name == 'schedule' }}
314+
id: heavy-summary
315+
shell: bash
316+
run: |
317+
set -euo pipefail
318+
node scripts/pipelines/render-heavy-trend-summary.mjs \
319+
--limit 5 \
320+
--json-output reports/heavy-test-trends-history/summary.json \
321+
--warn-mutation-score 98 \
322+
--critical-mutation-score 96 \
323+
--warn-mutation-delta -1.0 \
324+
--critical-mutation-delta -2.5 \
325+
--warn-property-failed 1 \
326+
--critical-property-failed 3 \
327+
--warn-property-failure-rate 0.1 \
328+
--warn-mbt-violations 1 \
329+
--critical-mbt-violations 3
330+
severity=$(jq -r '.highestSeverity' reports/heavy-test-trends-history/summary.json)
331+
echo "severity=${severity}" >> "$GITHUB_OUTPUT"
332+
333+
- name: Upload heavy test trend report
334+
if: ${{ always() && steps.flags.outputs.should_run == 'true' }}
335+
uses: actions/upload-artifact@v4
336+
with:
337+
name: heavy-test-trends
338+
path: reports/heavy-test-trends.json
339+
if-no-files-found: ignore
340+
retention-days: 14
341+
342+
- name: Upload heavy test trend history
343+
if: ${{ always() && steps.flags.outputs.should_run == 'true' && github.event_name == 'schedule' }}
344+
uses: actions/upload-artifact@v4
345+
with:
346+
name: heavy-test-trends-history
347+
path: reports/heavy-test-trends-history
348+
if-no-files-found: ignore
349+
retention-days: 30
350+
351+
- name: Notify Slack (heavy trend alert)
352+
if: ${{ always() && steps.flags.outputs.should_run == 'true' && github.event_name == 'schedule' && steps.heavy-summary.outputs.severity != 'ok' && env.SLACK_WEBHOOK_URL != '' }}
353+
uses: rtCamp/action-slack-notify@v2
354+
env:
355+
SLACK_WEBHOOK: ${{ env.SLACK_WEBHOOK_URL }}
356+
SLACK_COLOR: ${{ steps.heavy-summary.outputs.severity == 'critical' && '#E01E5A' || '#FFC107' }}
357+
SLACK_MESSAGE: |
358+
${{ steps.heavy-summary.outputs.severity == 'critical' && ':rotating_light:' || ':warning:' }} Heavy test trend ${{ steps.heavy-summary.outputs.severity }} detected in ${{ github.workflow }}
359+
• run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
360+
• summary: heavy-test-trends-history/summary.md
361+
362+
- name: Create heavy trend issue
363+
if: ${{ always() && steps.flags.outputs.should_run == 'true' && github.event_name == 'schedule' && steps.heavy-summary.outputs.severity == 'critical' }}
364+
uses: actions/github-script@v7
365+
with:
366+
github-token: ${{ secrets.GITHUB_TOKEN }}
367+
script: |
368+
const fs = require('fs');
369+
const path = require('path');
370+
const summaryPath = path.resolve('reports/heavy-test-trends-history/summary.json');
371+
const summary = JSON.parse(fs.readFileSync(summaryPath, 'utf8'));
372+
const snapshots = Array.isArray(summary.snapshots) ? summary.snapshots : [];
373+
const criticalSnapshot = snapshots.find(s => (s.severity || '').toLowerCase() === 'critical');
374+
if (!criticalSnapshot) {
375+
core.info('No critical snapshot detected; skipping issue creation.');
376+
return;
377+
}
378+
const criticalEntries = (criticalSnapshot.entries || []).filter(e => (e.severity || '').toLowerCase() === 'critical');
379+
const detailLines = criticalEntries.map(entry => {
380+
const reasons = (entry.reasons || []).join('; ') || 'threshold exceeded';
381+
return `- **${entry.label}**: ${reasons}`;
382+
}).join('\n');
383+
const title = `[CI Extended] Heavy test critical alert - ${criticalSnapshot.label}`;
384+
const body = [
385+
'## Alert',
386+
`- Workflow: ${context.workflow} (run: ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`,
387+
'- Severity: critical',
388+
`- Snapshot: ${criticalSnapshot.label}`,
389+
'- Summary: reports/heavy-test-trends-history/summary.md',
390+
'- JSON: reports/heavy-test-trends-history/summary.json',
391+
'',
392+
'### Details',
393+
detailLines || '- (no critical entry details recorded)',
394+
'',
395+
'## Next Steps',
396+
'- [ ] Download artifacts and inspect mutation/property/MBT outputs',
397+
'- [ ] Update issue with root cause and resolution plan',
398+
].join('\n');
399+
await github.rest.issues.create({
400+
owner: context.repo.owner,
401+
repo: context.repo.repo,
402+
title,
403+
body,
404+
labels: ['flaky-test', 'ci-stability', 'needs-investigation'],
405+
});
406+
core.info(`Issue created for snapshot ${criticalSnapshot.label}`);
407+
408+
- name: Stage test results for caching
409+
if: ${{ always() && steps.flags.outputs.should_run == 'true' }}
410+
run: node scripts/pipelines/sync-test-results.mjs --store
411+
412+
- name: Save heavy test cache
413+
if: ${{ always() && steps.flags.outputs.should_run == 'true' && steps.restore-heavy.outputs.cache-hit != 'true' }}
414+
uses: actions/cache/save@v4
415+
with:
416+
path: .cache/test-results
417+
key: ${{ steps.heavy-cache-key.outputs.key }}

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ cegis-report-*.json
5555
*-report-*.json
5656
conformance-results.json
5757
reports/conformance/
58+
reports/heavy-test-trends.json
59+
reports/heavy-test-trends-history/
5860
sample-*.json
5961
invalid-sample-*.json
6062
clean-sample-*.json

docs/ci-policy.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ This document defines CI policies to keep PR experience fast and stable while ma
2929
- `run-property`: execute property harness smoke within CI Extended
3030
- `run-mbt`: execute MBT smoke (`test:mbt:ci`) within CI Extended
3131
- `run-mutation`: execute mutation auto diff (extended pipeline)
32+
33+
CI Extended restores cached heavy test artifacts (`.cache/test-results`) when rerunning; the cache is refreshed at the end of each run via `node scripts/pipelines/sync-test-results.mjs --store`. Check or warm the cache locally with `--status` / `--restore` before dispatching reruns. Nightly runs use a stable cache key (`ci-heavy-${ runner.os }-schedule`) so the previous baseline is rehydrated before execution, call `node scripts/pipelines/compare-test-trends.mjs` to produce a Markdown diff (posted to the Step Summary), and persist both `reports/heavy-test-trends.json` and `reports/heavy-test-trends-history/<timestamp>.json` as artifacts (`heavy-test-trends`, `heavy-test-trends-history`).
3234
- `qa --light`: run QA in light mode (vitest -> `test:fast`); used in `ae-ci`
3335
- `ae-benchmark run --ci --light --dry-run`: benchmark config validation only in PRs (fast & stable)
3436
- `run-qa`: run `ae-ci` workflow’s `qa-bench` on PRs (default off)
@@ -140,6 +142,8 @@ This document defines CI policies to keep PR experience fast and stable while ma
140142
- `run-property`: CI Extended の property harness のみを実行
141143
- `run-mbt`: CI Extended の `test:mbt:ci` のみを実行
142144
- `run-mutation`: CI Extended の mutation auto diff のみを実行
145+
146+
CI Extended 実行後は heavy テスト成果物を `.cache/test-results` に保存し、再実行時に自動復元します。必要に応じて `node scripts/pipelines/sync-test-results.mjs --status` / `--restore` でキャッシュの状態を確認・展開してから再実行できます。差分の確認は `node scripts/pipelines/compare-test-trends.mjs` を実行すると Markdown と JSON で出力され、Step Summary にも自動追記されます。
143147
- `qa --light`: QA を軽量実行(vitest は `test:fast` 実行)。`ae-ci` の QA ステップに適用済み
144148
- `ae-benchmark run --ci --light --dry-run`: ベンチは PR では構成検証のみに留め、時間・安定性を優先
145149
- `run-qa`: `ae-ci` ワークフローの `qa-bench` を PR で実行(既定は非実行)
@@ -166,6 +170,8 @@ This document defines CI policies to keep PR experience fast and stable while ma
166170
### test:ci(ライト / 拡張)
167171
- `test:ci:lite`: Verify Lite のローカル実行口。types:check / lint / build / conformance report をまとめて実行し、PR ブロッキングの最小セットを再現。
168172
- `test:ci:extended`: Integration(`test:int`)/ property harness / `test:mbt:ci` / `pipelines:pact` を連続実行し、最後に `pipelines:mutation:quick` で mutation quick を叩くローカル向け統合スイート。
173+
- Heavy test artifacts for the extended suite are cached under `.cache/test-results`; run `node scripts/pipelines/sync-test-results.mjs --restore` before reruns to reuse survivors, MBT summaries, and property harness outputs, then `--store` after local runs to refresh the cache.
174+
- 拡張スイートで生成される成果物は `.cache/test-results` にキャッシュされるため、再実行前に `node scripts/pipelines/sync-test-results.mjs --restore` を実行すると mutation survivors / MBT summary / property summary を再利用できます(ローカル実行後は `--store` で更新)。
169175
- `.github/workflows/ci-extended.yml`: `run-ci-extended` で上記一式を PR から opt-in。`run-integration` / `run-property` / `run-mbt` / `run-mutation` で部分実行を選択でき、main push / schedule では常時稼働。
170176
- Vitest ベースの安定プロファイルは従来通り `test:ci:stable`(Docker/Podman smoke イメージで利用)として提供。
171177

docs/ci/heavy-test-album.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Heavy Test Trend Visualization PoC
2+
3+
heavy-test トレンドの履歴 (`reports/heavy-test-trends-history/*.json`) を整形して可視化するためのアイデアをまとめます。
4+
5+
## CSV / Markdown 生成スクリプト
6+
```bash
7+
pnpm node scripts/pipelines/export-heavy-trend-history.mjs --history-dir reports/heavy-test-trends-history --csv-output reports/heavy-test-trends-history/history.csv --markdown-output reports/heavy-test-trends-history/history.md --markdown-limit 20
8+
```
9+
- `history.csv`: 全スナップショットの `snapshot,label,metric,baseline,current,delta` を含む。Observable や Excel での分析に利用。
10+
- `history.md`: 直近 N 件を Markdown テーブルで出力し、PR やドキュメントに貼り付け可能。
11+
12+
## Markdown プレビュー例
13+
| Snapshot | Label | Metric | Baseline | Current | Δ |
14+
| --- | --- | --- | --- | --- | --- |
15+
| ... | ... | ... | ... | ... | ... |
16+
17+
## Observable Notebook での活用例
18+
1. `history.csv``FileAttachment` として Notebook にアップロード。
19+
2. 以下のコードで CSV を読み込み、Mutation score のトレンドを描画。
20+
```js
21+
viewof metric = Inputs.select([...new Set(data.map(d => d.metric))], {value: "mutationScore"})
22+
filtered = data.filter(d => d.metric === metric && d.label === "Mutation quick")
23+
Plot.plot({
24+
marginLeft: 80,
25+
x: {label: "Snapshot", tickRotate: -45},
26+
y: {label: "Current"},
27+
marks: [
28+
Plot.ruleY([98], {stroke: "orange", strokeDash: "4,2"}),
29+
Plot.ruleY([96], {stroke: "red", strokeDash: "4,2"}),
30+
Plot.line(filtered, {x: "snapshot", y: "current", stroke: "steelblue", marker: true})
31+
]
32+
})
33+
```
34+
3. `delta` をヒートマップ表示する場合は `Plot.rectY` を利用し、critical しきい値を別色で塗り分ける。
35+
36+
## 今後のステップ
37+
- Slack 通知/Issue 起票で共有された snapshot ラベルから直接 Notebook の該当位置へリンクさせる。
38+
- Grafana 等の BI ツールへ `history.csv` を取り込み、ダッシュボード化する。
39+
- delta が連続で悪化した場合の自動コメントなど、可視化結果とアラートを結びつける。

docs/ci/heavy-test-alerts.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Heavy Test Trend Alerting Plan
2+
3+
## 対象メトリクス
4+
- **Mutation quick**
5+
- `mutationScore` の絶対値と直近比較 (`Δ`).
6+
- レポート件数(`survived`, `timedOut`, `ignored`)の急増を補足指標とする。
7+
- **Property harness**
8+
- `failed` 件数、`runs` に対する失敗率。
9+
- `traceId` 単位での連続失敗を検知対象にする。
10+
- **MBT harness**
11+
- `violations` 件数、および `runs` / `depth` の極端な変化。
12+
13+
`scripts/pipelines/render-heavy-trend-summary.mjs```--warn-*`` / ``--critical-*`` オプションを指定し、`summary.md` / `summary.json` から自動判定できるようになりました。
14+
15+
## 初期閾値案
16+
| メトリクス | Warning | Critical | 備考 |
17+
|------------|---------|----------|------|
18+
| Mutation score | `current < 98` または `Δ <= -1.0` | `current < 96` または `Δ <= -2.5` | Δ は baseline との差。Warning で Slack 通知、Critical で Issue 起票を検討。|
19+
| Property failed count | `failed >= 1` | `failed >= 3` | 失敗率が 10% を超えた場合も Warning。|
20+
| MBT violations | `violations >= 1` | `violations >= 3` | violations が 0 でない場合は詳細ログ確認を必須にする。|
21+
22+
## 通知フロー案
23+
1. `render-heavy-trend-summary.mjs` に閾値判定オプションを追加し、Markdown 出力内に :warning:/:rotating_light: を埋め込む。
24+
2. Warning 以上の項目が存在する場合は Slack Webhook(`ci-extended.yml` スケジュール実行に追加済み)でメッセージ送信。
25+
3. Critical 判定時は GitHub Issue(`flaky-test` ラベル)を自動作成し、関連ログ/アーティファクトへのリンクを添付。
26+
4. PR 上で手動 rerun を行う際も同スクリプトを実行し、Step Summary に判定結果を表示する。
27+
28+
### Critical 判定時の Issue 起票案
29+
- 作成先: `itdojp/ae-framework` / labels: `flaky-test`, `ci-stability`, `needs-investigation`
30+
- タイトル例: `[CI Extended] Heavy test critical alert - mutation score < 96`
31+
- 本文テンプレート:
32+
```md
33+
## Alert
34+
- Workflow: ${{ github.workflow }} (run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
35+
- Severity: critical
36+
- Snapshot: <timestamp>
37+
- Summary: heavy-test-trends-history/summary.md
38+
- JSON: heavy-test-trends-history/summary.json
39+
40+
## Next Steps
41+
- [ ] Download artifacts and inspect mutation/property/MBT outputs
42+
- [ ] Update issue with root cause and resolution plan
43+
```
44+
- 実装案: `ci-extended.yml``severity == 'critical'` の場合に `gh issue create` を呼び出す(`GITHUB_TOKEN` の権限を要確認)。
45+
46+
1. `render-heavy-trend-summary.mjs` に閾値判定オプションを追加し、Markdown 出力内に :warning:/:rotating_light: を埋め込む。
47+
2. Warning 以上の項目が存在する場合は Slack Webhook(`nightly-monitoring` 既存通知を再利用)でメッセージ送信。
48+
3. Critical 判定時は GitHub Issue(`flaky-test` ラベル)を自動作成し、関連ログ/アーティファクトへのリンクを添付。
49+
4. PR 上で手動 rerun を行う際も同スクリプトを実行し、Step Summary に判定結果を表示する。
50+
51+
## 実装ステップ
52+
1. `render-heavy-trend-summary.mjs` を拡張し、`--warn-mutation-score`, `--critical-mutation-score` 等の CLI オプションで閾値を受け取り、Markdown 内にバッジを表示する。
53+
2. CLI から JSON 形式の判定結果を吐き出す (`--json-output`)、Slack ワークフローで利用できるようにする。
54+
3. `ci-extended` のスケジュール実行後に判定スクリプトを実行し、Warning 以上の場合は Slack 通知ステップを追加する。
55+
4. Critical の場合は `gh issue create` を用いた自動起票か、既存 `nightly-monitoring` に統合する。
56+
57+
## 運用上の注意
58+
- 閾値は初期案。実データに基づき 2〜3 週間運用した後に見直す。
59+
- false positive を避けるため、`Δ` 判定は 2 回連続で閾値を下回った場合にエスカレーションするモードも検討する。
60+
- Slack 通知は深夜帯(JST)に偏るため、通知チャンネルのサイレンス設定を確認する。
61+
- Issue 起票時には関連する `heavy-test-trends-history/<timestamp>.json``summary.md`、該当 run の URL を必ず添付する。
62+
63+
## TODO
64+
- [x] `render-heavy-trend-summary.mjs` への閾値オプション追加
65+
- [x] Slack Webhook 通知ステップの実装(`ci-extended.yml` スケジュール実行に追加済み)
66+
- [x] 自動 Issue 起票フローの設計(Critical 判定時)
67+
- [ ] 閾値リファインのためのメトリクス実測データ収集

0 commit comments

Comments
 (0)