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
19 changes: 19 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,12 @@ jobs:
- name: Build engine package
if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' || needs.changes.outputs.engine == 'true' || needs.changes.outputs.ui == 'true' }}
run: npx turbo run build --filter=@loopover/engine
# Mirrors "MCP package check"/"Miner package check" below: the published npm tarball is a
# different surface than the workspace build above (files field, forbidden paths/content,
# stale README wording) and needs its own dry-run validation (#8591).
- name: Engine package check
if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' || needs.changes.outputs.engine == 'true' || needs.changes.outputs.ui == 'true' }}
run: npm run test:engine-pack
# .tsbuildinfo mutates every run (tsc's own incremental state), unlike node_modules above which is
# immutable per lockfile -- so this needs the run_id-suffixed-key + restore-keys-prefix pattern (always
# creates a new cache entry to save into, restore falls back to the most recent matching prefix) rather
Expand Down Expand Up @@ -733,6 +739,11 @@ jobs:
- name: Build UI-kit package
if: ${{ !cancelled() && (github.event_name == 'push' || needs.changes.outputs.ui == 'true') }}
run: npx turbo run build --filter=@loopover/ui-kit
# Mirrors "MCP package check"/"Miner package check" above: the published npm tarball is a
# different surface than the workspace build above and needs its own dry-run validation (#8591).
- name: UI-kit package check
if: ${{ !cancelled() && (github.event_name == 'push' || needs.changes.outputs.ui == 'true') }}
run: npm run test:ui-kit-pack
# apps/loopover-ui's fumadocs-mdx virtual modules (collections/browser, collections/server, imported by
# docs-client-loader.tsx/docs-source.ts) are gitignored generated output, normally produced by npm ci's
# own `postinstall` hook -- but "Install dependencies" above is SKIPPED on a node_modules cache hit (the
Expand Down Expand Up @@ -943,6 +954,14 @@ jobs:
exit 1
fi
done
# This job runs the general test/unit/*.test.ts suite unconditionally whenever it runs at all
# (unscoped on push, and SCOPED_TEST_SELECTION below only excludes mcp-cli-*/miner-* prefixed
# files, never this one) -- so test/unit/check-ui-kit-package.test.ts's own real-package
# regression guard needs packages/loopover-ui-kit/dist/ to exist here too, the same gap "Build
# engine package"/"Build MCP"/"Build miner CLI" above already close for their own pack-checks
# (#8592 confirmed live: this job has no ui-kit build at all, unlike validate-code).
- name: Build UI-kit package
run: npx turbo run build --filter=@loopover/ui-kit
- name: Save Turborepo cache
if: ${{ !cancelled() }}
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
Expand Down
81 changes: 81 additions & 0 deletions .github/workflows/package-release-watch.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
name: Package Release Watch

# Same "release due" tracking-issue safety net mcp-release-watch.yml already gives MCP, extended to
# engine/miner/ui-kit (#8591): if release-please ever silently stops proposing a release PR for one of
# these, nothing else would flag it. Deliberately ONE job iterating the three packages sequentially
# (not a matrix) -- the shared Actions runner pool is org-wide, and #8574's investigation found extra
# concurrent runners per scheduled run a direct contributor to queue saturation; this trades a few
# extra seconds of wall-clock for zero additional concurrent runner load.

on:
workflow_dispatch:
schedule:
- cron: "35 16 * * *"

permissions:
contents: read
issues: write

concurrency:
group: package-release-watch-${{ github.ref }}
cancel-in-progress: false

jobs:
release-watch:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
fetch-depth: 0
persist-credentials: false

- name: Setup workspace
uses: ./.github/actions/setup-workspace

# Each package's check runs independently of the others' outcome (`!cancelled()`, not the
# default "skip on any earlier failure") -- one package's transient failure (e.g. a flaky
# `npm view` registry read) must never hide whether the other two are due for a release.
- name: Check engine release status
if: ${{ !cancelled() }}
env:
GITHUB_TOKEN: ${{ github.token }}
run: npx tsx scripts/check-package-release-due.ts --package engine --json --output engine-release-due.json --upsert-issue

- name: Check miner release status
if: ${{ !cancelled() }}
env:
GITHUB_TOKEN: ${{ github.token }}
run: npx tsx scripts/check-package-release-due.ts --package miner --json --output miner-release-due.json --upsert-issue

- name: Check ui-kit release status
if: ${{ !cancelled() }}
env:
GITHUB_TOKEN: ${{ github.token }}
run: npx tsx scripts/check-package-release-due.ts --package ui-kit --json --output ui-kit-release-due.json --upsert-issue

- name: Summarize package release status
if: ${{ !cancelled() }}
run: |
node <<'NODE'
const fs = require("node:fs");
const lines = ["## Package Release Watch"];
for (const [label, file] of [["Engine", "engine-release-due.json"], ["Miner", "miner-release-due.json"], ["ui-kit", "ui-kit-release-due.json"]]) {
if (!fs.existsSync(file)) {
lines.push("", `### ${label}`, "- Check did not complete (see the step above).");
continue;
}
const report = JSON.parse(fs.readFileSync(file, "utf8"));
lines.push(
"",
`### ${label}`,
`- Due: \`${report.due}\``,
`- Proposed version: \`${report.proposedVersion}\``,
`- Latest tag: \`${report.latestTag ?? "none"}\``,
`- npm latest: \`${report.publishedVersion ?? "unknown"}\``,
`- Related commits: \`${report.commits.length}\``,
);
}
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, `${lines.join("\n")}\n`);
NODE
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
"build:miner": "turbo run build --filter=@loopover/engine && npm --workspace @loopover/miner run build",
"test:mcp-pack": "tsx scripts/check-mcp-package.ts",
"test:miner-pack": "tsx scripts/check-miner-package.ts",
"test:engine-pack": "tsx scripts/check-engine-package.ts",
"test:ui-kit-pack": "tsx scripts/check-ui-kit-package.ts",
"test:miner-deployment-docs-audit": "tsx scripts/check-miner-deployment-docs.ts",
"rees:install": "npm ci --prefix review-enrichment --prefer-offline --no-audit --no-fund",
"rees:test": "npm run rees:install && npm --prefix review-enrichment test",
Expand Down Expand Up @@ -112,7 +114,7 @@
"test:smoke:browser:install": "playwright install chromium",
"test:smoke:browser": "node --experimental-strip-types scripts/smoke-ui-browser.ts",
"pretest:ci": "npm run check-node-version",
"test:ci": "git diff --check && npm run actionlint && npm run lint:composite-actions && npm run db:migrations:check && npm run db:schema-drift:check && npm run selfhost:env-reference:check && npm run miner:env-reference:check && npm run selfhost:validate-observability && npm run cf-typegen:check && npm run build --workspace @loopover/engine && npm run build --workspace @loopover/discovery-index && npm run build:mcp && npm run build:miner && npm run typecheck && npm run test:coverage && npm run test:engine-parity && npm run test:live-gate-parity && npm run test:driver-parity && npm run test --workspace @loopover/engine && npm run test:workers && npm run test:mcp-pack && npm run test:miner-pack && npm run test:miner-deployment-docs-audit && npm run rees:test && npm run ui:openapi:check && npm run ui:openapi:settings-parity && npm run ui:version-audit && npm run docs:drift-check && npm run coverage-boltons:check && npm run branding-drift:check && npm run manifest:drift-check && npm run engine-parity:drift-check && npm run engines-nvmrc:check && npm run release-manifest:sync:check && npm run command-reference:check && npm run ui:lint && npm run ui:typecheck && npm run ui:test && npm run ui:build",
"test:ci": "git diff --check && npm run actionlint && npm run lint:composite-actions && npm run db:migrations:check && npm run db:schema-drift:check && npm run selfhost:env-reference:check && npm run miner:env-reference:check && npm run selfhost:validate-observability && npm run cf-typegen:check && npm run build --workspace @loopover/engine && npm run build --workspace @loopover/discovery-index && npm run build:mcp && npm run build:miner && npm run typecheck && npm run test:coverage && npm run test:engine-parity && npm run test:live-gate-parity && npm run test:driver-parity && npm run test --workspace @loopover/engine && npm run test:workers && npm run test:mcp-pack && npm run test:miner-pack && npm run test:engine-pack && npm run test:ui-kit-pack && npm run test:miner-deployment-docs-audit && npm run rees:test && npm run ui:openapi:check && npm run ui:openapi:settings-parity && npm run ui:version-audit && npm run docs:drift-check && npm run coverage-boltons:check && npm run branding-drift:check && npm run manifest:drift-check && npm run engine-parity:drift-check && npm run engines-nvmrc:check && npm run release-manifest:sync:check && npm run command-reference:check && npm run ui:lint && npm run ui:typecheck && npm run ui:test && npm run ui:build",
"test:release": "npm run test:ci && npm run changelog:check",
"test:release:mcp": "npm run test:ci",
"test:watch": "vitest",
Expand Down
58 changes: 58 additions & 0 deletions packages/loopover-ui-kit/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# @loopover/ui-kit

Shared design-system tokens and component primitives for `apps/loopover-ui` and `apps/loopover-miner-ui`.

This package houses the Radix-based component library both apps build on, so the same design tokens,
component behavior, and accessibility conventions stay identical across the two frontends instead of
drifting apart. It is versioned independently and published to npm as `@loopover/ui-kit`.

## Install

```
npm install @loopover/ui-kit
```

`react` and `react-dom` (`^19.2.7`) are peer dependencies — the consuming app supplies its own copy.

## Usage

Each component is its own subpath export, so a consumer only pulls in the components it actually
imports rather than the whole library:

```ts
import { Button } from "@loopover/ui-kit/components/button";
import { useIsMobile } from "@loopover/ui-kit/hooks/use-mobile";
import { cn } from "@loopover/ui-kit/utils";
import "@loopover/ui-kit/theme.css";
```

`theme.css` ships as source (not compiled) so a consumer's own Tailwind/PostCSS pipeline processes it
alongside the rest of the app's styles.

## Components

`accordion`, `alert`, `alert-dialog`, `aspect-ratio`, `avatar`, `badge`, `breadcrumb`, `button`,
`calendar`, `card`, `carousel`, `chart`, `checkbox`, `collapsible`, `command`, `context-menu`, `dialog`,
`drawer`, `dropdown-menu`, `form`, `hover-card`, `input`, `input-otp`, `label`, `menubar`,
`navigation-menu`, `pagination`, `popover`, `progress`, `radio-group`, `resizable`, `scroll-area`,
`select`, `separator`, `sheet`, `sidebar`, `skeleton`, `slider`, `sonner`, `state-views`, `switch`,
`table`, `tabs`, `textarea`, `toggle`, `toggle-group`, `tooltip`.

Plus one hook (`hooks/use-mobile`) and shared utilities (`utils`, notably `cn` for Tailwind class merging).

## Build

```
npm run build --workspace @loopover/ui-kit
```

Runs `tsc -p tsconfig.json`, emitting `dist/` (the only published output alongside `CHANGELOG.md` and
`src/theme.css`).

## Test

```
npm test --workspace @loopover/ui-kit
```

Runs the package's own `vitest` suite (jsdom) against the component source.
3 changes: 2 additions & 1 deletion packages/loopover-ui-kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
},
"files": [
"dist",
"src/theme.css"
"src/theme.css",
"CHANGELOG.md"
],
"scripts": {
"build": "tsc -p tsconfig.json",
Expand Down
74 changes: 74 additions & 0 deletions scripts/check-engine-package.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/usr/bin/env node
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { FORBIDDEN_CONTENT } from "./forbidden-content.js";

// Unlike MCP/miner's small, hand-curated lib/*.js lists, engine's dist/ mirrors its src/ tree 1:1
// (tsc's default output shape) and grows every time a follow-up issue extracts more logic from the
// app into this package (see the package's own README) -- an enumerated per-file allowlist would need
// editing on every such PR and would silently rot. Pattern-match the dist/ shape itself instead; the
// forbidden-path/forbidden-content/stale-text checks below still catch anything that doesn't belong.
const ALLOWED = [/^dist\/.*\.(js|d\.ts)$/, /^package\.json$/, /^README\.md$/, /^CHANGELOG\.md$/, /^LICENSE$/];
const REQUIRED = ["package.json", "README.md", "CHANGELOG.md", "dist/index.js", "dist/index.d.ts"];
const FORBIDDEN_PATH = /(^|\/)(\.dev\.vars|\.env|\.npmrc|.*\.pem|.*private.*key.*|.*secret.*)$/i;
const STALE_PACKAGE_TEXT = /(private beta|zeronode\.workers\.dev|preview URL)/i;

type PackedFile = string | { path: string };
type ReadContentFn = (file: string) => string;

export function validateEnginePackFileList(files: readonly PackedFile[], readContent: ReadContentFn): string[] {
const paths = files.map((file) => (typeof file === "string" ? file : file.path)).sort();
for (const file of paths) {
if (FORBIDDEN_PATH.test(file)) throw new Error(`Forbidden file in engine package: ${file}`);
if (!ALLOWED.some((pattern) => pattern.test(file))) throw new Error(`Unexpected file in engine package: ${file}`);
const content = readContent(file);
if (FORBIDDEN_CONTENT.test(content)) throw new Error(`Secret-like content found in engine package file: ${file}`);
if (file === "README.md" && STALE_PACKAGE_TEXT.test(content)) throw new Error(`Stale public-package wording found in engine package file: ${file}`);
}
for (const required of REQUIRED) {
if (!paths.includes(required)) throw new Error(`Engine package is missing required file: ${required}`);
}
return paths;
}

export function runEnginePackCheck(options: { pack?: { files: PackedFile[] }; packageRoot?: string; readContent?: ReadContentFn } = {}): string {
const pack = options.pack ?? loadEnginePackFromNpm();
const packageRoot = options.packageRoot ?? join(process.cwd(), "packages/loopover-engine");
const readContent: ReadContentFn =
options.readContent ??
((file) => {
if (process.env.CHECK_ENGINE_PACK_TEST_CONTENT !== undefined) return process.env.CHECK_ENGINE_PACK_TEST_CONTENT;
return readFileSync(join(packageRoot, file), "utf8");
});
const paths = validateEnginePackFileList(pack.files, readContent);
return `Engine package dry-run ok: ${paths.length} files.\n`;
}

function loadEnginePackFromNpm(): { files: PackedFile[] } {
if (process.env.CHECK_ENGINE_PACK_TEST_FILES) {
const paths: string[] = JSON.parse(process.env.CHECK_ENGINE_PACK_TEST_FILES);
return { files: paths.map((path) => ({ path })) };
}
const result = spawnSync("npm", ["pack", "--workspace", "@loopover/engine", "--dry-run", "--json"], {
encoding: "utf8",
maxBuffer: 1024 * 1024 * 16,
});
if (result.status !== 0) {
const message = result.stderr || result.stdout || "npm pack failed";
throw new Error(message.trim());
}
return JSON.parse(result.stdout)[0];
}

function main() {
try {
process.stdout.write(runEnginePackCheck());
} catch (err) {
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
process.exit(1);
}
}

if (process.argv[1] === fileURLToPath(import.meta.url)) main();
Loading
Loading