- Fix MD040 (11 bare fences): add language tags (text/bash) across 7 files
- Fix MD031 (2 violations): add blank lines around fence in GETTING-STARTED.md
- Wire 'Markdown lint' step to fast-checks job (after Format check, before Typecheck)
- Reformat .markdownlint-cli2.jsonc per Prettier (trailing commas in JSONC)
- pnpm md:lint exits 0; pnpm format:check exits 0; gate can fail on bare fence (verified)
- Updated calendar.spec.ts header to list all three profiles (iphone/pixel/desktop)
- Updated lists.spec.ts header to list all three profiles (iphone/pixel/desktop)
- Updated e2e/README.md preamble to add 'Desktop Chrome (1280x720)'
- Added --project=desktop example to README run-commands block
- Updated README full-suite command comment to name all three profiles
- Cosmetic: ci.yml step-name and comment updated to mention desktop (no plumbing change)
- Full suite verified: 85 passed, 5 skipped (3 desktop geometry + 2 parity guards), 0 failed
- Added test.skip(testInfo.project.name === 'desktop') to the two safe-area-inset
BottomTabBar in-viewport tests (BottomTabBar returns null at >=768px on desktop)
- Added test.skip(testInfo.project.name === 'desktop') to the 56x56 FAB geometry test
(on desktop 'New Event' resolves to the toolbar button, not the 56px FAB)
- Added desktop-only D-04 parity test asserting 'New Event' toolbar button height >=44px
guarded by test.skip(testInfo.project.name !== 'desktop')
- Updated header jsdoc to list all three profiles including desktop
- All mobile assertions preserved (toBeGreaterThanOrEqual(56) and (44) still present)
- Add .then(navigated) check: opens new window when client.navigate() resolves null
- Add .catch(): opens new window when client.focus() or client.navigate() rejects
- Both branches guarded by self.clients.openWindow per spec
- Returned chain (not floating) satisfies no-floating-promises gate
- All other behaviour preserved: close(), url extraction, post-loop fallback
GSD planning artifacts (.planning/**) are rewritten by tooling on every workflow
run and are never hand-formatted. Including them in Prettier scope caused
pnpm format:check to exit 1 on virtually every future PR, making the SC-3
green baseline impossible to hold. Add .planning/ to .prettierignore, mirroring
the same rationale as dist/ and apps/api/src/db/migrations/.
Real source docs (README.md, docs/**) remain in Prettier scope.
No playwright-report/, test-results/, or coverage/ directories exist to ignore.
- Insert 'Format check: pnpm format:check' after Lint in fast-checks job
- Remove the 3-line comment that said lint was a no-op (no longer true)
- Step order: Install → Lint → Format check → Typecheck → PWA unit tests
- Branch-protection contexts (name:CI, fast-checks, api, harness) unchanged
- Replace inline 'as string | null' assertion with an explicit typed const
declaration inside the vi.hoisted() callback body
- 'value: string | null' typed const satisfies both ESLint (no assertion) and
tsc (null assignment on line 82 is type-safe)
- eslint.config.js: disable React Compiler rules (v7 flat.recommended enables
them; codebase does not use the Compiler); add e2e/ to disableTypeChecked
block; promote exhaustive-deps to error
- API broker: remove redundant as-casts (outboxWorker, poller, reminderScheduler,
expand, sync, vevent, spike); add targeted ical.js no-unsafe-assignment/argument
disables with justifying comments inside try blocks
- API routes/sse.ts: fix no-misused-promises on async writeSSE callback with
void+IIFE+catch pattern
- API routes/lists.ts: let → const for updateValues
- API tests: remove unused imports (beforeEach, eq, vi); rename unused vars
with _ prefix; remove unused lastActiveId assignment
- PWA components: void navigate() and void queryClient.invalidateQueries() on
all fire-and-forget call sites; fix CalendarShell explicit-type-casts;
Couldn't → HTML entity
- PWA test files: as unknown as Response for partial mock objects; string | null
type annotation on mockLastSyncedUid; remove async from test callbacks without
await; act(() => {}) not await act(async () => {}) for sync ops
- sw.ts: restructure Notification.data?.url access as let+if so disable
comments land on the exact violation lines; void self.skipWaiting()
Plan 13-01 complete: ESLint flat config + Prettier installed and wired,
SC-1 gate proven (exit 1 on deliberate violations), 119 real violations
inventoried for Plan 02 (api:42, pwa:77). STATE advanced to Plan 2 of 3.
SC-1 smoke revealed flat.recommended enables react/react-in-jsx-scope (error),
while the legacy recommended disables it. This codebase uses jsx:"react-jsx"
(React 17+ automatic transform) so React does not need to be in scope. Disable
the rule explicitly alongside react/prop-types in the pwa-react config block.
Also proved both gate halves:
- pnpm lint exits 1 on no-floating-promises deliberate violation (SC-1 lint)
- pnpm format:check exits 1 listing _format-gate-test.ts (SC-1 format)
Both throwaway files deleted; neither staged nor committed.
- Create .gitea/workflows/publish.yml (push-to-main only, name=Publish)
- Strip publish job, push trigger, and MILESTONE env from ci.yml
- Eliminates orphaned CI / publish (pull_request) status on PRs
- Preserves all three required PR status contexts unchanged
The registry PAT secret had to be named REGISTRY_PAT (Gitea forbids the GITEA_
prefix for secret names). Point the publish docker-login at secrets.REGISTRY_PAT.
- Add publish job gated on push to refs/heads/main (never pull_request)
- docker login via --password-stdin with secrets.GITEA_REGISTRY_PAT (Pitfall 13)
- docker build --target production -f apps/api/Dockerfile . (repo-root context, T-08-10)
- Push :latest and :${MILESTONE}-${SHORT_SHA} tags per D-04
- docker logout in always() step to drop credential after push
- No dev-bypass flag in publish job (T-08-09 boundary)
- Run #11 (PR #3): 58 passed in 1.6 min (iphone/WebKit + pixel/Chromium)
- 4 infrastructure fixes: API-reap at step boundary, IPv4-first for Vite, dev-user FK seed, reporter double-forward via pnpm
- No Phase 7 harness file modified (phase boundary D-01/D-02 held)
- Advance position to 08-04 (publish job)
Run #10 cleared global-setup but playwright errored 'No tests found':
`pnpm test:e2e -- --reporter=list,html` routes through two pnpm script layers
(root→pwa) and the `--` survives into `playwright test -- --reporter=list,html`,
where playwright treats --reporter as a test-file filter. Call the pwa script
directly and append the flag without `--` (validated: 58 specs list vs 0).
global-setup.ts seeds calendars/lists/events for user_id=1 but never creates
the user (DEV_AUTH_BYPASS injects DEV_USER in-memory only). On a fresh CI DB the
calendars INSERT IGNORE is silently skipped on the users FK → calendar 10 missing
→ calendar_events insert fails FK. Add an idempotent users(id=1) seed after
migrate (validated locally: full insert chain passes). No harness files changed.
global-setup polls baseURL/health via Node fetch; the runner resolves localhost
to ::1 first, but Vite binds IPv4-only (127.0.0.1:5173), so localhost→::1:5173
→ ECONNREFUSED → health poll never returns 200. (curl passed for :3000 because
curl falls back IPv4↔IPv6 and the API is dual-stack; Node fetch does not.)
Proven: [::1]:5173 ECONNREFUSED vs 127.0.0.1:5173 200. Point PLAYWRIGHT_BASE_URL
at 127.0.0.1 and add --dns-result-order=ipv4first. No harness files changed.
Run #7 harness failed: global-setup polled :5173/health (Vite proxy → :3000)
and never got 200. The API connected to the DB and :3000/health was green
during the separate 'Wait for API' step, but the bare-backgrounded node
process was reaped at the step boundary and was dead by the time the e2e step
ran (after the multi-minute browser install). Confirmed locally the API does
not self-crash (alive + healthy for 75s in-shell).
Install browsers first, then start the API and run Playwright in a SINGLE step
so the API stays a child of the test shell for the whole run; capture the test
exit code and kill the API after. No harness files touched.
- Add harness job to ci.yml (ubuntu-latest, pull_request, parallel with fast-checks + api)
- MariaDB 11 service container with healthcheck.sh readiness (same pattern as api job)
- mysql2 readiness poll (no mysql CLI in runner image, D-PROBE-03)
- db:migrate via drizzle-kit (never db:push, T-08-07)
- pnpm --filter @familysync/api build before starting (Pitfall 4)
- API background: DEV_AUTH_BYPASS=true inline on node line (Pitfall 8), NODE_ENV=development
- curl retry loop on localhost:3000/health, 60s deadline, kill+exit on timeout (D-02/T-08-08)
Plans 08-03 (harness UI-regression job) and 08-04 (publish job) are not yet
built; CI-01's definition includes the harness step (08-03). Revert phase 8
and CI-01 from complete back to in-progress.
pnpm -r lint exited 1 (ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT) because no package
defines a lint script yet, failing the CI fast-checks job. Switch to
pnpm -r --if-present lint so it exits 0 today and gates automatically once a
lint script is added. (Lint tooling wiring remains out of scope.)
0000_easy_slipstream already created lists/list_shares/list_items and the
calendars unique constraint, but 0001_lists_schema re-created those tables and
0001_calendars_user_url_unique was an orphan (not in _journal) — so a cold
`drizzle-kit migrate` against an empty DB failed with ERROR 1050 'Table lists
already exists'. Dev only survived because its DB was built incrementally; CI
is the first cold migrate and exposed it.
Regenerated a single 0000_baseline.sql from schema.ts. Verified on a fresh
mariadb:11: migrate succeeds, schema is structurally identical to the running
dev DB, `drizzle-kit generate` reports no drift, and all 238 API tests pass.
Local dev DBs must be rebuilt (drop + db:migrate); no prod exists.
- api job: runs-on ubuntu-latest, if pull_request, parallel with fast-checks (no needs:)
- services: mariadb:11 with healthcheck.sh --connect --innodb_initialized options
(--health-start-period=30s for MariaDB 11 InnoDB cold-start, --health-retries=10)
- DB_HOST: mariadb (Docker-executor confirmed by D-PROBE-02)
- Throwaway creds: familysync/testpass scoped to ephemeral service container (T-08-03)
- No actions/cache (D-PROBE-04)
- Node mysql2 readiness poll via --input-type=commonjs inline script, 90s deadline
(no mysql CLI in runner image per D-PROBE-03; Pitfall 11 belt-and-suspenders)
- db:migrate (drizzle-kit migrate); drizzle push never used (T-08-04, MariaDB unsafe)
- pnpm --filter @familysync/api test: full DB-backed API test suite
- on: pull_request + push branches:[main]; workflow env MILESTONE: v1.1
- fast-checks job: runs-on ubuntu-latest, if pull_request
- Node 22 via actions/setup-node@v4 + corepack enable pnpm
- No actions/cache (D-PROBE-04: times out on this runner)
- pnpm install --frozen-lockfile, lint (no-op), typecheck, PWA unit tests
- DB-backed pnpm test intentionally absent from this job
The act_runner advertises ubuntu-latest/ubuntu-24.04/ubuntu-22.04; runs-on:
self-hosted matched no runner and the probe job stayed queued. Switch the
probe (and the plan key-link) to ubuntu-latest. ci.yml (Plans 02-04) must
use the same label.
- Probe-only workflow triggering on gsd/phase-08-gitea-ci branch only
- Answers P-01..P-11 + P-13: Node version, pnpm, runner mode (critical
fork Docker vs host), Docker socket, MariaDB service container spawn
and reachability on both hostnames, actions/cache, Playwright WebKit
deps, gitea-upload-artifact fork, and GITHUB_SHA short-SHA expression
- Uses healthcheck.sh --connect --innodb_initialized for MariaDB (never
the binary removed from mariadb:11 — Pitfall 11)
- Uses ChristopherHX/gitea-upload-artifact@v4 (not the official action
which aborts on Gitea with GHES detection — Pitfall 6 / T-08-SC)
- P-12 (docker login) deferred to Plan 04 — probe never references any
secret (T-08-01 compliant)
- All steps that may fail use continue-on-error: true so probe reports
findings instead of red-failing on expected unknowns
Deep review found the calendar 'populated state' assertions were vacuous:
- getByText('Nothing here').toHaveCount(0) targeted CalendarShell's EmptyState,
which CalendarShell NEVER renders (success branch always mounts ScheduleXCalendar;
EmptyState.tsx is dead code, imported by nothing). The check was permanently green
regardless of the seed — a regression dropping all events would have shipped green.
- .sx-react-calendar-wrapper renders on any successful auth, with or without events,
so it never proved the seed reached the UI.
Replaced the dead-EmptyState check with a real DB→UI proof: assert the seeded event
title 'Seeded Test Event' is rendered in the grid. Verified non-vacuous — passes with
the seed on both profiles; with /api/events mocked to [] the title is absent (would fail).
BL-02: the seed anchored the event at now+24h. Both phone profiles render the
month-agenda view of the CURRENT month, so on a month's last day 'tomorrow' falls into
the next month and vanishes from the grid, making the new visibility assertion date-fragile.
Re-anchored to noon-today (UTC) — always today's local date, always in the current-month view.
Verified: full 58-test suite passes both profiles; typecheck clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>