Skip to content
Merged
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
51 changes: 50 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,55 @@ jobs:
path: test-results/
retention-days: 7

firefox_smoke:
name: Firefox smoke
runs-on: ubuntu-24.04
timeout-minutes: 10
continue-on-error: false

steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.0.0
with:
node-version-file: .nvmrc
cache: npm
cache-dependency-path: package-lock.json

- name: Install dependencies
env:
HUSKY: 0
run: npm ci

- name: Cache Playwright browsers
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.4
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-firefox-${{ hashFiles('package-lock.json') }}

- name: Install Playwright browsers
run: npx playwright install --with-deps firefox

- name: Firefox smoke tests
run: npm run test:e2e:firefox

- name: Upload Firefox traces
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: firefox-smoke-traces
path: test-results/
retention-days: 7

verify:
name: verify
runs-on: ubuntu-latest
needs:
- verify_matrix
- webkit_smoke
- firefox_smoke
if: ${{ !cancelled() }}

steps:
Expand All @@ -149,6 +192,12 @@ jobs:
echo "webkit_smoke result: ${{ needs.webkit_smoke.result }}"
exit 1

- name: Fail when Firefox smoke does not succeed
if: ${{ needs.firefox_smoke.result != 'success' }}
run: |
echo "firefox_smoke result: ${{ needs.firefox_smoke.result }}"
exit 1

- name: Confirm required CI success
if: ${{ needs.verify_matrix.result == 'success' && needs.webkit_smoke.result == 'success' }}
if: ${{ needs.verify_matrix.result == 'success' && needs.webkit_smoke.result == 'success' && needs.firefox_smoke.result == 'success' }}
run: echo "All required CI jobs passed"
8 changes: 7 additions & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,11 @@ jobs:
restore-keys: |
${{ runner.os }}-playwright-release-
${{ runner.os }}-playwright-webkit-
${{ runner.os }}-playwright-firefox-
${{ runner.os }}-playwright-

- name: Install Playwright browsers
run: npx playwright install --with-deps chromium webkit
run: npx playwright install --with-deps chromium webkit firefox

- name: Run Chromium smoke against CI artifact
env:
Expand All @@ -112,6 +113,11 @@ jobs:
PLAYWRIGHT_WEB_SERVER_CMD: npm run preview:prod
run: npm run test:e2e:webkit:smoke

- name: Run Firefox release smoke against CI artifact
env:
PLAYWRIGHT_WEB_SERVER_CMD: npm run preview:prod
run: npm run test:e2e:firefox:smoke

deploy:
name: Deploy
if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' && github.event.workflow_run.head_branch == 'main' }}
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ The format is based on Keep a Changelog, and this project follows Semantic Versi
- removed the internal IndexedDB auto-increment `id` from new JSON and crash-JSON exports, while keeping legacy backups that still carry `id` importable
- hardened import commit verification by reading the pre-write snapshot and deriving the undo snapshot inside the rw transaction
- unified focus chrome on DomainCard radios, history grid cells, mobile day buttons, and the history scroll region under shared focus utilities
- added a narrow merge-blocking and release Firefox Playwright smoke lane, documented in ADR-0029, that proves Gecko engine-level compatibility without claiming live Firefox policy simulation

### Security

Expand Down
5 changes: 3 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ npm run typecheck
npm run test
npm run test:e2e
npm run test:e2e:webkit
npm run test:e2e:firefox
npm run build
```

Expand All @@ -51,6 +52,6 @@ npm run build
4. Keep language clinical and professional throughout the repo.
5. Any database schema change must add or update the migration registry, migration tests, and the relevant ADR before merge.
6. Any durability-sensitive migration change must include browser-level upgrade proof before merge, not just unit coverage.
7. The narrow WebKit smoke lane now gates CI. Keep it scoped to engine-level compatibility proof, not Safari policy simulation.
8. If a WebKit smoke failure is accepted as a platform boundary instead of a product bug, document that boundary in docs/webkit-limitations.md before merge.
7. The narrow WebKit and Firefox smoke lanes now gate CI. Keep them scoped to engine-level compatibility proof, not browser-policy simulation.
8. If a WebKit or Firefox smoke failure is accepted as a platform boundary instead of a product bug, document that boundary in docs/webkit-limitations.md or docs/firefox-limitations.md before merge.
9. When a module encodes a non-obvious architectural constraint, keep a short `// Architecture:` ADR reference comment near that boundary so future changes do not have to rediscover the rationale.
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

<div align="center">

[![Pipeline: Mainline Integrity](https://github.com/bradsaucier/opsnormal/actions/workflows/ci.yml/badge.svg)](https://github.com/bradsaucier/opsnormal/actions/workflows/ci.yml) [![Pipeline: Pages Release](https://github.com/bradsaucier/opsnormal/actions/workflows/deploy.yml/badge.svg)](https://github.com/bradsaucier/opsnormal/actions/workflows/deploy.yml) [![CodeQL](https://github.com/bradsaucier/opsnormal/actions/workflows/codeql.yml/badge.svg)](https://github.com/bradsaucier/opsnormal/actions/workflows/codeql.yml) [![Status: v1.0.0](https://img.shields.io/badge/Status-v1.0.0_public_release-36476F?style=flat-square)](./CHANGELOG.md) [![Data posture: Local only](https://img.shields.io/badge/Data_Posture-Local_Only-36476F?style=flat-square)](#trust-contract)
[![Pipeline: Mainline Integrity](https://github.com/bradsaucier/opsnormal/actions/workflows/ci.yml/badge.svg)](https://github.com/bradsaucier/opsnormal/actions/workflows/ci.yml) [![Pipeline: Pages Release](https://github.com/bradsaucier/opsnormal/actions/workflows/deploy.yml/badge.svg)](https://github.com/bradsaucier/opsnormal/actions/workflows/deploy.yml) [![Status: v1.0.0](https://img.shields.io/badge/Status-v1.0.0_public_release-36476F?style=flat-square)](./CHANGELOG.md) [![Data posture: Local only](https://img.shields.io/badge/Data_Posture-Local_Only-36476F?style=flat-square)](#trust-contract)

</div>

Expand Down Expand Up @@ -167,9 +167,9 @@ Each row's verification-truth column states what the repo currently proves, not
| --------------------------- | ---------------------- | -------------------------------------------------------------------------------- |
| Chromium-based browsers | Supported | Full Playwright Chromium coverage, production-artifact smoke, and release gating |
| Safari and other WebKit UIs | Supported with caveats | Merge-blocking and release WebKit smoke lanes prove engine compatibility only |
| Firefox current release | Expected to work | Manual verification recommended because there is no dedicated Firefox CI lane |
| Firefox current release | Supported with caveats | Merge-blocking and release Firefox smoke lanes prove engine compatibility only |

Read [WebKit CI coverage boundary](./docs/webkit-limitations.md) before making stronger Safari claims than the repo proves.
Read [WebKit CI coverage boundary](./docs/webkit-limitations.md) and [Firefox CI coverage boundary](./docs/firefox-limitations.md) before making stronger browser claims than the repo proves.

### Reliability posture

Expand All @@ -196,8 +196,8 @@ Accessibility is architectural, not decorative.

Quality is enforced through release gates, test coverage, and explicit design constraints.

- GitHub Actions runs lint, typecheck, Vitest coverage, Playwright Chromium verification, a merge-blocking Playwright WebKit smoke lane, and build validation
- GitHub Pages release downloads the `dist-ci-verified` artifact from the successful mainline integrity run, re-smokes that exact bundle in Chromium and WebKit, and only then publishes
- GitHub Actions runs lint, typecheck, Vitest coverage, Playwright Chromium verification, merge-blocking Playwright WebKit and Firefox smoke lanes, and build validation
- GitHub Pages release downloads the `dist-ci-verified` artifact from the successful mainline integrity run, re-smokes that exact bundle in Chromium, WebKit, and Firefox, and only then publishes
- The released bundle carries a Sigstore-backed build-provenance attestation that Pipeline: Pages Release verifies before upload. See ADR-0027.
- GitHub CodeQL code scanning gates mainline with the `security-extended` and `security-and-quality` query packs. See ADR-0028.
- JSON export carries versioning and integrity checks, and import commit verification fails closed before the app claims success
Expand Down Expand Up @@ -225,11 +225,14 @@ npm run typecheck
npm run test
npm run test:e2e
npm run test:e2e:webkit
npm run test:e2e:firefox
npm run build
npm run test:e2e:smoke
npm run test:e2e:webkit:smoke
npm run test:e2e:firefox:smoke
```

`npm run format:check` verifies repository formatting with Prettier, and `npm run format` applies the repository formatting baseline locally. `npm run test:e2e` builds the e2e-mode harness bundle and runs the full Chromium suite. `npm run test:e2e:webkit` runs the narrow WebKit smoke gate that verifies rendering and IndexedDB I/O on a WebKit engine without claiming to reproduce Safari eviction behavior. Run `npm run build` before `npm run test:e2e:smoke` so the smoke command reuses a real production `dist/` build and skips the harness-only specs.
`npm run format:check` verifies repository formatting with Prettier, and `npm run format` applies the repository formatting baseline locally. `npm run test:e2e` builds the e2e-mode harness bundle and runs the full Chromium suite. `npm run test:e2e:webkit` runs the narrow WebKit smoke gate that verifies rendering and IndexedDB I/O on a WebKit engine without claiming to reproduce Safari eviction behavior. `npm run test:e2e:firefox` runs the parallel Gecko smoke gate that verifies boot, IndexedDB persistence, service worker activation, the non-WebKit storage path, and the fallback download path without claiming to simulate live Firefox storage policy. Run `npm run build` before the `test:e2e:*:smoke` commands so the production-artifact smoke checks reuse a real `dist/` build and skip the harness-only specs.

## Documentation matrix

Expand All @@ -240,6 +243,7 @@ This README stays focused on orientation and first use. Deeper proof, limits, an
| [**Architecture overview**](./docs/architecture.md) | Runtime shape, persistence model, recovery posture, PWA behavior, and known limits |
| [**Risk register**](./docs/risk-register.md) | Known operational risks, browser-storage hazards, and current mitigations |
| [WebKit CI coverage boundary](./docs/webkit-limitations.md) | What the merge-blocking WebKit lane proves, what it cannot prove, and how to triage failures |
| [Firefox CI coverage boundary](./docs/firefox-limitations.md) | What the merge-blocking Firefox lane proves, what it cannot prove, and how to triage failures |
| [**Architecture Decision Records**](./docs/decisions/README.md) | Why the repo chose IndexedDB, local-only boundaries, export integrity rules, and related constraints |
| [**Test plan**](./docs/test-plan.md) | Verification strategy, release checks, and coverage priorities |
| [Release checklist](./docs/release-checklist.md) | Pre-release validation and operator-facing quality gates |
Expand Down
1 change: 1 addition & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ Current repo controls include:
- Root and section-level React error boundaries
- Crash fallback that preserves export access after a render fault
- CI validation across lint, typecheck, unit and integration tests, end-to-end tests, and build
- Playwright Firefox smoke coverage enforces Gecko-engine compatibility at merge and release
- Dependabot coverage for npm and GitHub Actions dependencies

## Dependency maintenance posture
Expand Down
51 changes: 51 additions & 0 deletions docs/decisions/0029-firefox-smoke-engine-compat-gate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
## Status

Accepted
Amends ADR-0020 and ADR-0021.

## Context

ADR-0020 made the narrow WebKit smoke lane merge-blocking CI coverage.
ADR-0021 extended that enforcement to the signed release artifact.
That closed the repository's cross-engine gap for WebKit, but Firefox still remained a README-declared browser surface without dedicated CI evidence.

That remaining Firefox row was materially weaker than the rest of the repository's posture.
The application depends on Gecko-sensitive browser behavior for IndexedDB persistence, service worker registration, fallback Blob-download export when `showSaveFilePicker` is unavailable, and normal boot under the pinned CSP and Trusted Types contract.
Leaving Firefox on manual-only verification preserved a truthful caveat, but it also left the final supported engine family outside enforced merge and release evidence.

## Decision

Add a narrow Playwright Firefox smoke lane that runs both at merge time and at release time against the same `dist-ci-verified` artifact.

This lane is allowed to prove only these engine-level behaviors on Gecko:

1. app boot on a Gecko engine without CSP refusal events during normal startup
2. the non-WebKit storage-health path still renders in the shell
3. IndexedDB persistence across reload for the core check-in path
4. service worker registration reaches `activated`
5. the fallback Blob-download export path still triggers when `showSaveFilePicker` is unavailable

This decision does not authorize stronger claims about live Firefox policy behavior.
The lane is not a browser-policy oracle.
It does not prove OS-level storage persistence policy, live-hardware behavior, privacy-mode persistence differences, or file-manager handoff after the fallback download leaves the browser.
Manual verification on a current Firefox release remains in the release checklist.

## Triage rule for Firefox smoke failures

1. If the failure shows shipped behavior is broken on Firefox, fix the product code before merge.
2. If the failure is test fragility, harden the test without widening the claim.
3. If the failure reflects a real platform boundary that the repository accepts, document the boundary here and in `docs/firefox-limitations.md` before merge.

## Consequences

Positive:

- closes the last README compatibility row that lacked automated evidence
- extends the existing engine-compat gate pattern from WebKit to Gecko without changing application code
- blocks merge and release when Firefox-specific regressions hit IndexedDB persistence, service worker registration, or the fallback export path

Trade-offs:

- adds another Playwright lane that can block merge or release on real Gecko regressions or runner instability
- increases release latency by one more smoke pass against the shipped artifact
- requires accepted Firefox platform boundaries to be documented explicitly instead of handled silently
1 change: 1 addition & 0 deletions docs/decisions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ OpsNormal uses lightweight ADRs to record constraints that should not drift casu
| 0026 | CSP directive contract and Trusted Types |
| 0027 | Build provenance attestation for release artifact |
| 0028 | CodeQL source code scanning gate |
| 0029 | Firefox smoke engine compatibility gate |

## Operating rule

Expand Down
33 changes: 33 additions & 0 deletions docs/firefox-limitations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Firefox CI coverage boundary

OpsNormal treats the Playwright Firefox lane as a merge-blocking and release-blocking compatibility gate.
That gate is intentionally narrow.
It proves engine-level behavior on a Gecko implementation in CI.
It does not prove live Firefox storage-policy behavior on real hardware.

## What the Firefox gate is allowed to prove

1. the shell boots on a Gecko engine without CSP refusal events during normal startup
2. the non-WebKit storage-health path still renders in the shell
3. the core Dexie-backed check-in path persists across a page reload
4. service worker registration reaches an activated state on Firefox
5. the fallback Blob-download export path still works when `showSaveFilePicker` is unavailable

## What the Firefox gate is not allowed to prove

1. Firefox storage persistence policy or quota behavior on a real operating system profile
2. private-browsing persistence differences or profile-isolation behavior
3. live-device service-worker lifecycle behavior outside the narrow activation check
4. file-manager handoff behavior after the browser launches the fallback download on a real system

## Triage rule for Firefox smoke failures

1. If the failure shows shipped behavior is broken on Firefox, fix the product code before merge.
2. If the failure is test fragility, harden the test without widening the claim.
3. If the failure reflects a real platform boundary that the repository accepts, document the boundary here and in the affected test before merge.

## Manual verification remains required

Current Firefox release verification still belongs in the release checklist.
The CI gates strengthen enforcement.
They do not change the truth boundary.
4 changes: 3 additions & 1 deletion docs/release-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ Before tagging a release:
- [ ] per-file coverage thresholds on src/services/importService.ts and src/db/appDb.ts pass
- [ ] Playwright Chromium tests pass
- [ ] Playwright WebKit smoke gate passes in CI and any failure is triaged as a real regression, a test issue, or an explicitly documented platform boundary in docs/webkit-limitations.md
- [ ] Pages release waits for the successful main-branch run of Pipeline: Mainline Integrity, downloads the exact dist-ci-verified artifact from that run, reruns Chromium smoke and WebKit release smoke against that artifact, and only then publishes
- [ ] Playwright Firefox smoke gate passes in CI and any failure is triaged as a real regression, a test issue, or an explicitly documented platform boundary in docs/firefox-limitations.md
- [ ] Pages release waits for the successful main-branch run of Pipeline: Mainline Integrity, downloads the exact dist-ci-verified artifact from that run, reruns Chromium smoke, WebKit release smoke, and Firefox release smoke against that artifact, and only then publishes
- [ ] Pages release verifies the dist-ci-verified build-provenance attestation against the triggering commit SHA before smoke or upload
- [ ] public/CNAME matches the GitHub Pages custom-domain setting and the enforced HTTPS origin
- [ ] Vitest accessibility assertions pass on the direct-select check-in and history surfaces
Expand All @@ -28,6 +29,7 @@ Before tagging a release:
- [ ] manual recovery in one tab clears stale loop-breaker state in another open tab
- [ ] blocked duplicate-tab schema recovery completes after the 5000 millisecond guard window without entering a reload loop
- [ ] offline reopen verified manually
- [ ] current Firefox release verified manually for boot, reload persistence, fallback JSON export, and the non-WebKit storage-health path
- [ ] export verified manually
- [ ] root crash fallback verified manually where browser-specific behavior still matters
- [ ] history grid crash containment verified manually
Expand Down
Loading
Loading