Skip to content
Merged
24 changes: 13 additions & 11 deletions web/packages/scripts/src/cherry-pick.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { execSync } from 'child_process';
import { execFileSync } from 'child_process';
import * as readline from 'readline';
import * as process from 'process';
import { openBrowser, getBaseUrl } from './git-utils.js';

const git = (...args: string[]) => execFileSync('git', args, { stdio: 'inherit' });

// Helper to prompt the user for input.
function prompt(question: string): Promise<string> {
const rl = readline.createInterface({
Expand All @@ -29,9 +31,9 @@ async function handleMergeConflicts() {
if (response === 'yes') {
try {
console.log('Staging resolved changes...');
execSync('git add .', { stdio: 'inherit' });
git('add', '.');
console.log('Attempting to continue cherry-pick...');
execSync('git cherry-pick --continue', { stdio: 'inherit' });
git('cherry-pick', '--continue');
Comment thread
marcusds marked this conversation as resolved.
console.log('Cherry-pick completed successfully after resolving conflicts.');
resolved = true;
} catch {
Expand All @@ -43,7 +45,7 @@ async function handleMergeConflicts() {
await prompt('Waiting for you to resolve conflicts. Press enter to check again...');
} else if (response === 'abort') {
console.log('Aborting cherry-pick...');
execSync('git cherry-pick --abort', { stdio: 'inherit' });
git('cherry-pick', '--abort');
process.exit(1);
} else {
console.log("Please answer 'yes', 'no', or 'abort'.");
Expand All @@ -63,20 +65,20 @@ async function main() {

try {
console.log('Fetching latest changes from origin...');
execSync('git fetch origin', { stdio: 'inherit' });
git('fetch', 'origin');

console.log(`Checking out the release branch: ${releaseBranch}`);
execSync(`git checkout ${releaseBranch}`, { stdio: 'inherit' });
execSync(`git pull origin ${releaseBranch}`, { stdio: 'inherit' });
git('checkout', releaseBranch);
git('pull', 'origin', releaseBranch);

// Create a new branch based on the release branch.
const newBranchName = `cherry-pick-${commitHash.substring(0, 7)}`;
console.log(`Creating and switching to new branch: ${newBranchName}`);
execSync(`git checkout -b ${newBranchName}`, { stdio: 'inherit' });
git('checkout', '-b', newBranchName);

console.log(`Attempting to cherry-pick commit: ${commitHash}`);
try {
execSync(`git cherry-pick ${commitHash}`, { stdio: 'inherit' });
git('cherry-pick', commitHash);
console.log('Cherry-pick completed successfully without conflicts.');
} catch {
console.error('Merge conflicts detected during cherry-pick!');
Expand All @@ -85,10 +87,10 @@ async function main() {

// Push the new branch to origin.
console.log(`Pushing branch ${newBranchName} to origin...`);
execSync(`git push origin ${newBranchName}`, { stdio: 'inherit' });
git('push', 'origin', newBranchName);

// Retrieve the remote URL to construct the merge request URL.
const remoteUrlRaw = execSync('git remote get-url origin').toString().trim();
const remoteUrlRaw = execFileSync('git', ['remote', 'get-url', 'origin']).toString().trim();
const baseUrl = getBaseUrl(remoteUrlRaw);
const mergeRequestUrl = `${baseUrl}/-/merge_requests/new?merge_request[source_branch]=${newBranchName}&merge_request[target_branch]=${releaseBranch}`;

Expand Down
37 changes: 26 additions & 11 deletions web/packages/scripts/src/git-utils.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,39 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { exec, execSync } from 'child_process';
import { execFile, execFileSync } from 'child_process';
import * as process from 'process';

/**
* Open a URL in the default browser (works on macOS, Windows, and most Linux distros).
* Open an HTTP/HTTPS URL in the default browser (works on macOS, Windows, and most Linux distros).
*/
export function openBrowser(url: string): void {
let command = '';
// Escape the URL to prevent shell interpretation of special characters
const escapedUrl = url.replace(/"/g, '\\"');
let parsed: URL;
try {
parsed = new URL(url);
} catch {
console.error('Refusing to open invalid URL:', url);
return;
}
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
console.error('Refusing to open non-http(s) URL:', parsed.protocol);
return;
}
const safeUrl = parsed.toString();

let cmd: string;
let args: string[];
if (process.platform === 'darwin') {
command = `open "${escapedUrl}"`;
cmd = 'open';
args = [safeUrl];
} else if (process.platform === 'win32') {
command = `start "" "${escapedUrl}"`;
cmd = 'rundll32';
args = ['url.dll,FileProtocolHandler', safeUrl];
} else {
command = `xdg-open "${escapedUrl}"`;
cmd = 'xdg-open';
args = [safeUrl];
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
exec(command, (error) => {
execFile(cmd, args, (error) => {
if (error) {
console.error('Failed to open browser:', error);
}
Expand Down Expand Up @@ -56,7 +71,7 @@ export function getBaseUrl(remoteUrl: string): string {
// Check if git status is clean (no uncommitted changes)
export function isGitStatusClean(): boolean {
try {
const status = execSync('git status --porcelain').toString().trim();
const status = execFileSync('git', ['status', '--porcelain']).toString().trim();
return status === '';
} catch (error) {
console.error('Failed to check git status:', error);
Expand All @@ -67,7 +82,7 @@ export function isGitStatusClean(): boolean {
// Get the current branch name
export function getCurrentBranch(): string {
try {
return execSync('git rev-parse --abbrev-ref HEAD').toString().trim();
return execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD']).toString().trim();
} catch (error) {
console.error('Failed to get current branch:', error);
throw error;
Expand Down
15 changes: 10 additions & 5 deletions web/packages/sdk/orval/format-generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* Runs prettier and eslint fix on generated API files, and prefixes unused parameters with underscores.
*/

import { execSync } from 'child_process';
import { execFileSync } from 'child_process';
import { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
Expand Down Expand Up @@ -475,10 +475,15 @@

// Step 3: Run prettier
console.log('Running prettier...');
execSync(`prettier --write ${generatedPath}`, {
stdio: 'inherit',
cwd: path.join(__dirname, '..'),
});
const isWindows = process.platform === 'win32';
execFileSync(
isWindows ? 'cmd.exe' : 'prettier',
isWindows ? ['/c', 'prettier', '--write', generatedPath] : ['--write', generatedPath],
{
stdio: 'inherit',
cwd: path.join(__dirname, '..'),
}
);
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed

console.log('✅ Successfully processed generated files\n');
} catch (error) {
Expand Down
74 changes: 39 additions & 35 deletions web/packages/sdk/orval/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,15 @@
// SPDX-License-Identifier: Apache-2.0

/**
* This script generates the types for the openapi specs.
*
* For private GitHub raw URLs, set the GITHUB_TOKEN environment variable.
* For local files, no token is required.
* This script generates the types for the openapi specs from local YAML files.
*/
import { execSync } from 'child_process';
import { execFileSync } from 'child_process';
import fs from 'fs';
import os from 'os';
import { serviceConfigs } from './constants';
import path from 'path';
import { generateCustomFetcher } from './generateCustomFetcher';
import { getGithubTokenHeaders } from './githubTokenHeaders';

const githubToken = process.env.GITHUB_TOKEN;
const client = process.env.ORVAL_CLIENT;

const service = process.argv[2] as keyof typeof serviceConfigs;
Expand All @@ -25,21 +20,16 @@ if (!config) {
throw new Error('Unsupported OpenAPI Spec.');
}

const getFile = async () => {
if (config.url.startsWith('http')) {
const remoteUrl = new URL(config.url);
const headers = getGithubTokenHeaders(remoteUrl, githubToken);
const res = await fetch(config.url, headers ? { headers } : undefined);
if (Math.floor(res.status / 100) !== 2) {
throw new Error(`${res.status} - Failed to fetch spec. ${res.statusText}`);
}
return await res.text();
} else {
// Load local file otherwise
const filePath = path.resolve(__dirname, config.url);
const spec = fs.readFileSync(filePath, 'utf8');
return spec;
}
if (config.url.startsWith('http')) {
throw new Error(
`Remote spec URLs are not supported by this script. Got: ${config.url}. ` +
`Vendor the spec locally and reference it by relative path.`
);
}

const getFile = () => {
const filePath = path.resolve(__dirname, config.url);
return fs.readFileSync(filePath, 'utf8');
};

/**
Expand All @@ -49,14 +39,18 @@ const getFile = async () => {
const postProcessZodFiles = (zodPath: string) => {
const zodDefaultFile = path.join(__dirname, '..', zodPath);

if (!fs.existsSync(zodDefaultFile)) {
console.log(`Zod file not found at ${zodDefaultFile}, skipping post-processing`);
return;
let content: string;
try {
content = fs.readFileSync(zodDefaultFile, 'utf8');
} catch (err) {
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
console.log(`Zod file not found at ${zodDefaultFile}, skipping post-processing`);
return;
}
throw err;
}

console.log(`Post-processing Zod file: ${zodDefaultFile}`);

const content = fs.readFileSync(zodDefaultFile, 'utf8');
const lines = content.split('\n');
let fixCount = 0;

Expand Down Expand Up @@ -109,8 +103,8 @@ const postProcessZodFiles = (zodPath: string) => {
const main = async () => {
console.log(`Generating types for: ${service}.`);
const spec = await getFile();
const tempFile = path.join(os.tmpdir(), `openapi-spec-${config.path}.yaml`);
const clientVar = client ? `ORVAL_CLIENT=${client}` : '';
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'openapi-spec-'));
const tempFile = path.join(tempDir, `${config.path}.yaml`);
const target =
client === 'zod'
? `./generated/${config.path}/zod/index.ts`
Expand All @@ -123,19 +117,29 @@ const main = async () => {
}

try {
execSync(
`ORVAL_SERVICE=${service} ORVAL_INPUT=${tempFile} ${clientVar} ORVAL_TARGET=${target} ORVAL_SCHEMAS=./generated/${config.path}/schema pnpm exec orval`,
{
stdio: 'inherit',
}
const orvalEnv: NodeJS.ProcessEnv = {
...process.env,
ORVAL_SERVICE: service,
ORVAL_INPUT: tempFile,
ORVAL_TARGET: target,
ORVAL_SCHEMAS: `./generated/${config.path}/schema`,
};
if (client) {
orvalEnv.ORVAL_CLIENT = client;
}
const isWindows = process.platform === 'win32';
execFileSync(
isWindows ? 'cmd.exe' : 'pnpm',
isWindows ? ['/c', 'pnpm', 'exec', 'orval'] : ['exec', 'orval'],
{ stdio: 'inherit', env: orvalEnv }
);

// Post-process Zod files if generating with zod client
if (client === 'zod') {
postProcessZodFiles(`./generated/${config.path}/zod/default.ts`);
}
} finally {
fs.unlinkSync(tempFile);
fs.rmSync(tempDir, { recursive: true, force: true });
}
};

Expand Down
4 changes: 1 addition & 3 deletions web/packages/studio/e2e-tests/api/customizations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ import {
import { APIRequestContext } from '@playwright/test';

export class CustomizationsAPI {
constructor(private request: APIRequestContext) {
this.request = request;
}
constructor(private request: APIRequestContext) {}

async createCustomizationJob(data: CustomizationJobInput) {
const response = await this.request.post(`${NMP_BASE_URL}/v1/customization/jobs`, {
Expand Down
4 changes: 1 addition & 3 deletions web/packages/studio/e2e-tests/api/datasets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@ interface Dataset {
}

export class DatasetsAPI {
constructor(private request: APIRequestContext) {
this.request = request;
}
constructor(private request: APIRequestContext) {}

private getFileNameFromPath(filePath: string) {
const fileNameParts = filePath.split('/');
Expand Down
4 changes: 1 addition & 3 deletions web/packages/studio/e2e-tests/api/evaluations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ type EvaluationConfig = Record<string, unknown>;
type EvaluationConfigInput = Record<string, unknown>;

export class EvaluationsAPI {
constructor(private request: APIRequestContext) {
this.request = request;
}
constructor(private request: APIRequestContext) {}

async createEvaluationConfig(data: EvaluationConfigInput) {
const response = await this.request.post(`${NMP_BASE_URL}/v1/evaluation/configs`, {
Expand Down
4 changes: 1 addition & 3 deletions web/packages/studio/e2e-tests/api/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ import { CreateModelEntityRequest, ModelEntity } from '@nemo/sdk/generated/platf
import { APIRequestContext } from '@playwright/test';

export class ModelsAPI {
constructor(private request: APIRequestContext) {
this.request = request;
}
constructor(private request: APIRequestContext) {}

async createModel(workspace: string, data: CreateModelEntityRequest) {
const response = await this.request.post(`${NMP_BASE_URL}/v2/workspaces/${workspace}/models`, {
Expand Down
4 changes: 1 addition & 3 deletions web/packages/studio/e2e-tests/api/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ import { ProjectInput, Project, ProjectsPage } from '@nemo/sdk/generated/platfor
import { APIRequestContext } from '@playwright/test';

export class ProjectsAPI {
constructor(private request: APIRequestContext) {
this.request = request;
}
constructor(private request: APIRequestContext) {}

async createProject(workspace: string, data: ProjectInput) {
const response = await this.request.post(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ import { CustomizationJob as CustomizationJobOutput } from '@nemo/sdk/vendored/c
import { type Page } from '@playwright/test';

export class ProjectCustomizationsPage {
constructor(public readonly page: Page) {
this.page = page;
}
constructor(public readonly page: Page) {}

async goto(
projectNamespace: string,
Expand Down
4 changes: 1 addition & 3 deletions web/packages/studio/e2e-tests/pages/project-datasets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ import path from 'path';
/** Dataset shape for e2e page (files_url, name, etc.). */
type Dataset = { files_url?: string; name?: string; [key: string]: unknown };
export class ProjectDatasetsPage {
constructor(public readonly page: Page) {
this.page = page;
}
constructor(public readonly page: Page) {}

private async openQuickActionsMenu(name: string, actionName: string) {
const fileRow = await getRowByName(this.page, name);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ import { waitForLongOperation } from '@e2e-tests/utils/pageUtils';
import { expect, type Page } from '@playwright/test';

export class ProjectEvaluationsPage {
constructor(public readonly page: Page) {
this.page = page;
}
constructor(public readonly page: Page) {}

async gotoEvaluations(projectNamespace: string, projectName: string) {
await this.page.goto(`projects/${projectNamespace}/${projectName}/evaluation/jobs`);
Expand Down
4 changes: 1 addition & 3 deletions web/packages/studio/e2e-tests/pages/project-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ import { getRowByName } from '@e2e-tests/utils/tables';
import { expect, type Page } from '@playwright/test';

export class ProjectModelsPage {
constructor(public readonly page: Page) {
this.page = page;
}
constructor(public readonly page: Page) {}

private async openQuickActionsMenu(modelName: string, actionName: string) {
const modelRow = await getRowByName(this.page, modelName);
Expand Down
Loading
Loading