fix: two menus that closed while in use, and the e2e drift that hid them #474
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |