Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 191 additions & 0 deletions .github/workflows/measurement-validation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
name: Measurement Validation

on:
push:
branches: ["main", "release/*"]
paths:
- "src/**"
- "scripts/**"
- ".measurement-baseline.json"
pull_request:
branches: ["main"]
paths:
- "src/**"
- "scripts/**"
- ".measurement-baseline.json"
schedule:
# Daily canary run at 06:00 UTC
- cron: "0 6 * * *"
workflow_dispatch:
inputs:
update_baseline:
description: "Update the performance baseline after this run"
required: false
default: "false"
slack_notify:
description: "Send Slack notification on completion"
required: false
default: "true"

permissions:
contents: read

env:
BUN_VERSION: "1.x"
BASELINE_PATH: ".measurement-baseline.json"

jobs:
validate:
name: Measurement Validation
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: write
pull-requests: write

steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: ${{ env.BUN_VERSION }}

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Type-check
run: bun run check

- name: Run unit tests
run: bun test src/measurement-validator/

- name: Run performance tracking check
id: perf
run: |
bun run scripts/performance-trends.ts \
--baseline=${{ env.BASELINE_PATH }} \
--output=.perf-report.json \
--format=json
continue-on-error: true

- name: Upload performance report
if: always()
uses: actions/upload-artifact@v4
with:
name: performance-report-${{ github.run_id }}
path: .perf-report.json
if-no-files-found: ignore
retention-days: 90

- name: Post PR comment
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
let report = '## 📊 Measurement Validation Report\n\n';

try {
const raw = fs.readFileSync('.perf-report.json', 'utf8');
const data = JSON.parse(raw);

if (data.regressions && data.regressions.length > 0) {
const critical = data.regressions.filter(r => r.severity === 'critical');
const warnings = data.regressions.filter(r => r.severity === 'warning');
if (critical.length > 0) {
report += '### 🔴 Critical Regressions\n';
critical.forEach(r => {
report += `- **${r.language}** (${r.metric}): ${r.baselineMs.toFixed(2)}ms → ${r.currentMs.toFixed(2)}ms (+${r.changePercent.toFixed(1)}%)\n`;
});
report += '\n';
}
if (warnings.length > 0) {
report += '### 🟡 Warnings\n';
warnings.forEach(r => {
report += `- **${r.language}** (${r.metric}): ${r.baselineMs.toFixed(2)}ms → ${r.currentMs.toFixed(2)}ms (+${r.changePercent.toFixed(1)}%)\n`;
});
report += '\n';
}
} else {
report += '✅ No performance regressions detected.\n\n';
}

if (data.metrics) {
report += '### Current Metrics\n';
report += '| Language | Avg Total | p95 Total | Samples |\n';
report += '|----------|-----------|-----------|----------|\n';
for (const [key, m] of Object.entries(data.metrics)) {
const [lang] = key.split('::');
report += `| ${lang} | ${m.avgTotalMs.toFixed(2)}ms | ${m.p95TotalMs.toFixed(2)}ms | ${m.sampleCount} |\n`;
}
}
} catch (e) {
report += `> Performance report not available: ${e.message}\n`;
}

report += `\n---\n*Run ID: \`${context.runId}\` · Commit: \`${context.sha.slice(0, 8)}\`*`;

// Find or create comment
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const existing = comments.find(c =>
c.user.login === 'github-actions[bot]' &&
c.body.includes('Measurement Validation Report')
);

if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body: report,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: report,
});
}

- name: Fail on critical regressions
if: steps.perf.outcome == 'failure'
run: |
echo "::error::Performance regressions detected. See the performance report artifact for details."
exit 1

- name: Update baseline (manual trigger only)
if: |
github.event_name == 'workflow_dispatch' &&
github.event.inputs.update_baseline == 'true' &&
steps.perf.outcome != 'failure'
run: |
bun run scripts/performance-trends.ts \
--update-baseline \
--baseline=${{ env.BASELINE_PATH }}
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add ${{ env.BASELINE_PATH }}
git commit -m "chore: update measurement performance baseline [skip ci]" || true
git push

- name: Send Slack notification
if: |
(github.event_name == 'schedule' || github.event.inputs.slack_notify == 'true') &&
env.SLACK_WEBHOOK_URL != ''
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
run: |
bun run scripts/performance-trends.ts \
--slack-notify \
--baseline=${{ env.BASELINE_PATH }} \
--branch="${{ github.ref_name }}" \
--commit="${{ github.sha }}"
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,7 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json

# Finder (MacOS) folder config
.DS_Store

# measurement-validator runtime artifacts (do not commit)
.measurement-results.db
.perf-report.json
58 changes: 58 additions & 0 deletions .measurement-baseline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"version": "1",
"createdAt": 1743849600000,
"updatedAt": 1743849600000,
"commitSha": null,
"metrics": {
"english::16px Inter, sans-serif": {
"avgPrepareMs": 0.85,
"avgLayoutMs": 0.02,
"avgTotalMs": 0.87,
"p95PrepareMs": 1.20,
"p95LayoutMs": 0.04,
"p95TotalMs": 1.24,
"sampleCount": 100,
"capturedAt": 1743849600000
},
"arabic::16px Inter, sans-serif": {
"avgPrepareMs": 1.10,
"avgLayoutMs": 0.02,
"avgTotalMs": 1.12,
"p95PrepareMs": 1.60,
"p95LayoutMs": 0.04,
"p95TotalMs": 1.64,
"sampleCount": 100,
"capturedAt": 1743849600000
},
"chinese::16px Inter, sans-serif": {
"avgPrepareMs": 0.95,
"avgLayoutMs": 0.02,
"avgTotalMs": 0.97,
"p95PrepareMs": 1.35,
"p95LayoutMs": 0.04,
"p95TotalMs": 1.39,
"sampleCount": 100,
"capturedAt": 1743849600000
},
"japanese::16px Inter, sans-serif": {
"avgPrepareMs": 0.90,
"avgLayoutMs": 0.02,
"avgTotalMs": 0.92,
"p95PrepareMs": 1.30,
"p95LayoutMs": 0.04,
"p95TotalMs": 1.34,
"sampleCount": 100,
"capturedAt": 1743849600000
},
"thai::16px Inter, sans-serif": {
"avgPrepareMs": 1.05,
"avgLayoutMs": 0.02,
"avgTotalMs": 1.07,
"p95PrepareMs": 1.50,
"p95LayoutMs": 0.04,
"p95TotalMs": 1.54,
"sampleCount": 100,
"capturedAt": 1743849600000
}
}
}
Loading