Skip to content
Open
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
138 changes: 138 additions & 0 deletions .github/actions/cli-scaffold-e2e/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
name: CLI scaffold end to end
description: Scaffold a template with the packed CLI, then install and build the generated app.

inputs:
template:
description: Template key passed to `openui create --template`.
required: true
backend-framework:
description: Overlay key passed to `openui create --backend-framework`.
required: false
default: default
node-version:
description: Node.js major version used to run the CLI and the generated app.
required: true
package-manager:
description: Package manager the CLI should select (npm or pnpm).
required: true
package-manager-version:
description: Expected package manager major version.
required: true

runs:
using: composite
steps:
- uses: actions/setup-node@v6
with:
node-version: ${{ inputs.node-version }}

- name: Set up npm ${{ inputs.package-manager-version }}
if: inputs.package-manager == 'npm'
shell: bash
run: npm install --global npm@${{ inputs.package-manager-version }}

- uses: pnpm/action-setup@v6
if: inputs.package-manager == 'pnpm'
with:
version: ${{ inputs.package-manager-version }}

- name: Verify package manager version
shell: bash
env:
PACKAGE_MANAGER: ${{ inputs.package-manager }}
EXPECTED_MAJOR: ${{ inputs.package-manager-version }}
run: |
actual_version="$("${PACKAGE_MANAGER}" --version)"
if [[ "${actual_version}" != "${EXPECTED_MAJOR}".* ]]; then
echo "Expected ${PACKAGE_MANAGER} ${EXPECTED_MAJOR}.x, got ${actual_version}" >&2
exit 1
fi

- uses: actions/download-artifact@v8
with:
name: openui-cli-package
path: ${{ runner.temp }}/cli-package

- name: Locate CLI package and prepare workspace
id: cli-package
shell: bash
env:
CLI_PACKAGE_DIR: ${{ runner.temp }}/cli-package
CLI_E2E_DIR: ${{ runner.temp }}/cli-e2e
run: |
node --input-type=module -e "
import fs from 'node:fs';
import path from 'node:path';
const tarballs = fs.readdirSync(process.env.CLI_PACKAGE_DIR).filter((file) => file.endsWith('.tgz'));
if (tarballs.length !== 1) throw new Error('Expected exactly one CLI tarball');
const tarball = path.join(process.env.CLI_PACKAGE_DIR, tarballs[0]);
fs.mkdirSync(process.env.CLI_E2E_DIR, { recursive: true });
fs.appendFileSync(process.env.GITHUB_OUTPUT, 'path=' + tarball + '\n');
"

# OPENUI_SOURCE_DIR makes the CLI scaffold the checkout's templates instead
# of fetching them from `main`, so template changes are tested here.
- name: Run CLI with npm
if: inputs.package-manager == 'npm'
shell: bash
working-directory: ${{ runner.temp }}/cli-e2e
env:
OPENUI_SOURCE_DIR: ${{ github.workspace }}
run: >-
npm exec --yes --package="${{ steps.cli-package.outputs.path }}" --
openui --no-telemetry create
--name generated-app
--template ${{ inputs.template }}
--backend-framework ${{ inputs.backend-framework }}
--api-key sk-test
--no-interactive
--no-skill

- name: Run CLI with pnpm
if: inputs.package-manager == 'pnpm'
shell: bash
working-directory: ${{ runner.temp }}/cli-e2e
env:
OPENUI_SOURCE_DIR: ${{ github.workspace }}
run: >-
pnpm dlx "${{ steps.cli-package.outputs.path }}"
--no-telemetry create
--name generated-app
--template ${{ inputs.template }}
--backend-framework ${{ inputs.backend-framework }}
--api-key sk-test
--no-interactive
--no-skill

# npm- and pnpm-installed node_modules can both satisfy either run command.
# Check manager metadata and lockfile retention to verify CLI selection.
- name: Verify selected package manager
shell: bash
working-directory: ${{ runner.temp }}/cli-e2e/generated-app
env:
PACKAGE_MANAGER: ${{ inputs.package-manager }}
run: |
node --input-type=module -e "
import fs from 'node:fs';
import path from 'node:path';
const marker = process.env.PACKAGE_MANAGER === 'pnpm'
? path.join('node_modules', '.modules.yaml')
: path.join('node_modules', '.package-lock.json');
if (!fs.existsSync(marker)) throw new Error('Missing ' + process.env.PACKAGE_MANAGER + ' install marker: ' + marker);
const hasNpmLockfile = fs.existsSync('package-lock.json');
if (process.env.PACKAGE_MANAGER === 'npm' && !hasNpmLockfile) {
throw new Error('npm-generated project is missing package-lock.json');
}
if (process.env.PACKAGE_MANAGER !== 'npm' && hasNpmLockfile) {
throw new Error(process.env.PACKAGE_MANAGER + '-generated project retained package-lock.json');
}
"

- name: Build generated app
shell: bash
working-directory: ${{ runner.temp }}/cli-e2e/generated-app
run: ${{ inputs.package-manager }} run build
env:
OPENAI_API_KEY: sk-test
THESYS_API_KEY: sk-test
DEMO_USER_ID: test-user
133 changes: 36 additions & 97 deletions .github/workflows/cli-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ on:
push:
branches: [main]
paths:
- ".github/actions/cli-scaffold-e2e/action.yml"
- ".github/workflows/cli-e2e.yml"
- "packages/openui-cli/**"
- "templates/**"
- "tsconfig.json"
pull_request:
branches: [main]
paths:
- ".github/actions/cli-scaffold-e2e/action.yml"
- ".github/workflows/cli-e2e.yml"
- "packages/openui-cli/**"
- "templates/**"
Expand Down Expand Up @@ -150,105 +152,42 @@ jobs:
steps:
- uses: actions/checkout@v6

- uses: actions/setup-node@v6
- uses: ./.github/actions/cli-scaffold-e2e
with:
template: ${{ matrix.template }}
backend-framework: default
node-version: ${{ matrix.node-version }}
package-manager: ${{ matrix.package-manager }}
package-manager-version: ${{ matrix.package-manager-version }}

# The default overlay is covered above across operating systems and package
# manager majors; overlays only change dependencies and app code, so one
# current Linux configuration per package manager is enough.
cli-e2e-overlays:
name: ${{ matrix.template }} + ${{ matrix.overlay }} (${{ matrix.package-manager }}@11, ubuntu, Node 24)
needs: package-cli
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
template:
- openui-cloud
- openui-self-hosted
overlay:
- langgraph
- vercel-ai-sdk
- vercel-eve
package-manager:
- npm
- pnpm

- name: Set up npm ${{ matrix.package-manager-version }}
if: matrix.package-manager == 'npm'
run: npm install --global npm@${{ matrix.package-manager-version }}

- uses: pnpm/action-setup@v6
if: matrix.package-manager == 'pnpm'
with:
version: ${{ matrix.package-manager-version }}

- name: Verify package manager version
shell: bash
env:
PACKAGE_MANAGER: ${{ matrix.package-manager }}
EXPECTED_MAJOR: ${{ matrix.package-manager-version }}
run: |
actual_version="$("${PACKAGE_MANAGER}" --version)"
if [[ "${actual_version}" != "${EXPECTED_MAJOR}".* ]]; then
echo "Expected ${PACKAGE_MANAGER} ${EXPECTED_MAJOR}.x, got ${actual_version}" >&2
exit 1
fi
steps:
- uses: actions/checkout@v6

- uses: actions/download-artifact@v8
- uses: ./.github/actions/cli-scaffold-e2e
with:
name: openui-cli-package
path: ${{ runner.temp }}/cli-package

- name: Locate CLI package and prepare workspace
id: cli-package
shell: bash
env:
CLI_PACKAGE_DIR: ${{ runner.temp }}/cli-package
CLI_E2E_DIR: ${{ runner.temp }}/cli-e2e
run: |
node --input-type=module -e "
import fs from 'node:fs';
import path from 'node:path';
const tarballs = fs.readdirSync(process.env.CLI_PACKAGE_DIR).filter((file) => file.endsWith('.tgz'));
if (tarballs.length !== 1) throw new Error('Expected exactly one CLI tarball');
const tarball = path.join(process.env.CLI_PACKAGE_DIR, tarballs[0]);
fs.mkdirSync(process.env.CLI_E2E_DIR, { recursive: true });
fs.appendFileSync(process.env.GITHUB_OUTPUT, 'path=' + tarball + '\n');
"

- name: Run CLI with npm
if: matrix.package-manager == 'npm'
working-directory: ${{ runner.temp }}/cli-e2e
run: >-
npm exec --yes --package="${{ steps.cli-package.outputs.path }}" --
openui --no-telemetry create
--name generated-app
--template ${{ matrix.template }}
--api-key sk-test
--no-interactive
--no-skill

- name: Run CLI with pnpm
if: matrix.package-manager == 'pnpm'
working-directory: ${{ runner.temp }}/cli-e2e
run: >-
pnpm dlx "${{ steps.cli-package.outputs.path }}"
--no-telemetry create
--name generated-app
--template ${{ matrix.template }}
--api-key sk-test
--no-interactive
--no-skill

# npm- and pnpm-installed node_modules can both satisfy either run command.
# Check manager metadata and lockfile retention to verify CLI selection.
- name: Verify selected package manager
working-directory: ${{ runner.temp }}/cli-e2e/generated-app
shell: bash
env:
PACKAGE_MANAGER: ${{ matrix.package-manager }}
run: |
node --input-type=module -e "
import fs from 'node:fs';
import path from 'node:path';
const marker = process.env.PACKAGE_MANAGER === 'pnpm'
? path.join('node_modules', '.modules.yaml')
: path.join('node_modules', '.package-lock.json');
if (!fs.existsSync(marker)) throw new Error('Missing ' + process.env.PACKAGE_MANAGER + ' install marker: ' + marker);
const hasNpmLockfile = fs.existsSync('package-lock.json');
if (process.env.PACKAGE_MANAGER === 'npm' && !hasNpmLockfile) {
throw new Error('npm-generated project is missing package-lock.json');
}
if (process.env.PACKAGE_MANAGER !== 'npm' && hasNpmLockfile) {
throw new Error(process.env.PACKAGE_MANAGER + '-generated project retained package-lock.json');
}
"

- name: Build generated app
working-directory: ${{ runner.temp }}/cli-e2e/generated-app
run: ${{ matrix.package-manager }} run build
env:
OPENAI_API_KEY: sk-test
THESYS_API_KEY: sk-test
DEMO_USER_ID: test-user
template: ${{ matrix.template }}
backend-framework: ${{ matrix.overlay }}
node-version: 24
package-manager: ${{ matrix.package-manager }}
package-manager-version: "11"
33 changes: 33 additions & 0 deletions packages/openui-cli/src/lib/checkout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,29 @@ const SOURCE_REPO = "openui";
const SOURCE_REF = "main";
const SOURCE_GIT_URL = `https://github.com/${SOURCE_OWNER}/${SOURCE_REPO}.git`;

/**
* Absolute path to a source-repository checkout to read templates from
* instead of fetching `main` from GitHub, so CI can scaffold the templates of
* the commit under test.
*/
function localSourceDir(): string | undefined {
const dir = process.env["OPENUI_SOURCE_DIR"]?.trim();
return dir || undefined;
}

function localSourcePath(sourceDir: string, normalizedPath: string): string {
const resolved = path.join(sourceDir, ...normalizedPath.split("/"));
if (!fs.existsSync(resolved)) {
throw new CreateError(
"source_checkout",
`Path "${normalizedPath}" was not in OPENUI_SOURCE_DIR (${sourceDir}).`,
"filesystem",
"SOURCE_MISSING",
);
}
return resolved;
}

export type SourceFetchOptions = {
dest?: string;
};
Expand Down Expand Up @@ -98,6 +121,10 @@ function runGit(

export async function fetchSourceFile(repoPath: string): Promise<FetchedFile> {
const normalizedPath = posixRepoPath(repoPath);
const sourceDir = localSourceDir();
if (sourceDir) {
return { content: fs.readFileSync(localSourcePath(sourceDir, normalizedPath), "utf8") };
}
const url = `https://raw.githubusercontent.com/${SOURCE_OWNER}/${SOURCE_REPO}/${SOURCE_REF}/${normalizedPath}`;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
Expand Down Expand Up @@ -134,6 +161,12 @@ export async function checkoutSource(
opts: SourceFetchOptions = {},
): Promise<CheckedOutSource> {
const normalizedPath = posixRepoPath(repoPath);
const sourceDir = localSourceDir();
if (sourceDir) {
const dest = opts.dest ?? fs.mkdtempSync(path.join(os.tmpdir(), "openui-src-"));
copyDir(localSourcePath(sourceDir, normalizedPath), dest);
return { dir: dest };
}
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "openui-src-"));
try {
await runGit(["init", "--quiet"], { cwd: tmpDir });
Expand Down
3 changes: 2 additions & 1 deletion templates/openui-cloud/overlays/vercel-eve/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"allowImportingTsExtensions": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
Expand All @@ -31,5 +32,5 @@
".eve/**/*.d.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
"exclude": ["node_modules", "agent"]
}
Loading