Skip to content

fix: two menus that closed while in use, and the e2e drift that hid them #474

fix: two menus that closed while in use, and the e2e drift that hid them

fix: two menus that closed while in use, and the e2e drift that hid them #474

Workflow file for this run

name: CI
on:
pull_request:
push:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
# Cancel superseded runs on PR branches, but never on main: each commit to
# main must keep its own completed CI status, so two merges minutes apart
# do not leave the first commit with a cancelled, statusless run.
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
# P1 says a PR body quotes the issue's acceptance lines, and the Spec review
# axis reads them. The only check was that the template FILE contains the
# heading, which no PR body has to honour: the clause landed 2026-08-28 and
# the next four merged PRs all ignored it.
#
# Fork PRs are exempt. The rule governs how this project's own work lands;
# failing a first-time outside contributor on a heading nobody told them
# about would cost more than it buys.
#
# Bots are exempt too. P1 is about a human landing work against an issue's
# acceptance lines; a dependabot bump has no issue and no acceptance lines to
# quote, and this job failed all five of the first batch it ever saw.
pr-acceptance:
runs-on: ubuntu-latest
if: >-
github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository &&
github.event.pull_request.user.type != 'Bot'
steps:
- name: PR body quotes the issue's acceptance lines
env:
PR_BODY: ${{ github.event.pull_request.body }}
run: |
section=$(printf '%s' "$PR_BODY" | awk '/^##[[:space:]]*Acceptance/{f=1;next} /^##[[:space:]]/{f=0} f')
# Drop the template's HTML comment and any blank lines before judging.
content=$(printf '%s' "$section" | perl -0pe 's/<!--.*?-->//gs' | tr -d '[:space:]')
if [ -z "$content" ]; then
echo "::error title=No acceptance lines::This PR body has no '## Acceptance' section with content. P1 in AGENTS.md: land through an issue-linked PR whose body quotes the issue's acceptance lines, because the Spec review axis judges the diff against them."
exit 1
fi
echo "Acceptance section present."
native-version-guard:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '22'
# Rule 28 / refs #217 finding 1: the local commit-msg hook that blocks
# incidental native build-number bumps (versionCode / CURRENT_PROJECT_VERSION)
# from landing outside a `chore:` commit only runs if a contributor ran
# `npm install` at the repo root, and `git commit -n` skips it outright.
# This job re-checks every commit on the PR/push so the rule can't be
# bypassed by a missing hook install or a --no-verify commit.
- name: Check native-version-bump guard on every commit
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
RANGE="${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}"
else
RANGE="${{ github.event.before }}..${{ github.event.after }}"
fi
node scripts/check-native-version-bump.mjs --ci "$RANGE"
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '22'
cache: 'npm'
cache-dependency-path: app/package-lock.json
- name: Install dependencies
working-directory: ./app
run: npm ci
# Advisory only: the repo has a pre-existing lint backlog (~213 problems,
# mostly react-hooks/* and no-explicit-any) that is out of scope to fix
# here. continue-on-error keeps this from blocking PRs. Remove it to make
# lint a hard gate once the backlog is burned down (tracked in #217).
- name: Run lint
working-directory: ./app
continue-on-error: true
run: npm run lint
# Hard gate on the size of that backlog (refs #281). .lint-baseline.json
# records the count per rule; this fails when any rule goes above its
# number. The backlog can shrink or hold, never grow, which is what the
# advisory job above cannot enforce on its own.
- name: Lint ratchet
working-directory: ./app
run: npm run lint:ratchet
lint-a11y:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '22'
cache: 'npm'
cache-dependency-path: app/package-lock.json
- name: Install dependencies
working-directory: ./app
run: npm ci
# Hard gate: jsx-a11y is the one ruleset fully clean on this repo (all
# 35 pre-existing violations were fixed, refs #217). eslint.a11y.config.js
# enables only jsx-a11y rules (everything else off), so this job is not
# affected by the react-hooks/*, no-explicit-any backlog that keeps the
# `lint` job above advisory. No continue-on-error: a new a11y violation
# fails the PR.
- name: Run a11y lint
working-directory: ./app
run: npm run lint:a11y
lint-react-correctness:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '22'
cache: 'npm'
cache-dependency-path: app/package-lock.json
- name: Install dependencies
working-directory: ./app
run: npm ci
# Hard gate: react-hooks/purity and react-hooks/immutability are at zero
# (refs #281). Unlike the rest of the react-hooks backlog that keeps the
# `lint` job advisory, these two describe behaviour that changes under
# memoization rather than performance advice, so a regression fails the
# PR. eslint.correctness.config.js enables only those two rules.
- name: Run React correctness lint
working-directory: ./app
run: npm run lint:correctness
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '22'
cache: 'npm'
cache-dependency-path: app/package-lock.json
- name: Install dependencies
working-directory: ./app
run: npm ci
- name: Typecheck and build
working-directory: ./app
run: npm run build
unit-tests:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
# agents-contracts.test.ts verifies commit hashes cited as
# evidence in agents/project/domain-context.md; a shallow clone
# has no history and fails that check.
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '22'
cache: 'npm'
cache-dependency-path: app/package-lock.json
- name: Install dependencies
working-directory: ./app
run: npm ci
- name: Run unit tests
working-directory: ./app
run: npm run test:unit
proven-red:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '22'
cache: 'npm'
cache-dependency-path: app/package-lock.json
- name: Install dependencies
working-directory: ./app
run: npm ci
- name: Script unit tests
run: npm run test:scripts
# P2 by machine: the tests this range changed must fail on the code
# the range replaces (scripts/proven-red.mjs). The unit-tests job
# proves the same tests are green on head.
- name: Changed tests go red on the pre-change code
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
node scripts/proven-red.mjs "${{ github.event.pull_request.base.sha }}" "${{ github.event.pull_request.head.sha }}" --title "${{ github.event.pull_request.title }}"
else
node scripts/proven-red.mjs "${{ github.event.before }}" "${{ github.event.after }}"
fi
e2e-tests:
runs-on: ubuntu-latest
needs: [lint, build, unit-tests]
steps:
- name: Checkout repository
uses: actions/checkout@v4
# The browser e2e suite drives a real ZoneMinder, so without those
# secrets there is nothing to run against. The job still has to end
# green or it would block every PR on a fork and on any clone that has
# no server, which is why it is not a required check: a green tick here
# means "ran, or had nothing to run", and only the summary below tells
# the two apart. Rule M2: read what a gate measured, not its exit code.
- name: Check E2E secrets
id: e2e-secrets
run: |
if [ -n "${{ secrets.ZM_HOST_1 }}" ] && [ -n "${{ secrets.ZM_USER_1 }}" ] && [ -n "${{ secrets.ZM_PASSWORD_1 }}" ]; then
echo "has_secrets=true" >> "$GITHUB_OUTPUT"
else
echo "has_secrets=false" >> "$GITHUB_OUTPUT"
fi
- name: Report that no E2E test ran
if: steps.e2e-secrets.outputs.has_secrets != 'true'
run: |
echo "::warning title=E2E skipped::No ZM_HOST_1/ZM_USER_1/ZM_PASSWORD_1 secret is configured, so no browser e2e test ran. This job is green because it had nothing to run, not because the suite passed."
{
echo "### Browser e2e did not run"
echo
echo "No \`ZM_HOST_1\` / \`ZM_USER_1\` / \`ZM_PASSWORD_1\` secret is configured."
echo "The suite drives a real ZoneMinder, so it had nothing to run against."
echo "This job is green because it was skipped, **not** because the tests passed."
} >> "$GITHUB_STEP_SUMMARY"
- name: Setup Node.js
if: steps.e2e-secrets.outputs.has_secrets == 'true'
uses: actions/setup-node@v7
with:
node-version: '22'
cache: 'npm'
cache-dependency-path: app/package-lock.json
- name: Install dependencies
if: steps.e2e-secrets.outputs.has_secrets == 'true'
working-directory: ./app
run: npm ci
- name: Install Playwright browsers
if: steps.e2e-secrets.outputs.has_secrets == 'true'
working-directory: ./app
run: npx playwright install --with-deps chromium
- name: Run E2E tests
if: steps.e2e-secrets.outputs.has_secrets == 'true'
working-directory: ./app
env:
ZM_HOST_1: ${{ secrets.ZM_HOST_1 }}
ZM_USER_1: ${{ secrets.ZM_USER_1 }}
ZM_PASSWORD_1: ${{ secrets.ZM_PASSWORD_1 }}
run: npm run test:e2e