diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index af2145b..64c52e4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -178,8 +178,42 @@ jobs: name: sbom path: ./release-assets + # Extract the section for this tag from CHANGELOG.md so it shows + # up at the top of the GitHub Release body. `generate_release_notes` + # below still appends the auto-generated "What's Changed" PR list + # afterwards — best of both: hand-curated narrative + complete + # commit/PR audit trail. The awk picks the block between + # "## []" and the next "## [". + - name: Extract CHANGELOG section for this release + id: changelog + run: | + set -euo pipefail + TAG_RAW="${GITHUB_REF_NAME:-${{ github.event.inputs.tag }}}" + VERSION="${TAG_RAW#v}" + echo "Extracting CHANGELOG section for version: ${VERSION}" + # Substring-based extraction (no regex escapes — different awk + # builds disagree on whether `\[` is literal or a metachar). + # `index($0, marker) == 1` is true iff the line starts with the + # exact marker string, which is what we want. + awk -v marker="## [${VERSION}]" ' + index($0, marker) == 1 { p=1; next } + p && index($0, "## [") == 1 { exit } + p && $0 != "---" { print } + ' CHANGELOG.md > release-body.md + if [[ ! -s release-body.md ]]; then + echo "::warning::CHANGELOG.md has no section for [${VERSION}] — falling back to auto-generated notes only" + else + echo "--- release-body.md preview (first 40 lines) ---" + head -40 release-body.md + echo "--- (truncated) ---" + fi + - uses: softprops/action-gh-release@v3 with: + body_path: release-body.md + # `body_path` gets *prepended* to the auto-generated PR list + # when both are set, so we end up with a curated header + the + # full commit audit trail underneath. generate_release_notes: true draft: false prerelease: ${{ contains(github.ref, '-') }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d6582e..fb23bf2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,182 +11,62 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2 ## [Unreleased] -### Added - -- **Automation Engine v1** (closes #44, implements [ADR 0010](docs/architecture/0010-automation-engine.md)) - — declarative "when X then Y" rules per board, fully zero-knowledge. - Rule bodies (name + conditions + actions) live inside `enc_rule` - on the server and are decrypted on the client; a small plaintext - **trigger envelope** (`trigger_type` + `trigger_meta`) is mirrored - in two new columns so a future BullMQ scheduler can route ticks - for time-based triggers without breaking E2EE. The envelope is - validated against a strict whitelist on every write (server-side - via `TriggerEnvelopeSchema`) so the metadata field cannot become - a back channel for plaintext leakage. -- **v1 triggers wired**: `card_created`, `card_moved`. **In the DSL - but not yet emitted**: `card_label_added`, `comment_added` (drawer - wiring is a follow-up). -- **v1 actions**: `set_label`, `assign_member`, `move_to_column` — - all idempotent at the repository level, so the Redis SETNX claim - from ADR 0010 §"Conflict resolution" is **not** needed in v1 and - ships alongside `post_comment` later. -- New code: `src/lib/automation/{dsl,evaluator,executor}.ts`, - `src/lib/repositories/automations.ts`, - `src/app/api/boards/[id]/automations/route.ts`, - `src/app/api/automations/[id]/route.ts`, - `src/app/(app)/.../automation-panel.tsx` (minimal UI: list / add / - delete / toggle active). -- Schema: `AutomationRule` gains `trigger_type` (`VARCHAR(64)`) and - `trigger_meta` (`JSON?`) plus a composite index - `(boardId, trigger_type, active)` for the future scheduler's - read path. -- Tests: `tests/unit/automation-dsl.test.ts` (14 cases), - `tests/unit/automation-evaluator.test.ts` (10 cases) — covers - DSL validation, envelope projection, server-side meta whitelist, - and condition matching including fail-closed for missing fields. - -- **Board-level presence** (closes #43) — Yjs awareness over the - existing E2EE WebSocket relay. Avatar-stack of online users - appears top-right of the board header (max 5 + "+N"); each card - in the kanban view shows small coloured dots when other users - have the card drawer open ("Lisa-sees-this-card" indicator). - Awareness payload carries only `{userId, color, viewing?}` — - no email, no card title, no description text. Server (the WS - relay) only sees opaque XChaCha20-Poly1305 ciphertext bound to - `board::awareness`. Colour is a deterministic HSL hash of - `userId` (stable across reloads). -- New module `src/lib/realtime/awareness.ts` (`BoardPresenceProvider` - class that wraps Yjs `Awareness` with our encrypted transport), - `src/lib/realtime/user-color.ts` (pure hash, no client-only deps), - and `src/hooks/use-board-presence.ts` (React hook). -- WebSocket relay gains a new room kind `board:` with workspace- - member authorization; existing `card:` semantics are unchanged. - Awareness late-joiner sync emits `request-state` to **all** peers - in the room (one peer would be enough for Y.Doc — but Awareness - is per-clientID and doesn't merge, so the joiner needs everyone's - state). -- `tests/unit/user-color.test.ts` — 6 cases including determinism, - HSL shape, negative-mod normalisation, and empty-input safety. - -- **Timeline / Gantt view** (closes #42) — fifth board view, sibling - of Board / Calendar / Table / Analytics. Cards with both `startAt` - and `dueAt` render as horizontal bars; cards with only `dueAt` - render as diamonds on the due day; cards without either are - omitted and counted in the footer hint. Rows are grouped by - milestone (cards without a milestone fall into a final "Ohne - Milestone" group). Day / week / month zoom toggle, a vertical - "today" line, and a "Today" reset button. The viewport - auto-fits to the data range; arrow keys pan and `+` / `-` zoom - (keyboard alternative to mouse-only Gantt UX, per - CLAUDE.md §5.10 WCAG 2.1 AA). Pure SVG renderer — no chart - library dependency. -- **Card `startAt` editor** — `card-drawer.tsx` now exposes a - start-date input alongside the existing due-date input. The - schema field has existed since the original `Card` model but was - not yet user-editable; needed for the Gantt view to draw range - bars instead of only diamonds. -- New file: `src/app/(app)/workspaces/[wsId]/boards/[boardId]/timeline-view.tsx`. +_Nothing yet._ -### Changed +--- -- `KanbanCard` (UI view-model) gains `startAt: string | null`. All - five construction sites in `board-client.tsx` (initial load, SSE - refetch, create-from-scratch, create-from-template, drawer - snapshot reconcile) propagate `startAt` from the `ApiCard` / - `CardSnapshot` it already carried but previously dropped on the - floor. The previously-hardcoded `startAt: null` in the - `` wiring is now `openCard.startAt`. +## [0.2.0-beta] — 2026-05-25 -### Security - -- **`otplib` 12 → 13** — auth-critical API rewrite. The `authenticator` - namespace is gone; v13 ships top-level functional exports - (`generateSecret`, `generateURI`, `generate`, `verify`) with the - default `verify` now async. `src/lib/auth/totp.ts` deliberately - uses `verifySync` instead of the async `verify` — in this path a - missed `await` would always evaluate the returned Promise as truthy - and silently bypass 2FA, so the sync variant removes the footgun. - `epochTolerance: 30 s` reproduces the old `window: 1` (±1 step × - 30 s period). Existing `User.totpSecret` rows remain valid (same - Base32 + HMAC-SHA1 + 30 s period); v13's new 16-byte minimum- - secret guardrail does not reject the 20-byte secrets our - `generateSecret()` produces. -- **`@simplewebauthn/{browser,server}` 11 → 13** — kept in lock-step - with the otplib bump even though @simplewebauthn is not yet used in - `src/` (reserved for the upcoming WebAuthn phase). Avoids future - split-version surprises. +The first sweep of post-beta work. Highlights: **Milestones + Burn-Down + Timeline (Gantt)** for time-/scope-based planning; **board-level presence** via Yjs awareness over the existing E2EE WS relay; **Automation Engine v1** (declarative `{when, do}` rules, zero-knowledge, [ADR 0010](docs/architecture/0010-automation-engine.md)); **MSKanban docs site** under [docu.msk-scripts.de/ecosystem/mskanban](https://docu.msk-scripts.de/ecosystem/mskanban); **auto-deploy** from GitHub Actions to the production server with a ForceCommand-locked SSH key. ### Added -- **`tests/unit/totp.test.ts`** — 17 cases covering roundtrip, - backward-compat to v12-era Base32 secrets, malformed-token - rejection, tampered-envelope behaviour and the ±30 s clock-drift - window. Closes a pre-existing zero-coverage gap on the TOTP path - (CLAUDE.md §9 mandates 100 % coverage for crypto code). +- **Milestones** (#39 + #40 + #41 + #48) — group cards by deliverable with an optional date window. Server sees the date range + board association (needed for filtering / charts); name + description live encrypted under the BoardKey. New `Milestone` model, REST routes, board-level manager modal, card-drawer selector, per-card badge, and a **Burn-Down chart** per milestone in the Analytics view. +- **Timeline / Gantt view** (#49) — fifth board tab next to Board / Calendar / Table / Analytics. Cards with `startAt` + `dueAt` render as range bars; only-`dueAt` cards as diamonds; missing-data cards are counted in the footer hint. Day / Week / Month zoom, vertical "today" line, arrow-key pan + `+`/`-` zoom (WCAG keyboard alternative). Pure SVG renderer. +- **Card `startAt` editor** (#49) — the field has existed in the `Card` model since Phase 3 but was never UI-editable. `card-drawer.tsx` now exposes a start-date input alongside the existing due-date input; the Gantt bars depend on it. +- **Board-level presence** (#50) — Yjs `Awareness` over the existing E2EE WebSocket relay. Avatar-stack of online users in the board header (max 5 + "+N"); per-card coloured dots when other users have the card drawer open. Awareness payload is `{userId, color, viewing?}` only — no email, no titles, no description text. Server only sees opaque ciphertext bound to `board::awareness`. Colour is a deterministic HSL hash of `userId`. New `BoardPresenceProvider` (`src/lib/realtime/awareness.ts`), `useBoardPresence` React hook, and a new room kind `board:` on the relay with workspace-member authorisation. +- **Automation Engine v1** (#52, implements [ADR 0010](docs/architecture/0010-automation-engine.md)) — declarative `{when, do}` rules per board, fully zero-knowledge. Rule bodies live encrypted in `enc_rule`; a small plaintext **trigger envelope** (`trigger_type` + `trigger_meta`) is mirrored in two new columns so a future BullMQ scheduler can route ticks for time-based triggers without breaking E2EE. The envelope is validated against a strict whitelist on every write so the metadata field cannot become a back channel for plaintext leakage. v1 wired triggers: `card_created`, `card_moved`; v1 actions: `set_label`, `assign_member`, `move_to_column` (all idempotent at the repo layer — Redis SETNX claim deferred together with `post_comment`). +- **MSKanban documentation site** — published as the third ecosystem entry on the existing Docusaurus instance ([docu.msk-scripts.de/ecosystem/mskanban](https://docu.msk-scripts.de/ecosystem/mskanban)). Seven pages: overview, installation (Docker + bare-metal Apache), getting-started, features, REST API, privacy & security (full key hierarchy + threat model), FAQ. +- **Auto-deploy from GitHub Actions to the production server** (#54, #55, #56) — `workflow_run` triggers on CI success, SSHes via a ForceCommand-locked action key, runs `scripts/deploy.sh` on the server (self-updating, idempotent, hard-fail on build / migration / health-check errors, optional WS-relay restart, deploy tags for rollback). Strict known_hosts check via `DEPLOY_HOST_FINGERPRINT` secret; no `StrictHostKeyChecking=no` anywhere. Full setup walkthrough in [`docs/deployment/auto-deploy.md`](docs/deployment/auto-deploy.md). +- **ADRs** — [0010 Automation Engine](docs/architecture/0010-automation-engine.md) added; previous ADRs index updated. +- **New unit-test suites** — `automation-dsl` (14 cases, #52), `automation-evaluator` (10 cases, #52), `user-color` (6 cases, #50), `totp` (17 cases, #35). 137 tests total at release time. ### Changed -- **Seven low-risk Dependabot bumps** landed in a single 2026-05-25 - sweep: `@types/node 22 → 25`, `prettier-plugin-tailwindcss 0.6 → 0.8`, - `eslint-config-prettier 9 → 10`, `vitest 3 → 4`, `@hookform/resolvers - 3 → 5`, `@dnd-kit/sortable 9 → 10`, `jsdom 25 → 29`. All shipped with - full CI (lint, typecheck, unit, E2E Playwright) green; no source - changes needed. -- **Second 2026-05-25 Dependabot sweep** (afternoon): 14 PRs across - 5 GitHub Actions majors (`docker/setup-qemu-action 3 → 4`, - `actions/cache 4 → 5`, `actions/download-artifact 4 → 8`, - `github/codeql-action 3 → 4`, `actions/upload-artifact 4 → 7`), - 9 npm bumps (`bullmq 5.77.2 → 5.77.3`, `lint-staged 15 → 17`, - `pino-pretty 11 → 13`, `typescript 5.9 → 6.0`, - `tailwind-merge 2 → 3`, `pino 9 → 10`, `lucide-react 0.460 → 1.16`, - `zod 3 → 4`, `y-websocket 2 → 3`). All shipped CI-green incl. - Playwright E2E. +- **`KanbanCard` view-model gains `startAt: string \| null`** (#49) — propagated through all five construction sites in `board-client.tsx` (initial load, SSE refetch, create-from-scratch, create-from-template, drawer reconcile). The previously-hardcoded `startAt: null` in the `` wiring is now `openCard.startAt`. +- **Repo-URL cleanup** (#53) — every `github.com/musiker15/mskanban` and `ghcr.io/musiker15/mskanban` reference in `README.md`, `CHANGELOG.md`, `SECURITY.md`, `CONTRIBUTING.md`, `CLAUDE.md`, `docs/public-launch.md`, `docs/deployment/mskanban.service` rewritten to the canonical `MSK-Scripts/mskanban`. The `release.yml` workflow already pushed containers to `ghcr.io/${{ github.repository_owner }}` (= `MSK-Scripts`) so the broken README badges were the only visible symptom. Also normalised the Code-of-Conduct contact e-mail to `@msk-scripts.de`. +- **README post-beta refresh** (#53) — "five views per board" (Timeline added), "WebAuthn / Passkeys planned" → both shipped, Milestones / Burn-Down / Timeline / Presence / Automation added to the feature highlights, broken `docs/crypto/` link replaced with ADR 0003 + threat-model, link to the new docs site. +- **21 Dependabot bumps** consolidated into the v0.1.0-beta → v0.2.0-beta window. **First sweep** (low-risk, single 2026-05-25 batch): `@types/node 22→25`, `prettier-plugin-tailwindcss 0.6→0.8`, `eslint-config-prettier 9→10`, `vitest 3→4`, `@hookform/resolvers 3→5`, `@dnd-kit/sortable 9→10`, `jsdom 25→29`. **Second sweep** (afternoon): 14 PRs across 5 GitHub Actions majors (`docker/setup-qemu-action 3→4`, `actions/cache 4→5`, `actions/download-artifact 4→8`, `github/codeql-action 3→4`, `actions/upload-artifact 4→7`) and 9 npm bumps (`bullmq 5.77.2→5.77.3`, `lint-staged 15→17`, `pino-pretty 11→13`, `typescript 5.9→6.0`, `tailwind-merge 2→3`, `pino 9→10`, `lucide-react 0.460→1.16`, `zod 3→4`, `y-websocket 2→3`). All shipped CI-green incl. Playwright E2E. - **`@vitest/coverage-v8` 3 → 4** — peer-match for the vitest 4 bump. -- **`@otplib/core`** removed as a direct dependency — it was unused in - `src/` (leftover from an earlier setup needing direct access to the - `HashAlgorithm` type from `@otplib/core`). Stays in the tree as a - transitive of `otplib`, so no functional change. ### Removed -- **Phase-3 plaintext plumbing** — `src/lib/encoding/plaintext-blob.ts`, - `tests/unit/plaintext-blob.test.ts`, and - `scripts/migrate-phase3-to-e2ee.ts` are gone. ADR 0007 is "executed": - every data-bearing API and every read path now goes through the - Phase-4 `v1..` envelope (28 call sites across workspace, - board, card and card-drawer clients). The `encodeBlob` / `decodeBlob` - helpers had zero consumers left. CLAUDE.md roadmap table updated to - reflect Phase 4 ✅. +- **Phase-3 plaintext plumbing** — `src/lib/encoding/plaintext-blob.ts`, `tests/unit/plaintext-blob.test.ts`, and `scripts/migrate-phase3-to-e2ee.ts` are gone. ADR 0007 is "executed": every data-bearing API and every read path now goes through the Phase-4 `v1..` envelope (28 call sites across workspace, board, card and card-drawer clients). The `encodeBlob` / `decodeBlob` helpers had zero consumers left. +- **`@otplib/core`** removed as a direct dependency — was unused in `src/`. Stays in the tree as a transitive of `otplib`, so no functional change. ### Fixed -- **Flaky `tests/unit/totp.test.ts` clock-drift cases** — original - wall-clock arithmetic (`Math.floor(Date.now() / 1000) - 31`) jumped - two 30-s steps back instead of one when `Date.now()/1000 mod 30` was - 0 – 1, flaking ~3 % of CI runs. Re-implemented with `vi.useFakeTimers()` - locked to Unix second 1000 (mid-step, well above otplib v13's - non-negative-epoch guardrail). 10× local stress loop now passes - cleanly. Hit Dependabot PRs #32 and #33 before the fix. +- **Flaky `tests/unit/totp.test.ts` clock-drift cases** — original wall-clock arithmetic (`Math.floor(Date.now() / 1000) - 31`) jumped two 30-s steps back instead of one when `Date.now()/1000 mod 30` was 0 – 1, flaking ~3 % of CI runs. Re-implemented with `vi.useFakeTimers()` locked to Unix second 1000 (mid-step, well above otplib v13's non-negative-epoch guardrail). 10× local stress loop now passes cleanly. Hit Dependabot PRs #32 and #33 before the fix. +- **Inline `# comment` on env values in `.env.example`** (#56) — `ATTACHMENT_MAX_BYTES="26214400" # 25 MiB` and `LOG_LEVEL="info" # ...` were copy-pasted into production `.env` files where `systemd EnvironmentFile=` does **not** strip trailing comments, breaking the app's zod env-schema at startup (`Number("26214400 # 25 MB")` = NaN). Both moved to comment-above-value. Discovered live during the first production auto-deploy. + +### Security + +- **`otplib` 12 → 13** — auth-critical API rewrite. The `authenticator` namespace is gone; v13 ships top-level functional exports with the default `verify` now async. `src/lib/auth/totp.ts` deliberately uses `verifySync` instead of the async `verify` — in this path a missed `await` would always evaluate the returned Promise as truthy and silently bypass 2FA. `epochTolerance: 30 s` reproduces the old `window: 1`. Existing `User.totpSecret` rows remain valid; v13's new 16-byte minimum-secret guardrail does not reject the 20-byte secrets our `generateSecret()` produces. +- **`@simplewebauthn/{browser,server}` 11 → 13** — kept in lock-step with the otplib bump even though @simplewebauthn is not yet used in `src/` (reserved for the upcoming WebAuthn phase). Avoids future split-version surprises. ### Tooling -- **`.github/dependabot.yml`** — the `crypto` group (`libsodium*`, - `argon2-browser`, `@simplewebauthn/*`, `otplib`) is now restricted to - `update-types: [minor, patch]`. Major bumps in any auth-critical - package now arrive as individual PRs so they can be reviewed against - `src/lib/auth/*` in isolation, rather than being bundled with - unrelated updates that block each other. +- **`.github/dependabot.yml`** — the `crypto` group (`libsodium*`, `argon2-browser`, `@simplewebauthn/*`, `otplib`) is now restricted to `update-types: [minor, patch]`. Major bumps in any auth-critical package now arrive as individual PRs so they can be reviewed against `src/lib/auth/*` in isolation. +- **`.gitattributes`** — `*.sh` + `*.service` forced to `text eol=lf`. Without this, Windows-side git's default LF→CRLF on checkout would silently break the shebang of `scripts/deploy.sh` on the Linux server with a cryptic `^M: command not found`. ### Deferred (not landed in this sweep) -- **`eslint` 9 → 10** — blocked on upstream `eslint-plugin-react` - (pulled in via `eslint-config-next`) which still uses the - ESLint 9 context API removed in 10. -- **`@vitejs/plugin-react` 5 → 6** — needs Vite 8 as a direct - dependency; we only have Vite 6 transitively today. -- **otplib + @simplewebauthn major bumps re-evaluated quarterly** — - next time they show up as separate Dependabot PRs they get a - dedicated review pass against the auth surface. +- **`eslint` 9 → 10** — blocked on upstream `eslint-plugin-react` (pulled in via `eslint-config-next`) which still uses the ESLint 9 context API removed in 10. +- **`@vitejs/plugin-react` 5 → 6** — needs Vite 8 as a direct dependency; we only have Vite 6 transitively today. +- **Automation triggers `card_label_added` + `comment_added`** — exist in the DSL but their emitters need card-drawer wiring; separate PR. +- **Automation `post_comment` action + `card_due_reached` trigger** — both need the Redis SETNX idempotency claim (post_comment) and the per-minute BullMQ scheduler (card_due_reached) from ADR 0010 §"Conflict resolution". +- **Live cursors in the card description editor** — needs swapping the plain `