diff --git a/content/docs/guide/ci-cd-pipeline.md b/content/docs/guide/ci-cd-pipeline.md index 7c1f94fa4..37d8dbb36 100644 --- a/content/docs/guide/ci-cd-pipeline.md +++ b/content/docs/guide/ci-cd-pipeline.md @@ -27,6 +27,7 @@ one has its own section below. | `lint.yml` | Lint | Push / PR to `main`, `develop`; manual | **Yes** — ESLint **errors** only | | `changeset-guard.yml` | Changeset Bump Policy | PR / push touching `.changeset/**` | **Yes** | | `performance-budget.yml` | Bundle Analysis | Push / PR touching `packages/**`, `apps/console/**`, `pnpm-lock.yaml` | **Yes** — the console entry gzip budget | +| `live-e2e.yml` | Live E2E (informational) | PR to `main`, `develop` (code paths); nightly cron `30 6 * * *`; manual | No — informational lane, `continue-on-error` | | `labeler.yml` | Auto Label PRs | PR `opened`, `synchronize`, `reopened` | No | | `dependabot-auto-merge.yml` | Dependabot Auto-merge | PR to `main`/`develop` authored by `dependabot[bot]` | No | | `cross-repo-issue-closer.yml` | Cross-repo Issue Closer | PR `closed` (acts only when merged) | No — runs after merge | @@ -144,6 +145,25 @@ reviewers; exceeding any of them turns no check red and blocks no merge: > these tiers enforced, add the comparison to the workflow — do not describe it as > enforced here. +## Live E2E (`live-e2e.yml`) + +**Trigger:** PRs to `main` / `develop` (same code-path filter as `ci.yml` — docs-only and +changeset-only PRs skip it), a nightly cron (`30 6 * * *`) on `main`, and manual dispatch. + +**Blocks a merge: no.** The job runs with `continue-on-error: true` by construction — a red run +is informational and never ejects a PR from the merge queue. Do not add it to required checks +(and do not remove `continue-on-error`) until the nightly record proves the lane stable; see the +header comment in the workflow file (#2835). + +What it does: runs the allowlisted live specs (`pnpm test:e2e:live:ci` — screen-flow, +action-modal, master-detail) against a real `objectstack dev` backend booted from **published** +`@objectstack/*` packages serving the showcase app, catching the class of bug only a real +browser against a real backend can see. Failures still surface as a red step plus an uploaded +Playwright report and job summary. + +Backend pins live in `e2e/live/ci/backend.env` and must match the `@objectstack/spec` version in +`pnpm-lock.yaml` — bump both in the same PR, or the run proves nothing. + ## Link Checking (`check-links.yml`) **Trigger:** Manual workflow dispatch (`workflow_dispatch`). diff --git a/packages/core/src/utils/dataset-format.ts b/packages/core/src/utils/dataset-format.ts index 9c043cf5b..7b8bc4d2b 100644 --- a/packages/core/src/utils/dataset-format.ts +++ b/packages/core/src/utils/dataset-format.ts @@ -17,6 +17,8 @@ * by both `@object-ui/plugin-dashboard` and `@object-ui/plugin-report`. */ +import type { PercentScale } from '@objectstack/spec/data'; + /** * Column metadata the analytics server returns alongside the rows: a display * `label` for both dimensions and measures, plus a measure's numeral `format` @@ -41,8 +43,12 @@ export interface DatasetResultField { * field's scale) and carried on the result column, so display never has to * infer it from the value's magnitude — the inference that printed a ratio of * exactly 1 as "1.0%" (#3136). + * + * Spec-owned since 17.0.0-rc.2 (`@objectstack/spec/data` exports the identical + * union); re-exported here so existing `@object-ui/core` consumers keep their + * import path. */ -export type PercentScale = 'fraction' | 'whole'; +export type { PercentScale } from '@objectstack/spec/data'; /** * Scale a stored `percent`-field value to its DISPLAY magnitude. diff --git a/packages/types/src/__tests__/bulk-action-spec-parity.test.ts b/packages/types/src/__tests__/bulk-action-spec-parity.test.ts new file mode 100644 index 000000000..86d1ac1c8 --- /dev/null +++ b/packages/types/src/__tests__/bulk-action-spec-parity.test.ts @@ -0,0 +1,86 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * BulkAction* ↔ `@objectstack/spec` drift guard (objectui#3334). + * + * Spec 17.0.0-rc.2 absorbed the bulk-action vocabulary (`BulkActionDefSchema` + * et al. on `@objectstack/spec/ui`). `BulkActionOperation` is identical and is + * imported from the spec at its declaration in `objectql.ts`; the interfaces + * `BulkActionParam` / `BulkActionDef` stay objectui-side dialects (ALLOW + * entries in scripts/check-spec-symbol-derivation.mjs) because they type what + * the renderer EXECUTES, not what an author may write: + * + * - a def promoted at runtime from `bulkActions: ['']` carries the + * source object action as `actionDef` (objectui#3002/#3139) — a resolution + * artifact the spec's strict schema rejects by design; + * - a promoted def's params come from the object action, so the renderer's + * param `type` is an open string with a widget-config catch-all, while the + * spec closes `type` over the authored FieldWidget enum. + * + * Each divergence is pinned here: if the spec starts ACCEPTING `actionDef`, + * or the two shapes converge some other way, a pin fails and the dialect must + * be reconciled (derive/re-export) instead of silently drifting. + */ + +import { describe, it, expect } from 'vitest'; +import { + BulkActionDefSchema as SpecBulkActionDefSchema, + BulkActionParamSchema as SpecBulkActionParamSchema, + BulkActionOperationSchema as SpecBulkActionOperationSchema, +} from '@objectstack/spec/ui'; +import type { BulkActionDef, BulkActionOperation } from '../objectql'; + +const minimalDef = { name: 'close_won', operation: 'update' as const }; + +describe('BulkAction* ↔ spec parity (objectui#3334)', () => { + it('BulkActionOperation is the spec union, verbatim', () => { + // Compile-time: the re-exported type accepts exactly the spec's members … + const ops: BulkActionOperation[] = ['update', 'delete', 'custom']; + // … and runtime: the spec enum still holds exactly those members. + expect([...SpecBulkActionOperationSchema.options].sort()).toEqual( + [...ops].sort(), + ); + }); + + it('spec def schema still REJECTS the runtime `actionDef` artifact', () => { + // The reason the local dialect exists. If the spec starts accepting + // `actionDef`, the shapes may have converged — reconcile, don't shadow. + const res = SpecBulkActionDefSchema.safeParse({ + ...minimalDef, + actionDef: { name: 'approve', type: 'script' }, + }); + expect(res.success).toBe(false); + + // Sanity: the strictness is specific to the unknown key, not the base def. + expect(SpecBulkActionDefSchema.safeParse(minimalDef).success).toBe(true); + + // The local dialect carries it (compile-time pin). + const local: BulkActionDef = { + ...minimalDef, + actionDef: { name: 'approve', type: 'script' }, + }; + expect(local.actionDef).toBeDefined(); + }); + + it('spec param `type` stays a closed enum the renderer deliberately widens', () => { + // A runtime-promoted param may carry an action-declared widget name the + // authored enum does not model; the local open `type: string` accepts it. + const res = SpecBulkActionParamSchema.safeParse({ + name: 'assignee', + type: 'not_an_authored_widget', + }); + expect(res.success).toBe(false); + + // Shared vocabulary sanity: an authored widget type parses on both sides. + expect( + SpecBulkActionParamSchema.safeParse({ name: 'assignee', type: 'lookup' }) + .success, + ).toBe(true); + }); +}); diff --git a/packages/types/src/objectql.ts b/packages/types/src/objectql.ts index 03d4ba4aa..f17ced57f 100644 --- a/packages/types/src/objectql.ts +++ b/packages/types/src/objectql.ts @@ -19,6 +19,7 @@ */ import type { BaseSchema } from './base'; +import type { BulkActionOperation } from '@objectstack/spec/ui'; import type { FormField } from './form'; // ListView type is now derived from the zod schema (issue #2231) — see ListViewSchema below. import type { ListViewInferred } from './zod/objectql.zod.js'; @@ -309,8 +310,13 @@ export interface BulkActionParam { * executor dispatches that action through the action runner: once per record by * default, or once for the whole selection when the def opts into * {@link BulkActionDef.execution} `'aggregate'` (objectui#3139). + * + * Spec-owned since 17.0.0-rc.2 (`@objectstack/spec/ui` exports the identical + * `'update' | 'delete' | 'custom'` union); re-exported so consumers keep this + * import path. (Imported at the top of this module — BulkActionDef below + * references it.) */ -export type BulkActionOperation = 'update' | 'delete' | 'custom'; +export type { BulkActionOperation }; /** * Rich, schema-driven definition of a bulk action. diff --git a/scripts/check-spec-symbol-derivation.mjs b/scripts/check-spec-symbol-derivation.mjs index a95d84b25..dec1a3535 100644 --- a/scripts/check-spec-symbol-derivation.mjs +++ b/scripts/check-spec-symbol-derivation.mjs @@ -126,6 +126,31 @@ const ALLOW = { "the sanctioned set.", issue: 4115, }, + "@object-ui/types:BulkActionParam": { + reason: + "Renderer-side dialect of the spec's authored param (objectui#3334). The spec's " + + "BulkActionParamSchema closes `type` over the FieldWidget enum — right for authored " + + "view metadata that `objectstack build` validates. This interface types what the " + + "BulkActionDialog RENDERS, which also includes defs promoted at runtime from object " + + "actions (`resolveBulkActions`, objectui#3002) whose param types are whatever the " + + "action declared — so `type` stays an open string and a catch-all index signature " + + "forwards widget-specific config (min/max/step/…) the way the field renderers expect. " + + "Divergence pinned by packages/types/src/__tests__/bulk-action-spec-parity.test.ts.", + issue: 3334, + }, + "@object-ui/types:BulkActionDef": { + reason: + "Renderer-side dialect (objectui#3334): carries `actionDef` — the source object " + + "ActionDef attached at runtime when a `bulkActions: ['']` entry is promoted " + + "(objectui#3002/#3139) — which the spec's STRICT BulkActionDefSchema rejects by design " + + "(it validates authored view metadata, and `actionDef` is a resolution artifact that " + + "must never be authored). `visible` also stays on the pre-normalization " + + "`string | { dialect?, source }` wire shape the action bridge forwards. Divergences " + + "pinned by packages/types/src/__tests__/bulk-action-spec-parity.test.ts; " + + "`BulkActionOperation` and the param/def spec keys are shared, and the operation union " + + "is imported from the spec at its declaration in objectql.ts.", + issue: 3334, + }, "@object-ui/auth:AuthProvider": { reason: "A REACT CONTEXT PROVIDER COMPONENT, not a type — `` is the " +