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
42 changes: 42 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -430,3 +430,45 @@ jobs:
# make CI green.
- name: Audit (fail on high/critical)
run: pnpm audit --prod --audit-level high

workflow-lint:
name: Workflow files are valid
runs-on: ubuntu-latest
timeout-minutes: 5
# A workflow file with an invalid expression does not degrade gracefully:
# GitHub cannot parse it, so it never resolves `name:` or any job, and it
# records a bare failed run against whatever event fired. `perf-k6.yml`
# sat in exactly that state for weeks — `env.PERF_K6_FULL_RUN` in a
# job-level `if:`, where the `env` context is not available — failing on
# every single push. Nothing was watching, because nothing lints the
# workflows themselves. This job is that watcher.
steps:
- name: Checkout code
uses: actions/checkout@v7

# Pinned by version AND checksum rather than pulling a third-party
# action: one fewer `uses:` SHA to keep current, and the binary is
# verified before it runs.
- name: Install actionlint
env:
ACTIONLINT_VERSION: 1.7.10
ACTIONLINT_SHA256: f4c76b71db5755a713e6055cbb0857ed07e103e028bda117817660ebadb4386f
run: |
set -euo pipefail
curl -fsSL -o actionlint.tar.gz \
"https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz"
echo "${ACTIONLINT_SHA256} actionlint.tar.gz" | sha256sum --check --strict
tar xzf actionlint.tar.gz actionlint
./actionlint --version

# actionlint runs shellcheck over `run:` blocks only when the binary is
# on PATH, and it *skips silently* when it is not — which is how a local
# run can report clean while this job finds five issues. ubuntu-latest
# ships shellcheck, so assert it rather than trust it: if a future runner
# image drops it, the gate would quietly stop checking the `run:` blocks
# that drive Postgres, Redis and the deploy steps.
- name: Assert shellcheck is available
run: shellcheck --version

- name: Lint workflow files
run: ./actionlint
4 changes: 2 additions & 2 deletions .github/workflows/deploy-cms.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ jobs:
run: pnpm --filter @lumibase/cms test

- name: Build CMS
run: pnpm --filter @lumibase/cms run build:${TARGET_ENV}
run: pnpm --filter @lumibase/cms run "build:${TARGET_ENV}"

- name: Check Cloudflare deploy credentials
id: cloudflare-credentials
Expand All @@ -120,7 +120,7 @@ jobs:

- name: Deploy CMS
if: steps.cloudflare-credentials.outputs.can_deploy == 'true'
run: pnpm --filter @lumibase/cms run deploy:${TARGET_ENV}
run: pnpm --filter @lumibase/cms run "deploy:${TARGET_ENV}"

- name: Verify deployment health
if: steps.cloudflare-credentials.outputs.can_deploy == 'true'
Expand Down
15 changes: 10 additions & 5 deletions .github/workflows/perf-k6.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ concurrency:

env:
NODE_VERSION: 24
# Set to "true" in repo/org variables to run the full compose + k6 job nightly.
PERF_K6_FULL_RUN: ${{ vars.PERF_K6_FULL_RUN || 'false' }}

jobs:
validate-scripts:
Expand Down Expand Up @@ -57,10 +55,17 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 45
needs: validate-scripts
# `vars`, not `env`: the `env` context is not available in a job-level `if`
# (only `github`, `inputs`, `needs`, `vars` are). Referencing it there does
# not evaluate to empty — it makes the whole workflow file invalid, which is
# why this file failed instantly on every event for weeks. Set the repo/org
# variable PERF_K6_FULL_RUN to "true" to let the nightly schedule run the
# full compose + k6 job; unset means the scheduled run stops after
# validate-scripts.
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'pull_request' && github.event.label.name == 'perf-k6') ||
(github.event_name == 'schedule' && env.PERF_K6_FULL_RUN == 'true')
(github.event_name == 'schedule' && vars.PERF_K6_FULL_RUN == 'true')
steps:
- uses: actions/checkout@v7

Expand All @@ -77,7 +82,7 @@ jobs:

- name: Wait for Postgres
run: |
for i in $(seq 1 30); do
for _ in $(seq 1 30); do
docker compose -f docker/docker-compose.yml exec -T postgres pg_isready -U lumibase && exit 0
sleep 2
done
Expand Down Expand Up @@ -109,7 +114,7 @@ jobs:
PORT: '1989'
run: |
pnpm -F @lumibase/cms exec tsx src/serve.ts &
for i in $(seq 1 60); do
for _ in $(seq 1 60); do
curl -sf http://127.0.0.1:1989/health && exit 0
sleep 2
done
Expand Down
8 changes: 5 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,11 @@ jobs:
range="$tag"
echo "Changes included in ${tag}:" > release-notes.md
fi
echo >> release-notes.md
git log --pretty=format:'- %s (%h)' "$range" >> release-notes.md
echo >> release-notes.md
{
echo
git log --pretty=format:'- %s (%h)' "$range"
echo
} >> release-notes.md
fi

- name: Create GitHub Release
Expand Down
3 changes: 2 additions & 1 deletion .kiro/steering/out-of-scope-backlog.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,6 @@
| B29 | 2026-08-24 · landing/docs GA consent | task (gap · blocked) | `apps/marketplace` (submodule private `lumibase-ai/marketplace`) | Yêu cầu ban đầu là bật GA4-sau-consent cho **cả ba** site public: landing, docs, marketplace. Hai site đầu đã xong trong PR này; marketplace **không làm được từ repo này** — submodule chưa checkout (`git submodule status` cho `-5958c50e…`, dir chỉ còn `.next`/`out`/`node_modules` cũ, không có `.git` lẫn `src`) và remote không đọc được bằng cả hai account đang login (`git ls-remote` qua SSH và HTTPS đều trả `Repository not found`; `gh repo view lumibase-ai/marketplace` → `Could not resolve to a Repository`). Hệ quả: `marketplace.lumibase.dev` (hoặc domain tương ứng) vẫn **không có** banner consent lẫn control opt-out, trong khi privacy policy ở landing giờ đã mô tả cơ chế consent như đặc tính chung của các site public → policy nói rộng hơn thực tế đúng một site | medium | `open` | Sửa trong repo `lumibase-ai/marketplace`: nó là Next app (build ra `apps/marketplace/out`) nên copy nguyên mẫu của `apps/landing` — `<Analytics>` + `<CookiePreferences>` dùng `@lumibase/analytics-consent`. **Lưu ý ràng buộc:** marketplace bị loại khỏi pnpm workspace (`pnpm-workspace.yaml: "!apps/marketplace"`) và install bằng `--ignore-workspace`, nên KHÔNG dùng được `workspace:*` — hoặc publish package này lên registry, hoặc copy logic kèm chú thích trỏ về `packages/analytics-consent` làm nguồn. Cần thêm `NEXT_PUBLIC_GA_ID` vào step build marketplace ở `release.yml` + `pages-deploy.yml` (hiện chỉ truyền cho step build của landing/docs) |
| B30 | 2026-08-30 · rescue blog SSRF khỏi worktree cũ | bug (flake) | `apps/docs/src/components/__tests__/analytics-consent.test.tsx` (`beforeEach` dòng 34) | Cả 9 test của file đỏ với `TypeError: Cannot read properties of undefined (reading 'clear')` tại `localStorage.clear()` — kèm warning `localStorage is not available because --localstorage-file was not provided`, tức global `localStorage` bị resolve về builtin experimental của **Node 24** (undefined) thay vì `localStorage` của jsdom. Đo được **1/2 lần** chạy `pnpm test` (turbo, 12 package song song) trên `main` sạch tại `b124d953`; ngay sau đó: file chạy riêng **pass** (9/9), full suite `apps/docs` chạy riêng **pass** (24 file · 157 test), và `pnpm test` toàn repo chạy lại **pass** (12/12 task). Không tái lập theo yêu cầu ⇒ flake nhạy tải, không phải regression. Cùng class với B13 nhưng nguyên nhân khác: đây là **global bị thiếu**, không phải timeout của `findBy*`. File do PR #426 (GA consent) thêm, nên chưa từng chạy qua nhiều chu kỳ CI | medium | `open` | Hệ quả: pre-commit hook (`husky` → `check:all` + `pnpm test`) và CI job `test` đỏ ngẫu nhiên, chặn commit ở máy local dù diff không liên quan (gặp khi commit 3 file `blog/`). Fix: không dựa vào global `localStorage` của môi trường — stub tường minh trong `beforeEach` (hoặc `vi.stubGlobal('localStorage', …)`), hoặc set `environmentOptions`/`pool` cho `apps/docs` sao cho jsdom luôn thắng builtin của Node 24. Nếu đã chuẩn hoá Node 24 toàn team thì rà cả các test khác đọc `localStorage`/`sessionStorage` trực tiếp — cùng một class |
| B33 | 2026-08-30 · rà batch dependabot (#411–#421) | bug (guard chết) | `scripts/version-check.mjs` | Script **không được gọi từ đâu cả** — không có trong `scripts` của `package.json` (`pnpm version:check` chạy `sync-version.mjs --check`, một file khác), không trong workflow nào, không trong `.husky/pre-commit` — và nếu chạy thì nó **fail**: nó assert `engines.node === '>=20'` trong khi root khai `>=22.13.0` (nay là `^22.22.2 \|\| ^24.15.0 \|\| >=26.0.0`). Nó cũng assert `packageManager` và `lockfileVersion`. Hệ quả: hàng rào cơ giới duy nhất canh `engines.node` không hề chạy — phát hiện đúng lúc jsdom 30 nâng sàn Node và không gate nào nói gì | medium | `open` | Chọn một trong hai, đừng để lửng: (a) nối vào `check:all` + đổi expectation `engines.node` thành hằng đọc từ chính `package.json` hay một sàn khai tường minh, hoặc (b) xoá file và thừa nhận `sync-version.mjs` là guard duy nhất. Nếu chọn (a) thì đây là chỗ đúng để cơ giới hoá DoD §2e: so `engines.node` của repo với `engines.node` của mọi toolchain đã cài, thay vì rà tay |
| B31 | 2026-08-30 · rà batch dependabot (#411–#421) | bug (CI) | `.github/workflows/perf-k6.yml` | Workflow đỏ ở **mọi** commit push lên `main` — kiểm 30 run gần nhất (2026-08-29 → 08-30), không có một `success` nào, kể cả trên `b124d953` (trước batch này). Không phải do batch. Vì nó không phải required check nên không chặn merge, nhưng một workflow đỏ vĩnh viễn làm mờ tín hiệu: người ta học cách bỏ qua màu đỏ, đúng lúc một cái đỏ thật xuất hiện. Liên quan `v1-release-criteria.md` §7 (k6 baseline "nên có, không chặn tag") và các task k6 đang hoãn của `high-load-cache-readiness` | low | `open` | Đọc log để biết nó thiếu môi trường tải hay thật sự vỡ. Rồi hoặc sửa, hoặc chuyển sang `workflow_dispatch`/schedule để nó không đỏ trên mọi push — đừng để nguyên trạng |
| B31 | 2026-08-30 · rà batch dependabot (#411–#421) | bug (CI) | `.github/workflows/perf-k6.yml` | Workflow đỏ ở **mọi** commit push lên `main` — kiểm 30 run gần nhất (2026-08-29 → 08-30), không có một `success` nào, kể cả trên `b124d953` (trước batch này). Không phải do batch. Vì nó không phải required check nên không chặn merge, nhưng một workflow đỏ vĩnh viễn làm mờ tín hiệu: người ta học cách bỏ qua màu đỏ, đúng lúc một cái đỏ thật xuất hiện. Liên quan `v1-release-criteria.md` §7 (k6 baseline "nên có, không chặn tag") và các task k6 đang hoãn của `high-load-cache-readiness` | low | `fixed` | **Nguyên nhân không phải thiếu môi trường tải mà là file workflow không parse được.** `on:` của nó vốn KHÔNG khai `push` (chỉ `workflow_dispatch` / `schedule` / `pull_request: [labeled]`), nhưng job `perf-gate` dùng `env.PERF_K6_FULL_RUN` trong `if:` **cấp job**, nơi context `env` không tồn tại (chỉ có `github`, `inputs`, `needs`, `vars`). Biểu thức sai không "đánh giá thành rỗng" — nó làm **cả file invalid**: GitHub không parse được nên không resolve nổi `name:` (API trả `name` = đúng đường dẫn file, dấu hiệu nhận biết), không tạo job nào, và ghi một run failed cho **bất kỳ** event nào, kể cả event mà `on:` không khai. Fix = `vars.PERF_K6_FULL_RUN`; sau đó workflow tôn trọng `on:` và không còn chạy trên push. Verify bằng `actionlint` (0 finding trên toàn bộ `.github/workflows`, gồm cả shellcheck). Chống tái diễn cả class bằng job CI mới `workflow-lint` chạy `actionlint` (pin version + sha256) — vì lý do thật sự khiến nó sống được nhiều tuần là **không có gì lint chính các workflow** |
| B32 | 2026-08-30 · rà batch dependabot (#411–#421) | bug (flake · class) | `.husky/pre-commit` ↔ `pnpm test` (turbo concurrency mặc định) | Nối tiếp B13: nâng `testTimeout` lên 15s **không** đóng được class, chỉ nâng trần. Đo tại commit này: `pnpm test` ở concurrency mặc định của turbo cho 6–8 fail rải khắp `apps/studio` (`field-inspector`, `fields-tab`, `materialize-page`, `marketplace-publish`, `security-audit-tab`, `setup-state-gate`, `mission-control`, `backup-code-page`) và `apps/cms` (`flow-service`, `backup-codes-persister`, `recovery/service`, `audit/routes`) — **tất cả** đều là `Test timed out in 15000ms`, tức là chạm đúng cái trần B13 vừa nâng. Cùng bộ đó với `TURBO_CONCURRENCY=1` thì **12/12 task xanh, 0 fail**; chạy từng package riêng cũng xanh. Nguyên nhân là tranh tài nguyên, không phải test sai. Nguy ở chỗ `.husky/pre-commit` gọi đúng `pnpm test`: commit hợp lệ bị chặn ngẫu nhiên, và cách chữa tự nhiên nhất mà người ta sẽ chọn là `--no-verify` — tắt luôn hàng rào. Đã gặp thật trong lần commit này | medium | `open` | Đừng nâng `testTimeout` lần nữa (đó là cách B13 thất bại). Giới hạn song song thay vì giới hạn thời gian: đặt `concurrency` cho task `test` trong `turbo.json` hoặc `TURBO_CONCURRENCY` trong `.husky/pre-commit`, và/hoặc hạ `poolOptions.maxThreads` cho hai suite jsdom nặng nhất. Cân nhắc cho pre-commit chạy tập bị ảnh hưởng thay vì toàn bộ suite, để hàng rào không đắt tới mức bị vòng qua |
| B34 | 2026-08-30 · rebase #441 lên main | bug (class) | `.kiro/steering/out-of-scope-backlog.md` ↔ `scripts/check-registry-numbering.mjs` | ID của **chính bảng này** không được kiểm trùng. #434 và #436 cùng cấp `B30` cho hai finding không liên quan (flake `localStorage` của `analytics-consent.test.tsx` vs guard chết `version-check.mjs`); PR merge sau phải renumber tay thành `B33`, và va chạm chỉ lộ ra dưới dạng conflict lúc rebase — không gate nào nói gì. Đúng cùng class mà `registry:check` đã cơ giới hoá cho cột `#` của Setup Impact Registry (từng có hai dòng #20/#31/#32), chỉ khác là bảng này bị bỏ sót. Nguy hơn vẻ ngoài vì ID backlog được **trích dẫn theo số** ở dòng khác ("Nối tiếp B13", "cùng class với B10", "Xem B24") và trong CHANGELOG, nên một lần renumber im lặng làm gãy tham chiếu mà không test nào phủ | medium | `fixed` | Mở rộng `scripts/check-registry-numbering.mjs` để kiểm **cả hai** bảng (`#` của setup-impact và `B<n>` của file này), báo lỗi kèm số kế tiếp an toàn và nhắc giữ lại occurrence đang được dòng khác trích dẫn. Đã verify hai chiều: chèn `B30` trùng → exit 1 đúng dòng; bỏ ra → exit 0. Chạy sẵn trong `pnpm check:all` (và do đó cả pre-commit + CI) nên không cần thêm entry mới |
37 changes: 37 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ Source: [github.com/khuepm/lumibase](https://github.com/khuepm/lumibase) · Webs
while `apps/docs` declared `shiki@^1.22.0` — so `MarkdownRenderer` was
handing a 1.x `Highlighter` to a 4.x rehype plugin. The docs test suite
mocks shiki, so nothing caught it; the pair is now on one major.
- **`@types/react-dom` override raised `19.2.4` → `19.2.5`.** The pin is exact,
so it wins over whatever the manifests declare — which is why the pending
minor-and-patch group bump (manifests to `^19.2.5`) failed `drift:check`: the
two ranges no longer intersected. Raising the override first clears that
without the group PR having to touch it, and `19.2.5` still satisfies today's
`^19.2.4` declarations, so this is a no-op for anyone not on the group bump.
- **`engines.node` raised to `^22.22.2 || ^24.15.0 || >=26.0.0`** (was
`>=22.13.0`), the floor jsdom 30 requires. The previous range admitted Node
22.13–22.22.1, 23.x and 24.0–24.14, all of which jsdom 30 rejects; nanoid 6
Expand All @@ -109,6 +115,37 @@ Source: [github.com/khuepm/lumibase](https://github.com/khuepm/lumibase) · Webs

### Fixed

- **`perf-k6.yml` was an invalid workflow file, failing on every push for
weeks.** Its `on:` never declared `push` at all — the failures were not the
load-test job running and breaking. The `perf-gate` job's `if:` referenced
`env.PERF_K6_FULL_RUN`, and the `env` context does not exist in a job-level
`if:` (only `github`, `inputs`, `needs`, `vars`). That does not evaluate to
empty; it makes the whole file unparseable, so GitHub never resolved `name:`
or any job and recorded a bare failed run against every event, `on:` filters
included. The giveaway was the API reporting the run's `name` as the file path
with an empty job list. Switched to `vars.PERF_K6_FULL_RUN`; the workflow now
honours `on:` and no longer runs on push. Set the repo variable
`PERF_K6_FULL_RUN=true` to let the nightly schedule run the full compose + k6
job. Closes backlog `B31`.
- **`registry:check` now also guards the out-of-scope backlog's `ID` column,**
not just the Setup Impact Registry's `#` column. Two PRs claimed `B30` for
unrelated findings and the collision only surfaced as a rebase conflict, which
is the same failure the `#` guard already existed to prevent — the backlog
table had simply been left out. It matters more than it looks: backlog ids are
cited by id from other rows ("Nối tiếp B13") and from CHANGELOG entries, so a
silent renumber breaks references nothing tests. Closes `B34`.
- **CI now lints the workflow files themselves** (`workflow-lint` job running
`actionlint`, pinned by version and SHA-256 rather than adding another
third-party action to keep current). The reason a broken workflow could stay
broken for weeks is that nothing checked this class at all: an unparseable
workflow looks, from the outside, exactly like a workflow that ran and failed.
`actionlint` reports zero findings across `.github/workflows`, shellcheck
included over the `run:` blocks that drive Postgres, Redis and the deploy
steps — which required fixing five pre-existing findings it surfaced on first
run: two unquoted `${TARGET_ENV}` expansions in `deploy-cms.yml` (SC2086), two
unused loop variables in `perf-k6.yml` (SC2034), and a run of individual
redirects in `release.yml` (SC2129). The job also asserts shellcheck is on
PATH, because actionlint skips it *silently* when it is absent.
- **The TOTP endpoints are actually reachable.** All six of them answered
`404 NOT_FOUND` against a running server. `index.ts` attached the sub-routers
*after* mounting their parents (`api.route('/auth', authRouter)` then
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
"pnpm": {
"overrides": {
"@types/react": "19.2.18",
"@types/react-dom": "19.2.4",
"@types/react-dom": "19.2.5",
"brace-expansion@1": "^1.1.16",
"brace-expansion@5": "^5.0.8",
"dompurify": "^3.4.13",
Expand Down
Loading