From 7b6a6fa3a14d21bf8b4fe42da5b312d87fe42973 Mon Sep 17 00:00:00 2001 From: Javier Date: Mon, 31 Aug 2026 07:47:20 +0700 Subject: [PATCH 1/3] fix(ci): perf-k6 was an invalid workflow file, and nothing lints workflows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `perf-k6.yml` had failed on every push for weeks. It was not the load-test job running and breaking — its `on:` never declared `push` at all. 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 entire file unparseable, so GitHub never resolved `name:` or any job and recorded a bare failed run against every event, `on:` filters included. The tell was the API reporting the run's `name` as the file path with an empty job list. Fixed by using `vars.PERF_K6_FULL_RUN`, which is available at job level. The workflow now honours `on:` and stops running on push. Set the repo variable `PERF_K6_FULL_RUN=true` to let the nightly schedule run the full compose + k6 job; unset stops the scheduled run after `validate-scripts`. Dropped the workflow-level `env` entry, which nothing else read. The more useful fix is the second one. The reason a broken workflow stayed broken for weeks is that nothing checked this class: an unparseable workflow looks, from the outside, identical to a workflow that ran and failed — and because it is not a required check, no gate objected. New `workflow-lint` job runs `actionlint`, pinned by version and SHA-256 rather than adding another third-party action SHA to keep current. Zero findings across `.github/workflows` today, shellcheck included over the `run:` blocks that drive Postgres, Redis and the deploy steps. Not adding a DoD section for this: the guard is mechanical and self-enforcing, which DoD 6 prefers over a checklist item. Also raises the `@types/react-dom` override from `19.2.4` to `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`) fails `drift:check`: the ranges stopped intersecting. Raising the override first clears that without the group PR touching it, and `19.2.5` still satisfies the current `^19.2.4` declarations, so it changes nothing for anyone not on the group bump. Verified in the lockfile: every importer now resolves 19.2.5, and the whole lockfile diff is that propagation. Backlog B31 closes with the root cause recorded. actionlint 0 findings, `pnpm check:all` green, typecheck 17/17, build 9/9, `pnpm audit --prod --audit-level high` clean, `pnpm install --frozen-lockfile` verified against a fresh checkout of main. --- .github/workflows/ci.yml | 36 +++++++++ .github/workflows/perf-k6.yml | 11 ++- .kiro/steering/out-of-scope-backlog.md | 2 +- CHANGELOG.md | 26 +++++++ package.json | 2 +- pnpm-lock.yaml | 102 ++++++++++++------------- pnpm-workspace.yaml | 2 +- 7 files changed, 124 insertions(+), 57 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 447624758..d5cfb0407 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -430,3 +430,39 @@ 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 + + # shellcheck/pyflakes are left on: `run:` blocks here drive Postgres, + # Redis and deploy steps, and a quoting bug in one of those is the same + # class of silent breakage. + - name: Lint workflow files + run: ./actionlint diff --git a/.github/workflows/perf-k6.yml b/.github/workflows/perf-k6.yml index b998f19f7..8c3611e9a 100644 --- a/.github/workflows/perf-k6.yml +++ b/.github/workflows/perf-k6.yml @@ -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: @@ -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 diff --git a/.kiro/steering/out-of-scope-backlog.md b/.kiro/steering/out-of-scope-backlog.md index 7b77cfaae..f801752de 100644 --- a/.kiro/steering/out-of-scope-backlog.md +++ b/.kiro/steering/out-of-scope-backlog.md @@ -52,5 +52,5 @@ | 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` — `` + `` 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 | diff --git a/CHANGELOG.md b/CHANGELOG.md index d70c70c89..6e22f4f7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -109,6 +115,26 @@ 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`. +- **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` today, with + shellcheck enabled over the `run:` blocks that drive Postgres, Redis and the + deploy steps. - **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 diff --git a/package.json b/package.json index f16784b1e..3efb6792e 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1e17842de..8062bbeeb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,7 +6,7 @@ settings: 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 @@ -169,8 +169,8 @@ importers: specifier: 19.2.18 version: 19.2.18 '@types/react-dom': - specifier: 19.2.4 - version: 19.2.4(@types/react@19.2.18) + specifier: 19.2.5 + version: 19.2.5(@types/react@19.2.18) eslint: specifier: ^10 version: 10.8.1(jiti@2.7.0) @@ -246,13 +246,13 @@ importers: version: 7.0.1(@testing-library/dom@10.4.1)(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.2.0)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12))) '@testing-library/react': specifier: ^16.0.1 - version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@types/react': specifier: 19.2.18 version: 19.2.18 '@types/react-dom': - specifier: 19.2.4 - version: 19.2.4(@types/react@19.2.18) + specifier: 19.2.5 + version: 19.2.5(@types/react@19.2.18) '@vitejs/plugin-react': specifier: ^6.1.0 version: 6.1.0(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)) @@ -318,8 +318,8 @@ importers: specifier: 19.2.18 version: 19.2.18 '@types/react-dom': - specifier: 19.2.4 - version: 19.2.4(@types/react@19.2.18) + specifier: 19.2.5 + version: 19.2.5(@types/react@19.2.18) autoprefixer: specifier: ^10.5.4 version: 10.5.4(postcss@8.5.26) @@ -404,7 +404,7 @@ importers: version: 2.11.1 '@xyflow/react': specifier: ^12.11.3 - version: 12.11.3(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + version: 12.11.3(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) class-variance-authority: specifier: ^0.7.0 version: 0.7.1 @@ -413,7 +413,7 @@ importers: version: 2.1.1 cmdk: specifier: ^1.0.0 - version: 1.1.1(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + version: 1.1.1(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) dompurify: specifier: ^3.4.13 version: 3.4.13 @@ -459,13 +459,13 @@ importers: version: 7.0.1(@testing-library/dom@10.4.1)(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.2.0)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12))) '@testing-library/react': specifier: ^16.0.1 - version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@types/react': specifier: 19.2.18 version: 19.2.18 '@types/react-dom': - specifier: 19.2.4 - version: 19.2.4(@types/react@19.2.18) + specifier: 19.2.5 + version: 19.2.5(@types/react@19.2.18) '@types/zxcvbn': specifier: ^4.4.5 version: 4.4.5 @@ -1747,7 +1747,7 @@ packages: resolution: {integrity: sha512-TDTYmpdq8dI2+Xgvgj9AJ8Ghqq+Eph/TRVEdaFQPDItIY+6QSkU7MJMeevw1568Yw/2Ijz8BTphPSP2XejKphw==} peerDependencies: '@types/react': 19.2.18 - '@types/react-dom': 19.2.4 + '@types/react-dom': 19.2.5 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1760,7 +1760,7 @@ packages: resolution: {integrity: sha512-2v+zNAWWe0ySxgC0D0yeXMPQ23xZVgXZTerTz+JKlmdRj6gfTqmCcR29jb6d290DezXPGgruHWDX/vYUebtErg==} peerDependencies: '@types/react': 19.2.18 - '@types/react-dom': 19.2.4 + '@types/react-dom': 19.2.5 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1782,7 +1782,7 @@ packages: resolution: {integrity: sha512-Fas/lXQqhVvqwAb64s5RFeHiHYElZ6SUQbZaNd6EkfhP/Al7wTIQ9WIR4QVX475tlu5yFCEdDcJH6/UwsZjMWw==} peerDependencies: '@types/react': 19.2.18 - '@types/react-dom': 19.2.4 + '@types/react-dom': 19.2.5 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1804,7 +1804,7 @@ packages: resolution: {integrity: sha512-m309havGzsjLHHaIX50G5PlvRs3xkgPCsGk/5PTvYm8D5q33yG0J7w/712PTOhid7NTaFETtnSXjngHQavvhVw==} peerDependencies: '@types/react': 19.2.18 - '@types/react-dom': 19.2.4 + '@types/react-dom': 19.2.5 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1817,7 +1817,7 @@ packages: resolution: {integrity: sha512-zdTk4PlUO0E18HnZ3wYbW0KkJJxWCdiNYp6g6X1PtONFhxVkg01vliTJAmwIszU6mHiyBOoW9P0rAugl5/hULQ==} peerDependencies: '@types/react': 19.2.18 - '@types/react-dom': 19.2.4 + '@types/react-dom': 19.2.5 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1830,7 +1830,7 @@ packages: resolution: {integrity: sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g==} peerDependencies: '@types/react': 19.2.18 - '@types/react-dom': 19.2.4 + '@types/react-dom': 19.2.5 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -2453,7 +2453,7 @@ packages: peerDependencies: '@testing-library/dom': ^10.0.0 '@types/react': 19.2.18 - '@types/react-dom': 19.2.4 + '@types/react-dom': 19.2.5 react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 peerDependenciesMeta: @@ -2564,8 +2564,8 @@ packages: '@types/prompts@2.4.9': resolution: {integrity: sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==} - '@types/react-dom@19.2.4': - resolution: {integrity: sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==} + '@types/react-dom@19.2.5': + resolution: {integrity: sha512-fMPwH9v7r/pp43yUd2/Mbiex5KouJwwR3dzHkhLREUC6764VyDsqxhAxv6OFEYR1RhjOyD1naqba8ECDBe7ZQg==} peerDependencies: '@types/react': 19.2.18 @@ -2832,7 +2832,7 @@ packages: resolution: {integrity: sha512-G3jogHz2GWUtIOkhavUGno2YzY9u6fILIJBttfsBendb0/HWB90JG+sOTAvlIMEwyvq9zgy9V9ZQSwyQjR5QzQ==} peerDependencies: '@types/react': 19.2.18 - '@types/react-dom': 19.2.4 + '@types/react-dom': 19.2.5 react: '>=17' react-dom: '>=17' peerDependenciesMeta: @@ -6839,18 +6839,18 @@ snapshots: optionalDependencies: '@types/react': 19.2.18 - '@radix-ui/react-dialog@1.1.17(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-dialog@1.1.17(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@radix-ui/primitive': 1.1.4 '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.18)(react@19.2.8) '@radix-ui/react-context': 1.1.4(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-dismissable-layer': 1.1.13(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-dismissable-layer': 1.1.13(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-focus-scope': 1.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-focus-scope': 1.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-id': 1.1.2(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-portal': 1.1.12(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-portal': 1.1.12(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-slot': 1.3.0(@types/react@19.2.18)(react@19.2.8) '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.18)(react@19.2.8) aria-hidden: 1.2.6 @@ -6859,20 +6859,20 @@ snapshots: react-remove-scroll: 2.7.2(@types/react@19.2.18)(react@19.2.8) optionalDependencies: '@types/react': 19.2.18 - '@types/react-dom': 19.2.4(@types/react@19.2.18) + '@types/react-dom': 19.2.5(@types/react@19.2.18) - '@radix-ui/react-dismissable-layer@1.1.13(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-dismissable-layer@1.1.13(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@radix-ui/primitive': 1.1.4 '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.18)(react@19.2.8) '@radix-ui/react-use-escape-keydown': 1.1.2(@types/react@19.2.18)(react@19.2.8) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) optionalDependencies: '@types/react': 19.2.18 - '@types/react-dom': 19.2.4(@types/react@19.2.18) + '@types/react-dom': 19.2.5(@types/react@19.2.18) '@radix-ui/react-focus-guards@1.1.4(@types/react@19.2.18)(react@19.2.8)': dependencies: @@ -6880,16 +6880,16 @@ snapshots: optionalDependencies: '@types/react': 19.2.18 - '@radix-ui/react-focus-scope@1.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-focus-scope@1.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.18)(react@19.2.8) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) optionalDependencies: '@types/react': 19.2.18 - '@types/react-dom': 19.2.4(@types/react@19.2.18) + '@types/react-dom': 19.2.5(@types/react@19.2.18) '@radix-ui/react-id@1.1.2(@types/react@19.2.18)(react@19.2.8)': dependencies: @@ -6898,33 +6898,33 @@ snapshots: optionalDependencies: '@types/react': 19.2.18 - '@radix-ui/react-portal@1.1.12(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-portal@1.1.12(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.18)(react@19.2.8) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) optionalDependencies: '@types/react': 19.2.18 - '@types/react-dom': 19.2.4(@types/react@19.2.18) + '@types/react-dom': 19.2.5(@types/react@19.2.18) - '@radix-ui/react-presence@1.1.6(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-presence@1.1.6(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.18)(react@19.2.8) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) optionalDependencies: '@types/react': 19.2.18 - '@types/react-dom': 19.2.4(@types/react@19.2.18) + '@types/react-dom': 19.2.5(@types/react@19.2.18) - '@radix-ui/react-primitive@2.1.6(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-primitive@2.1.6(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@radix-ui/react-slot': 1.3.0(@types/react@19.2.18)(react@19.2.8) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) optionalDependencies: '@types/react': 19.2.18 - '@types/react-dom': 19.2.4(@types/react@19.2.18) + '@types/react-dom': 19.2.5(@types/react@19.2.18) '@radix-ui/react-slot@1.3.0(@types/react@19.2.18)(react@19.2.8)': dependencies: @@ -7414,7 +7414,7 @@ snapshots: optionalDependencies: vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.2.0)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)) - '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@babel/runtime': 7.29.2 '@testing-library/dom': 10.4.1 @@ -7422,7 +7422,7 @@ snapshots: react-dom: 19.2.8(react@19.2.8) optionalDependencies: '@types/react': 19.2.18 - '@types/react-dom': 19.2.4(@types/react@19.2.18) + '@types/react-dom': 19.2.5(@types/react@19.2.18) '@turbo/darwin-64@2.10.11': optional: true @@ -7522,7 +7522,7 @@ snapshots: '@types/node': 26.2.0 kleur: 3.0.3 - '@types/react-dom@19.2.4(@types/react@19.2.18)': + '@types/react-dom@19.2.5(@types/react@19.2.18)': dependencies: '@types/react': 19.2.18 @@ -7785,7 +7785,7 @@ snapshots: '@whatwg-node/promise-helpers': 1.3.2 tslib: 2.8.1 - '@xyflow/react@12.11.3(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@xyflow/react@12.11.3(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@xyflow/system': 0.0.80 classcat: 5.0.5 @@ -7794,7 +7794,7 @@ snapshots: zustand: 4.5.7(@types/react@19.2.18)(react@19.2.8) optionalDependencies: '@types/react': 19.2.18 - '@types/react-dom': 19.2.4(@types/react@19.2.18) + '@types/react-dom': 19.2.5(@types/react@19.2.18) transitivePeerDependencies: - immer @@ -8070,12 +8070,12 @@ snapshots: cluster-key-slot@1.1.1: {} - cmdk@1.1.1(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + cmdk@1.1.1(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-dialog': 1.1.17(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-dialog': 1.1.17(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-id': 1.1.2(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) transitivePeerDependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 826936fa8..2d14b5994 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -24,7 +24,7 @@ packages: 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" From f3d9a69fe369639a5c1455d97f72e721e6da764c Mon Sep 17 00:00:00 2001 From: Javier Date: Mon, 31 Aug 2026 07:58:43 +0700 Subject: [PATCH 2/3] fix(ci): close the five shellcheck findings the new workflow gate surfaced MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `workflow-lint` job failed on its first real run, and the failure was correct. actionlint runs shellcheck over `run:` blocks only when the binary is on PATH and **skips it silently** otherwise — shellcheck was not installed on my machine, so the local run reported clean while the runner (ubuntu-latest ships it) found five pre-existing issues. Installed shellcheck 0.11.0 locally, reproduced all five, fixed them rather than lowering the gate: - `deploy-cms.yml` — `run: pnpm ... run build:${TARGET_ENV}` and the matching `deploy:` step, both unquoted (SC2086). Behaviourally a no-op today because TARGET_ENV is `production`/`staging`, but it is the exact class of quoting bug the comment on this job claims to care about. - `perf-k6.yml` — two `for i in $(seq ...)` readiness loops that never use `i` (SC2034), now `for _`. - `release.yml` — three consecutive `>> release-notes.md` redirects (SC2129), now one grouped `{ ... } >> release-notes.md`. Output is byte-identical: `git log --pretty=format:` emits no trailing newline, so the closing `echo` is still there. Also makes the gate unable to weaken quietly: the job now asserts `shellcheck --version` before running actionlint. Without that, a future runner image dropping shellcheck would silently stop checking every `run:` block — which is the same failure mode as B30, and I just demonstrated it on myself. actionlint exit 0 across `.github/workflows` with shellcheck actually present. --- .github/workflows/ci.yml | 12 +++++++++--- .github/workflows/deploy-cms.yml | 4 ++-- .github/workflows/perf-k6.yml | 4 ++-- .github/workflows/release.yml | 8 +++++--- CHANGELOG.md | 10 +++++++--- 5 files changed, 25 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d5cfb0407..467398b42 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -461,8 +461,14 @@ jobs: tar xzf actionlint.tar.gz actionlint ./actionlint --version - # shellcheck/pyflakes are left on: `run:` blocks here drive Postgres, - # Redis and deploy steps, and a quoting bug in one of those is the same - # class of silent breakage. + # 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 diff --git a/.github/workflows/deploy-cms.yml b/.github/workflows/deploy-cms.yml index 2ae61915d..350f8fe91 100644 --- a/.github/workflows/deploy-cms.yml +++ b/.github/workflows/deploy-cms.yml @@ -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 @@ -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' diff --git a/.github/workflows/perf-k6.yml b/.github/workflows/perf-k6.yml index 8c3611e9a..b26d55c78 100644 --- a/.github/workflows/perf-k6.yml +++ b/.github/workflows/perf-k6.yml @@ -82,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 @@ -114,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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4f0fc200f..10520d94d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e22f4f7a..665ad74c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -132,9 +132,13 @@ Source: [github.com/khuepm/lumibase](https://github.com/khuepm/lumibase) · Webs 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` today, with - shellcheck enabled over the `run:` blocks that drive Postgres, Redis and the - deploy steps. + `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 From 5e72dbab815b93c8f2b23fcc175c416c15ff6d55 Mon Sep 17 00:00:00 2001 From: Javier Date: Mon, 31 Aug 2026 10:20:26 +0700 Subject: [PATCH 3/3] chore(guards): registry:check now covers the backlog ID column too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rebasing this branch hit a conflict because #434 and #436 both assigned `B30`, to unrelated findings — a `localStorage` flake in `analytics-consent.test.tsx` and the dead `version-check.mjs` guard. Whichever merged second had to be renumbered by hand (`B33`), and nothing announced the collision; it showed up only as a rebase conflict. That is precisely the failure `registry:check` already prevents for the `#` column of the Setup Impact Registry, which once carried duplicate #20/#31/#32 rows. The backlog table was simply never included. It matters more than a cosmetic id clash: backlog ids are referenced *by id* from other rows ("Nối tiếp B13", "cùng class với B10", "Xem B24") and from CHANGELOG entries, so a silent renumber breaks cross-references that no test covers. `check-registry-numbering.mjs` now walks both tables — `#` in setup-impact.md and `B` in out-of-scope-backlog.md — reporting the offending line numbers, the next safe id, and a reminder to keep whichever occurrence other rows cite. Both tables still fail closed if their shape changes and the scan parses zero rows, so the guard cannot quietly stop guarding (the B30/B33 lesson, applied to the guard itself). Verified both directions: injecting a duplicate `B30` exits 1 naming lines 53 and 56 and suggesting B34; removing it exits 0. Already reachable through `pnpm check:all`, so pre-commit and CI pick it up with no wiring change. Logged as B34 (fixed). --- .kiro/steering/out-of-scope-backlog.md | 1 + CHANGELOG.md | 7 ++ scripts/check-registry-numbering.mjs | 160 ++++++++++++++++--------- 3 files changed, 112 insertions(+), 56 deletions(-) diff --git a/.kiro/steering/out-of-scope-backlog.md b/.kiro/steering/out-of-scope-backlog.md index f801752de..b37487f13 100644 --- a/.kiro/steering/out-of-scope-backlog.md +++ b/.kiro/steering/out-of-scope-backlog.md @@ -54,3 +54,4 @@ | 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 | `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` 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 | diff --git a/CHANGELOG.md b/CHANGELOG.md index 665ad74c6..a0f4f6d07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -127,6 +127,13 @@ Source: [github.com/khuepm/lumibase](https://github.com/khuepm/lumibase) · Webs 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 diff --git a/scripts/check-registry-numbering.mjs b/scripts/check-registry-numbering.mjs index 69ba72ff9..1b91194fc 100644 --- a/scripts/check-registry-numbering.mjs +++ b/scripts/check-registry-numbering.mjs @@ -2,77 +2,125 @@ import { readFile } from 'node:fs/promises'; import path from 'node:path'; /** - * Registry-numbering tripwire. + * Registry-id tripwire. * - * The `#` column of the Setup Impact Registry - * (`.kiro/specs/admin-setup-wizard/setup-impact.md`) is a unique row id. - * Parallel feature branches kept picking "the next number" independently and - * collided — at one point the table carried two #20/#31/#32 rows (and many - * more). Until now the only fence was a manual `grep` in the Definition of - * Done (§2), i.e. a checklist a reviewer had to remember to run. + * Two tables in this repo use a hand-assigned unique row id, and both have been + * bitten by the same thing: parallel branches independently pick "the next + * number", and whichever merges second silently duplicates it. * - * Per DoD §6 ("cơ giới hóa" — mechanize the fence, don't rely on human - * recall), this script fails CI when the `#` column contains a duplicate, so - * the collision is caught at PR time instead of during a later cleanup. + * 1. Setup Impact Registry (`.kiro/specs/admin-setup-wizard/setup-impact.md`) + * — `#` column. At one point the table carried two #20/#31/#32 rows (and + * many more). That is what this script was originally written for. * - * It parses ONLY the table under the `## Registry` heading, so the numbered - * prose list above it and the notes below it never register as rows. + * 2. Out-of-scope findings backlog (`.kiro/steering/out-of-scope-backlog.md`) + * — `ID` column (`B`). Added after PR #434 and PR #436 both claimed + * `B30` for unrelated findings; the collision only surfaced as a rebase + * conflict, and resolving it by hand is exactly the manual step DoD §6 + * says to mechanize. Backlog ids are cited by other rows ("Nối tiếp B13", + * "cùng class với B10") and by CHANGELOG entries, so a silent renumber + * breaks references that no test covers. + * + * Per DoD §6 ("cơ giới hóa" — mechanize the fence, don't rely on human recall), + * this script fails CI when either id column contains a duplicate, so the + * collision is caught at PR time instead of during a later cleanup. + * + * It parses ONLY the table under each file's `## Registry` heading, so numbered + * prose above it and notes below it never register as rows. */ const repoRoot = process.cwd(); -const registryPath = path.join( - repoRoot, - '.kiro/specs/admin-setup-wizard/setup-impact.md', -); -const source = await readFile(registryPath, 'utf8'); -const lines = source.split('\n'); +/** @type {{label: string, file: string, row: RegExp, render: (t: string) => string, next: (tokens: string[]) => string}[]} */ +const registries = [ + { + label: 'Setup Impact Registry', + file: '.kiro/specs/admin-setup-wizard/setup-impact.md', + // A data row looks like `| 42 | ... |` or `| 28b | ... |`. The header + // (`| # | ... |`) and separator (`|---|`) rows never match. + row: /^\|\s*(\d+[a-z]?)\s*\|/, + render: (token) => `#${token}`, + next: (tokens) => `#${maxNumeric(tokens) + 1}`, + }, + { + label: 'Out-of-scope backlog', + file: '.kiro/steering/out-of-scope-backlog.md', + // A data row looks like `| B30 | ... |`. The header (`| ID | ... |`) and + // separator rows never match. + row: /^\|\s*B(\d+)\s*\|/, + render: (token) => `B${token}`, + next: (tokens) => `B${maxNumeric(tokens) + 1}`, + }, +]; -// Bound the scan to the "## Registry" section (up to the next H2 heading). -const start = lines.findIndex((l) => /^##\s+Registry\s*$/.test(l)); -if (start === -1) { - console.error(`Registry numbering check failed: -- could not find the "## Registry" heading in ${path.relative(repoRoot, registryPath)}`); - process.exit(1); +/** Highest integer among the collected id tokens. */ +function maxNumeric(tokens) { + const numbers = tokens.map((t) => parseInt(t, 10)).filter((n) => !Number.isNaN(n)); + return numbers.length > 0 ? Math.max(...numbers) : 0; } -let end = lines.findIndex((l, i) => i > start && /^##\s+/.test(l)); -if (end === -1) end = lines.length; -// A registry data row looks like `| 42 | ... |` or `| 28b | ... |`. -// The header (`| # | ... |`) and separator (`|---|`) rows never match. -const ROW = /^\|\s*(\d+[a-z]?)\s*\|/; +const failures = []; +const summaries = []; -const seen = new Map(); // token -> [lineNumbers] -for (let i = start + 1; i < end; i++) { - const m = lines[i].match(ROW); - if (!m) continue; - const token = m[1]; - if (!seen.has(token)) seen.set(token, []); - seen.get(token).push(i + 1); -} +for (const registry of registries) { + const file = path.join(repoRoot, registry.file); + const rel = path.relative(repoRoot, file); -if (seen.size === 0) { - console.error(`Registry numbering check failed: -- parsed 0 rows under "## Registry" — the table shape may have changed`); - process.exit(1); -} + let source; + try { + source = await readFile(file, 'utf8'); + } catch (error) { + failures.push(`- could not read ${rel}: ${error.message}`); + continue; + } + + const lines = source.split('\n'); -const duplicates = [...seen.entries()].filter(([, ls]) => ls.length > 1); + // Bound the scan to the "## Registry" section (up to the next H2 heading). + const start = lines.findIndex((l) => /^##\s+Registry\s*$/.test(l)); + if (start === -1) { + failures.push(`- could not find the "## Registry" heading in ${rel}`); + continue; + } + let end = lines.findIndex((l, i) => i > start && /^##\s+/.test(l)); + if (end === -1) end = lines.length; + + const seen = new Map(); // token -> [lineNumbers] + for (let i = start + 1; i < end; i++) { + const m = lines[i].match(registry.row); + if (!m) continue; + const token = m[1]; + if (!seen.has(token)) seen.set(token, []); + seen.get(token).push(i + 1); + } + + // A table that suddenly parses as empty means the shape changed and this + // guard stopped guarding — fail rather than report a cheerful zero. + if (seen.size === 0) { + failures.push(`- parsed 0 rows under "## Registry" in ${rel} — the table shape may have changed`); + continue; + } -if (duplicates.length > 0) { - console.error('Registry numbering check failed — duplicate row numbers:'); - for (const [token, ls] of duplicates) { - console.error(`- #${token} used on lines ${ls.join(', ')}`); + const duplicates = [...seen.entries()].filter(([, ls]) => ls.length > 1); + if (duplicates.length > 0) { + failures.push(`- ${registry.label} (${rel}) has duplicate row ids:`); + for (const [token, ls] of duplicates) { + failures.push(` ${registry.render(token)} used on lines ${ls.join(', ')}`); + } + failures.push( + ` Give each colliding row a new id starting at ${registry.next([...seen.keys()])}; ` + + `keep the occurrence that other rows cite by id so cross-references stay valid.`, + ); + continue; } - const max = Math.max( - ...[...seen.keys()].map((t) => parseInt(t, 10)).filter((n) => !Number.isNaN(n)), - ); - console.error( - `\nThe # column is a unique row id. Give each colliding row a new number ` + - `greater than the current max (${max}); keep the occurrence that other ` + - `rows cite by number so cross-references stay valid. See DoD §2 and §6.`, - ); + + summaries.push(`${registry.label}: ${seen.size} rows, all ids unique`); +} + +if (failures.length > 0) { + console.error('Registry id check failed:'); + for (const failure of failures) console.error(failure); + console.error('\nSee DoD §2 and §6.'); process.exit(1); } -console.log(`Registry numbering OK: ${seen.size} rows, all # values unique.`); +console.log(`Registry ids OK — ${summaries.join(' · ')}.`);