Merge pull request #45: enforce reproducible quality gates
All checks were successful
Quality gates / quality (push) Successful in 1m18s
All checks were successful
Quality gates / quality (push) Successful in 1m18s
This commit is contained in:
commit
8ab1e85bd1
56
.gitea/workflows/quality.yml
Normal file
56
.gitea/workflows/quality.yml
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
name: Quality gates
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
quality:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
steps:
|
||||||
|
- name: Check out source
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: npm
|
||||||
|
- name: Install reproducibly
|
||||||
|
run: |
|
||||||
|
npm ci
|
||||||
|
python3 -m pip install --break-system-packages -r requirements-test.txt
|
||||||
|
- name: Install browser
|
||||||
|
run: npx playwright install --with-deps chromium
|
||||||
|
- name: Unit and security tests
|
||||||
|
run: npm test
|
||||||
|
- name: Mobile browser acceptance
|
||||||
|
run: |
|
||||||
|
npm start > /tmp/timmy-server.log 2>&1 &
|
||||||
|
server_pid=$!
|
||||||
|
trap 'kill "$server_pid"' EXIT
|
||||||
|
for attempt in $(seq 1 30); do
|
||||||
|
if curl --fail --silent http://127.0.0.1:4173/ > /dev/null; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if [ "$attempt" -eq 30 ]; then
|
||||||
|
cat /tmp/timmy-server.log
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
npm run test:ui
|
||||||
|
npm run test:photo
|
||||||
|
- name: Dependency audit
|
||||||
|
run: npm audit --audit-level=high
|
||||||
|
- name: Syntax checks
|
||||||
|
run: npm run check:syntax
|
||||||
|
- name: Diff hygiene
|
||||||
|
run: npm run check:diff
|
||||||
|
|
@ -4,9 +4,11 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "node --test tests/domain.test.js tests/analysis.test.js tests/vision-service.test.js tests/vision-config.test.js tests/training-ingest.test.js",
|
"test": "node --test tests/domain.test.js tests/analysis.test.js tests/vision-service.test.js tests/vision-config.test.js tests/training-ingest.test.js tests/ci-workflow.test.js tests/release-demo.test.js",
|
||||||
"test:ui": "node tests/ui.acceptance.mjs",
|
"test:ui": "node tests/ui.acceptance.mjs",
|
||||||
"test:photo": "node tests/photo-first.acceptance.mjs",
|
"test:photo": "node tests/photo-first.acceptance.mjs",
|
||||||
|
"check:syntax": "node --check app.js && node --check server.mjs && node --check service-worker.js && node --check src/analysis.js && node --check src/domain.js && node --check src/vision-config.js && node --check src/vision-service.js && node --check scripts/record_release_demo.mjs && bash -n scripts/run_selfhost_smolvlm.sh && python3 -m py_compile scripts/ingest_training_photo.py scripts/build_release.py",
|
||||||
|
"check:diff": "bash scripts/check_diff.sh",
|
||||||
"start": "node server.mjs"
|
"start": "node server.mjs"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
1
requirements-test.txt
Normal file
1
requirements-test.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Pillow==12.3.0
|
||||||
14
scripts/check_diff.sh
Normal file
14
scripts/check_diff.sh
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [[ -n "${GITHUB_BASE_REF:-}" ]] && git rev-parse --verify --quiet "origin/$GITHUB_BASE_REF" >/dev/null; then
|
||||||
|
git diff --check "origin/$GITHUB_BASE_REF...HEAD"
|
||||||
|
elif [[ -n "${GITHUB_EVENT_BEFORE:-}" ]] && [[ "$GITHUB_EVENT_BEFORE" != "0000000000000000000000000000000000000000" ]] && git cat-file -e "$GITHUB_EVENT_BEFORE^{commit}" 2>/dev/null; then
|
||||||
|
git diff --check "$GITHUB_EVENT_BEFORE..HEAD"
|
||||||
|
elif git rev-parse --verify --quiet HEAD^ >/dev/null; then
|
||||||
|
git diff --check HEAD^..HEAD
|
||||||
|
else
|
||||||
|
git diff --check HEAD
|
||||||
|
fi
|
||||||
|
|
||||||
|
git diff --check
|
||||||
|
|
@ -87,6 +87,7 @@ async function tap(selector, after = 650) {
|
||||||
}
|
}
|
||||||
|
|
||||||
await caption(`TIMMY ${version} • FEATURE DEMO`, 1300);
|
await caption(`TIMMY ${version} • FEATURE DEMO`, 1300);
|
||||||
|
await caption('Automated checks replay this synthetic path before review', 1300);
|
||||||
await caption('New path: private, photo-first stool logging', 1100);
|
await caption('New path: private, photo-first stool logging', 1100);
|
||||||
await tap('[data-scan]', 500);
|
await tap('[data-scan]', 500);
|
||||||
await page.getByText(/Self-hosted model ready/i).waitFor();
|
await page.getByText(/Self-hosted model ready/i).waitFor();
|
||||||
|
|
|
||||||
39
tests/ci-workflow.test.js
Normal file
39
tests/ci-workflow.test.js
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
|
||||||
|
const workflowPath = new URL('../.gitea/workflows/quality.yml', import.meta.url);
|
||||||
|
const diffCheckPath = new URL('../scripts/check_diff.sh', import.meta.url);
|
||||||
|
const pythonRequirementsPath = new URL('../requirements-test.txt', import.meta.url);
|
||||||
|
|
||||||
|
test('Gitea CI gates pull requests and main with the reproducible quality suite', async () => {
|
||||||
|
const workflow = await readFile(workflowPath, 'utf8');
|
||||||
|
|
||||||
|
assert.match(workflow, /^name: Quality gates$/m);
|
||||||
|
assert.match(workflow, /^on:\n push:\n branches: \[main\]\n pull_request:\n branches: \[main\]$/m);
|
||||||
|
assert.match(workflow, /^permissions:\n contents: read$/m);
|
||||||
|
assert.match(workflow, /^ timeout-minutes: 15$/m);
|
||||||
|
assert.match(workflow, /fetch-depth: 0/);
|
||||||
|
assert.match(workflow, /npm ci/);
|
||||||
|
assert.match(workflow, /python3 -m pip install --break-system-packages -r requirements-test\.txt/);
|
||||||
|
assert.match(workflow, /npm test/);
|
||||||
|
assert.match(workflow, /npm run test:ui/);
|
||||||
|
assert.match(workflow, /npm run test:photo/);
|
||||||
|
assert.match(workflow, /npm audit --audit-level=high/);
|
||||||
|
assert.match(workflow, /npm run check:syntax/);
|
||||||
|
assert.match(workflow, /npm run check:diff/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('CI pins the Python image dependency required by the full unit suite', async () => {
|
||||||
|
const requirements = await readFile(pythonRequirementsPath, 'utf8');
|
||||||
|
|
||||||
|
assert.match(requirements, /^Pillow==12\.3\.0$/m);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('diff hygiene checks only the pull request or latest commit range', async () => {
|
||||||
|
const script = await readFile(diffCheckPath, 'utf8');
|
||||||
|
|
||||||
|
assert.match(script, /origin\/\$GITHUB_BASE_REF\.\.\.HEAD/);
|
||||||
|
assert.match(script, /HEAD\^\.\.HEAD/);
|
||||||
|
assert.doesNotMatch(script, /hash-object -t tree/);
|
||||||
|
});
|
||||||
|
|
@ -41,6 +41,7 @@ await page.locator('#analyze-photo').click();
|
||||||
await page.getByText(/83% confidence/i).waitFor();
|
await page.getByText(/83% confidence/i).waitFor();
|
||||||
assert.equal(await page.getByText('Type 4', { exact: true }).isVisible(), true);
|
assert.equal(await page.getByText('Type 4', { exact: true }).isVisible(), true);
|
||||||
assert.equal(await page.getByText('brown', { exact: true }).isVisible(), true);
|
assert.equal(await page.getByText('brown', { exact: true }).isVisible(), true);
|
||||||
|
assert.match(await page.locator('.scan-result .fine').innerText(), /not a diagnosis/i);
|
||||||
await page.screenshot({ path: 'artifacts/photo-first-result-mobile.png', fullPage: false });
|
await page.screenshot({ path: 'artifacts/photo-first-result-mobile.png', fullPage: false });
|
||||||
|
|
||||||
await page.locator('#use-suggestion').click();
|
await page.locator('#use-suggestion').click();
|
||||||
|
|
|
||||||
13
tests/release-demo.test.js
Normal file
13
tests/release-demo.test.js
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
|
||||||
|
const demoPath = new URL('../scripts/record_release_demo.mjs', import.meta.url);
|
||||||
|
|
||||||
|
test('release demo visibly explains the CI-protected browser path without overstating safety', async () => {
|
||||||
|
const demo = await readFile(demoPath, 'utf8');
|
||||||
|
|
||||||
|
assert.match(demo, /Automated checks replay this synthetic path before review/);
|
||||||
|
assert.match(demo, /tests\/fixtures\/synthetic-type4\.jpg/);
|
||||||
|
assert.match(demo, /Visual assistance — never a diagnosis\./);
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user