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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -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/
4 changes: 4 additions & 0 deletions schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@
"type": "string"
},
"description": "Command arguments"
},
"condition": {
"type": "string",
"description": "Condition to execute the step"
}
},
"required": [
Expand Down
54 changes: 47 additions & 7 deletions src/commands/workflow/exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<GlobalOptions>()
.option('-d, --dry-run', 'Dry run')
.type('mode', new EnumType(Mode))
Expand All @@ -69,27 +100,36 @@ export const exec = new Command<GlobalOptions>()
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))
}
})
1 change: 1 addition & 0 deletions src/lib/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
}),
),
}),
Expand Down
27 changes: 21 additions & 6 deletions src/lib/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string | undefined> = 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<string, string | undefined>) => {
Expand Down