diff --git a/.gitignore b/.gitignore index 6493e58..8f7d924 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,9 @@ build !src/commands/build scratch/ -# This file is generated by the runreal CLI during the test .runreal +# This file is generated by the runreal CLI during the test +.runreal/dist/script.esm.js +.runreal/dist/hello-world.esm.js .DS_Store docs/ \ No newline at end of file diff --git a/schema.json b/schema.json index 361156e..1b9effb 100644 --- a/schema.json +++ b/schema.json @@ -102,6 +102,10 @@ "type": "string" }, "description": "Command arguments" + }, + "condition": { + "type": "string", + "description": "Condition to execute the step" } }, "required": [ diff --git a/src/commands/workflow/exec.ts b/src/commands/workflow/exec.ts index 46929d3..91881a5 100644 --- a/src/commands/workflow/exec.ts +++ b/src/commands/workflow/exec.ts @@ -49,6 +49,37 @@ async function buildkiteExecutor(steps: { command: string; args: string[] }[]) { } } +function evaluateCondition(condition?: string): boolean { + if (!condition) return true + + try { + // Environment variable checks: ${env('NAME=VALUE')} + const envRegex = /\${env\('([^']+)'\)}/ + const envMatch = condition.match(envRegex) + + if (envMatch) { + const envVar = envMatch[1] + const [name, expectedValue] = envVar.split('=') + const value = Deno.env.get(name) + + if (expectedValue) { + return value === expectedValue + } + + return value === 'true' || value === '1' || value === 'yes' + } + + // Support for direct boolean values or string "true"/"false" + if (condition === 'true') return true + if (condition === 'false') return false + + return false + } catch (error) { + console.log(`[workflow] error evaluating condition "${condition}":`, error) + return false + } +} + export const exec = new Command() .option('-d, --dry-run', 'Dry run') .type('mode', new EnumType(Mode)) @@ -69,27 +100,36 @@ export const exec = new Command() throw new ValidationError(`Workflow ${workflow} not found`) } - const steps: { command: string; args: string[] }[] = [] + const steps: { command: string; args: string[]; condition?: string }[] = [] for await (const step of run.steps) { - const command = render([step.command], cfg)[0] - const args = render(step.args || [], cfg) - steps.push({ command, args }) + const command = render(step.command, cfg) + const args = step.args ? step.args.map((arg) => render(arg, cfg)) : [] + steps.push({ command, args, condition: step.condition }) } if (dryRun) { for (const step of steps) { console.log(`[workflow] exec => ${step.command} ${step.args.join(' ')}`) } - return } // Stop cliffy for exiting the process after running single command cmd.noExit() + // Filter steps based on conditions + const filteredSteps = [] + for (const step of steps) { + if (evaluateCondition(step.condition)) { + filteredSteps.push(step) + } else { + console.log(`[workflow] skipping step due to condition: ${step.command} ${step.args.join(' ')}`) + } + } + if (mode === Mode.Local) { - await localExecutor(steps).catch((e) => Deno.exit(1)) + await localExecutor(filteredSteps).catch((e) => Deno.exit(1)) } else if (mode === Mode.Buildkite) { - await buildkiteExecutor(steps).catch((e) => Deno.exit(1)) + await buildkiteExecutor(filteredSteps).catch((e) => Deno.exit(1)) } }) diff --git a/src/lib/schema.ts b/src/lib/schema.ts index 17d3cd9..5a186de 100644 --- a/src/lib/schema.ts +++ b/src/lib/schema.ts @@ -53,6 +53,7 @@ export const ConfigSchema = z.object({ z.object({ command: z.string().describe('Command to execute'), args: z.array(z.string()).optional().describe('Command arguments'), + condition: z.string().optional().describe('Condition to execute the step'), }), ), }), diff --git a/src/lib/template.ts b/src/lib/template.ts index dea4e12..25e392b 100644 --- a/src/lib/template.ts +++ b/src/lib/template.ts @@ -32,15 +32,30 @@ const placeholderRegex = /\$\{([^}]+)\}/g const pathRegex = /\$path\(([^)]+)\)/g /** - * Replace all ${placeholders} in the items with values from the substitutions object. + * Replace all ${placeholders} in a string with values from the substitutions object. * If a placeholder is not found in the substitutions object, it will be kept as is. - * @param {string[]} input - * @param {RunrealConfig} cfg - * @returns {string[]} the rendered items + * @param {string} input Single string to render + * @param {RunrealConfig} cfg Config to use for substitutions + * @returns {string} The rendered string + */ +export function render(input: string, cfg: RunrealConfig): string +/** + * Replace all ${placeholders} in an array of strings with values from the substitutions object. + * If a placeholder is not found in the substitutions object, it will be kept as is. + * @param {string[]} input Array of strings to render + * @param {RunrealConfig} cfg Config to use for substitutions + * @returns {string[]} The rendered strings */ -export function render(input: string[], cfg: RunrealConfig): string[] { +export function render(input: string[], cfg: RunrealConfig): string[] +export function render(input: string | string[], cfg: RunrealConfig): string | string[] { const substitutions: Record = getSubstitutions(cfg) - return input.map((arg) => subReplace(placeholderRegex, arg, substitutions)) + + if (typeof input === 'string') { + return subReplace(placeholderRegex, input, substitutions) + } + + const rendered = input.map((arg) => subReplace(placeholderRegex, arg, substitutions)) + return rendered } const subReplace = (regex: RegExp, item: string, substitutions: Record) => {