Lighthouse CIをGitHubActionsで動かして、Laravelに自動保存してみた1
Webサイトのパフォーマンスを定点観測したくて、Lighthouse CIをGitHub Actionsで回す仕組みを作ってみました。
前回は「GitHub ActionsでLighthouse CIを動かす」ところまででしたが、今回はその続きで「取得したスコアをLaravelにPOSTして保存する」までやってみたメモです。
細かい設計は手探りでしたが、最小構成で毎月のスコアが自動的にDBに溜まっていくようになったので紹介します。
1. やりたいこと
- 毎月サイトを自動で計測して、スコア履歴を残したい
- desktop / mobile で分けて保存したい
- URLごとのrun結果は平均化して記録したい
- Laravel側にAPIを用意して、GitHub Actionsから直接叩く
2. 流れ
- GitHubActionsを実行
- GitHubActionsからLightHouseCIを実行して結果をjsonに保存
- 保存したjsonをPythonが読んで、LaravelのAPIにPOST
- LaravelがDBに保存
- 完了
今回はまずLaravelにPOSTするまでを書いてみます。
3. jsonデータ設計
まずはLaravelに送るペイロード(json送信内容)を決めました。
月単位でユニークにしたかったので、site_id + month(YYYY-MM-01) + preset + url_hash をキーにしています。
{
"site_id": 1,
"month": "2025-09-01",
"preset": "mobile",
"url": "https://example.com/",
"url_hash": "sha256(url)",
"performance": 0.91,
"accessibility": 0.97,
"seo": 0.98,
"best_practices": 0.92,
"raw_json": { "runs": 3, "site_name": "example-com", "domain": "example.com" }
}スコアは 0.0〜1.0 で、raw_jsonにメタ情報や平均値を入れる方針です。
このデータはGitHubActions内で作成します。
4. GitHubActionsの設定
次にGitHubActionsの設定をします。GitHubActionsでは全体の処理のコントロールを行います。
コードはこんな感じ。
name: Lighthouse Monthly Report CI to API
on:
workflow_dispatch:
# schedule:
# - cron: "0 18 1 * *" # UTC 18:00 = JST 03:00(毎月2日03:00相当)
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.read.outputs.matrix }}
steps:
- uses: actions/checkout@v4
- id: read
run: |
echo "matrix=$(jq -c . ci/config/lighthouse_sites.json)" >> "$GITHUB_OUTPUT"
run-lhci:
needs: prepare
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
matrix: ${{ fromJSON(needs.prepare.outputs.matrix) }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install Lighthouse CI
run: npm i -g @lhci/cli@0.13.0
- name: Run LHCI for each preset
env:
SITE_NAME: ${{ matrix.name }}
URLS_JSON: ${{ toJSON(matrix.urls) }}
PRESETS_JSON: ${{ toJSON(matrix.presets) }}
RUNS: ${{ matrix.numberOfRuns }}
run: |
set -euo pipefail
mkdir -p "lhci_reports/${SITE_NAME}"
for PRESET in $(echo "$PRESETS_JSON" | jq -r '.[]'); do
OUT="lhci_reports/${SITE_NAME}/${PRESET}"
mkdir -p "$OUT"
if [ "$PRESET" = "desktop" ]; then
# デスクトップは preset=desktop を使う
jq -n \
--argjson urls "$URLS_JSON" \
--argjson runs "$RUNS" \
--arg outdir "$OUT" \
'{
ci:{
collect:{
url:$urls,
numberOfRuns:$runs,
settings:{ preset:"desktop" }
},
assert:{
assertions:{
"categories:performance":["warn",{minScore:0.8}],
"categories:accessibility":["warn",{minScore:0.9}],
"categories:seo":["warn",{minScore:0.9}]
}
},
upload:{target:"filesystem",outputDir:$outdir}
}
}' > .lighthouserc.effective.json
else
# モバイル:presetは使わず、formFactor指定(または設定自体を省略でも可)
jq -n \
--argjson urls "$URLS_JSON" \
--argjson runs "$RUNS" \
--arg outdir "$OUT" \
'{
ci:{
collect:{
url:$urls,
numberOfRuns:$runs,
settings:{
formFactor:"mobile",
screenEmulation:{mobile:true}
}
},
assert:{
assertions:{
"categories:performance":["warn",{minScore:0.8}],
"categories:accessibility":["warn",{minScore:0.9}],
"categories:seo":["warn",{minScore:0.9}]
}
},
upload:{target:"filesystem",outputDir:$outdir}
}
}' > .lighthouserc.effective.json
fi
lhci autorun --config=.lighthouserc.effective.json || true
done
# ここからPythonで集計→LaravelへPOST
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install Python deps
run: |
python -m pip install --upgrade pip
pip install requests
- name: Post Lighthouse summary via Python
env:
API_BASE: ${{ secrets.APP_API_BASE }} # 例: https://example.com/api
JOBS_TOKEN: ${{ secrets.JOBS_TOKEN }} # トークン
DOMAIN: ${{ matrix.domain }}
SITE_NAME: ${{ matrix.name }}
SITE_ID: ${{ matrix.site_id }}
MONTH: ${{ inputs.month || '' }}
run: |
python ci/scripts/lighthouse_report_to_api.py
- name: Upload raw reports (artifact)
if: always()
uses: actions/upload-artifact@v4
with:
name: lighthouse-reports-${{ matrix.name }}-${{ github.run_number }}
path: lhci_reports
retention-days: 30
if-no-files-found: warn
上記の大まかな流れは以下のとおりです。
- 計測したいサイト情報が載っている
ci/config/lighthouse_sites.jsonを読み、マトリクス化 run-lhciでlhci autorunを preset(desktop/mobile)ごとに回し、Pythonで集計- 集計したデータをPythonでjsonに保存
- jsonをPythonで読み込んでLaravelにPOST
lighthouse_sites.jsonはこんな感じ。
{
"include": [
{
"name": "hogehoge1",
"domain": "hogehoge1.com",
"site_id": 1,
"urls": [
"https://hogehoge1.com",
"https://hogehoge1.com/page1",
"https://hogehoge1.com/page2"
],
"presets": ["desktop", "mobile"],
"numberOfRuns": 3
},
{
"name": "hogehoge2",
"domain": "hogehoge2.com",
"site_id": 2,
"urls": [
"https://hogehoge2.com",
"https://hogehoge2.com/page1",
"https://hogehoge2.com/page2"
],
"presets": ["desktop", "mobile"],
"numberOfRuns": 3
}
]
}
2サイト、それぞれ3ページをPCとモバイルの両方計測する設定です。
ちなみに「numberOfRuns」はLighthouse CI で同じURLを何回測定するか の回数指定です。
5. GitHub Actionsの環境変数
このワークフローでは Secretsから受け取る値を環境変数としてPythonに渡しています。
値はリポジトリの「Settings > Secrets and variables > Actions」に登録しておきます。
APP_API_BASE:Laravel APIのベースURL。
例:https://example.com/apiJOBS_TOKEN:Laravel側で検証するトークン。
APIヘッダX-Job-Tokenとして送られます。
※ これらは公開リポジトリでも漏れないように必ずSecrets管理します。
6. Pythonで集計してPOST
上記のjsonデータをPythonで読み込んでLaravelのAPIにPOSTします。
GitHubActionsから直接POSTしてもいいのですが、複数runの平均化 や raw_jsonの整理など、ちょっと大変だったので、間にPythonを挟みました。
LHCIの出力は「lhci_reports/{SITE_NAME}/{PRESET}/*.report.json」に貯め、Python側でURL別に集約して送ります。
コードはこんな感じ。
#!/usr/bin/env python3
# LighthouseCIの *.lighthouse.json を集計してLaravel APIに送る
import os, sys, json, glob, hashlib, statistics, requests
from datetime import datetime
API_BASE = os.getenv("API_BASE", "").rstrip("/")
JOBS_TOKEN = os.getenv("JOBS_TOKEN", "")
SITE_ID = int(os.getenv("SITE_ID", "0"))
SITE_NAME = os.getenv("SITE_NAME", "").strip()
DOMAIN = os.getenv("DOMAIN", "").strip()
MONTH_STR = os.getenv("MONTH", "").strip() or datetime.now().strftime("%Y-%m")
MONTH_YMD = f"{MONTH_STR}-01"
def normalize_url(u: str) -> str:
return u.strip().rstrip("/").lower()
def sha256(s: str) -> str:
return hashlib.sha256(s.encode("utf-8")).hexdigest()
def avg(values):
vals = [v for v in values if isinstance(v, (int, float))]
return round(statistics.fmean(vals), 4) if vals else None
headers = {"X-Job-Token": JOBS_TOKEN, "Content-Type": "application/json"}
for preset in ("mobile","desktop"):
files = glob.glob(f"lhci_reports/{SITE_NAME}/{preset}/*.report.json")
if not files: continue
per_url = {}
for fpath in files:
rep = json.load(open(fpath))
url = normalize_url(rep.get("requestedUrl") or rep.get"(finalUrl"))
cats = rep.get("categories",{})
per_url.setdefault(url,{"performance":[],"accessibility":[],"seo":[],"best_practices":[]})
for k in per_url[url].keys():
v = cats.get(k.replace("_","-"),{}).get("score")
if isinstance(v,(int,float)): per_url[url][k].append(v)
for url, scores in per_url.items():
payload = {
"site_id": SITE_ID,
"month": MONTH_YMD,
"preset": preset,
"url": url,
"url_hash": sha256(url),
"performance": avg(scores["performance"]) or 0.0,
"accessibility": avg(scores["accessibility"]) or 0.0,
"seo": avg(scores["seo"]) or 0.0,
"best_practices": avg(scores["best_practices"]) or 0.0,
"raw_json": {
"site_name": SITE_NAME, "domain": DOMAIN,
"preset": preset, "month": MONTH_STR,
"runs": len(scores["performance"]),
}
}
r = requests.post(f"{API_BASE}/lighthouse", headers=headers, json=payload, timeout=60)
print(r.status_code, url, preset)
とりあえずここまで。次回はPOSTしたデータを受けるLaravel側を書いてみたいと思います。
今回は以上です!