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
80 changes: 50 additions & 30 deletions web/packages/sdk/orval/format-generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,34 @@
* Runs prettier and eslint fix on generated API files, and prefixes unused parameters with underscores.
*/

import { execSync } from 'child_process';
import { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'fs';
import { mkdirSync, readdirSync, readFileSync, writeFileSync, type Dirent } from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import prettier from 'prettier';
import { serviceConfigs } from './constants';

// Get __dirname equivalent for ES modules
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// Get the service path from command line args
const servicePath = process.argv[2];
const ALLOWED_SERVICE_PATHS: ReadonlySet<string> = new Set(
Object.values(serviceConfigs).map((c) => c.path)
);
const rawServicePath = process.argv[2];

if (!servicePath) {
if (!rawServicePath) {
console.error('Error: Service path is required');
console.error('Usage: node format-generated.js <service-path>');
process.exit(1);
}

if (!ALLOWED_SERVICE_PATHS.has(rawServicePath)) {
console.error(`Error: Unknown service path: ${rawServicePath}`);
console.error(`Allowed: ${[...ALLOWED_SERVICE_PATHS].join(', ')}`);
process.exit(1);
}

const servicePath = rawServicePath;
const generatedPath = path.join(__dirname, '..', 'generated', servicePath);

console.log(`\n📝 Processing generated files in ${generatedPath}...`);
Expand All @@ -36,15 +46,14 @@ function getTsFiles(dir: string): string[] {
const files: string[] = [];

try {
const entries = readdirSync(dir);
const entries = readdirSync(dir, { withFileTypes: true });

for (const entry of entries) {
const fullPath = path.join(dir, entry);
const stat = statSync(fullPath);
const fullPath = path.join(dir, entry.name);

if (stat.isDirectory()) {
if (entry.isDirectory()) {
files.push(...getTsFiles(fullPath));
} else if (entry.endsWith('.ts')) {
} else if (entry.isFile() && entry.name.endsWith('.ts')) {
files.push(fullPath);
}
}
Expand Down Expand Up @@ -435,53 +444,64 @@ function splitZodTagFile(filePath: string): number {
}

function splitZodTagFilesIn(zodDir: string): void {
let entries: string[];
let entries: Dirent[];
try {
entries = readdirSync(zodDir);
entries = readdirSync(zodDir, { withFileTypes: true });
} catch {
return;
}
for (const entry of entries) {
const fullPath = path.join(zodDir, entry);
if (!entry.endsWith('.ts')) continue;
if (!statSync(fullPath).isFile()) continue;
if (!entry.isFile() || !entry.name.endsWith('.ts')) continue;
const fullPath = path.join(zodDir, entry.name);
const count = splitZodTagFile(fullPath);
if (count > 0) {
console.log(` Split ${entry} into ${count} operation files`);
console.log(` Split ${entry.name} into ${count} operation files`);
}
}
}

try {
// Step 1: Prefix unused parameters with underscore
async function formatWithPrettier(dir: string): Promise<void> {
const entries = readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
await formatWithPrettier(fullPath);
continue;
}
if (!entry.isFile()) continue;
const fileInfo = await prettier.getFileInfo(fullPath);
if (fileInfo.ignored || !fileInfo.inferredParser) continue;
const opts = (await prettier.resolveConfig(fullPath)) ?? {};
const source = readFileSync(fullPath, 'utf-8');
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
const formatted = await prettier.format(source, { ...opts, filepath: fullPath });
if (formatted !== source) {
writeFileSync(fullPath, formatted, 'utf-8');
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
}
}
}

async function run(): Promise<void> {
console.log('Prefixing unused parameters...');
const tsFiles = getTsFiles(generatedPath);
let modifiedCount = 0;

for (const file of tsFiles) {
if (prefixUnusedParameters(file)) {
modifiedCount++;
}
}

console.log(` Modified ${modifiedCount} file(s)`);

// Step 2: Split large orval-generated zod tag files into per-operation files.
// Each tag file (e.g. zod/evaluator.ts) gets replaced with a barrel and a
// sibling directory of per-operation files (zod/evaluator/<op>.ts).
const zodDir = path.join(generatedPath, 'zod');
console.log('Splitting zod tag files by operation...');
splitZodTagFilesIn(zodDir);

// Step 3: Run prettier
console.log('Running prettier...');
execSync(`prettier --write ${generatedPath}`, {
stdio: 'inherit',
cwd: path.join(__dirname, '..'),
});
await formatWithPrettier(generatedPath);

console.log('✅ Successfully processed generated files\n');
} catch (error) {
}

run().catch((error) => {
console.error('❌ Error during processing:', (error as Error).message);
process.exit(1);
}
});
Loading
Loading