ci: enforce reproducible quality gates
Some checks failed
Quality gates / quality (pull_request) Failing after 1m56s
Some checks failed
Quality gates / quality (pull_request) Failing after 1m56s
This commit is contained in:
parent
535834c060
commit
29ba8594b8
54
.gitea/workflows/quality.yml
Normal file
54
.gitea/workflows/quality.yml
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
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
|
||||
- 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,
|
||||
"type": "module",
|
||||
"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: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"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
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('Automated checks replay this synthetic path before review', 1300);
|
||||
await caption('New path: private, photo-first stool logging', 1100);
|
||||
await tap('[data-scan]', 500);
|
||||
await page.getByText(/Self-hosted model ready/i).waitFor();
|
||||
|
|
|
|||
31
tests/ci-workflow.test.js
Normal file
31
tests/ci-workflow.test.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
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);
|
||||
|
||||
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, /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('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();
|
||||
assert.equal(await page.getByText('Type 4', { 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.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