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
7 changes: 4 additions & 3 deletions deno.jsonc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"tasks": {
"start": "deno run -A --watch=src src/index.ts",
"dev": "deno run -A --watch=src src/index.ts",
"test": "deno test -A --watch",
"run": "deno run -A src/index.ts",
"install": "deno install -A --force --global --name runreal src/index.ts",
"compile-win": "deno compile -A --target x86_64-pc-windows-msvc --output build/runreal-win-x64 src/index.ts",
Expand All @@ -9,15 +10,15 @@
"generate-schema": "deno run -A src/generate-schema.ts"
},
"lint": {
"include": ["src/"],
"include": ["src/", "tests/"],
"rules": {
"tags": ["recommended"],
"include": ["ban-untagged-todo"],
"exclude": ["no-unused-vars", "no-explicit-any"]
}
},
"fmt": {
"include": ["src/"],
"include": ["src/", "tests/"],
"useTabs": true,
"lineWidth": 120,
"indentWidth": 2,
Expand Down
22 changes: 22 additions & 0 deletions src/commands/debug/debug-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Command } from '../../deps.ts'
import { config } from '../../lib/config.ts'
import { CliOptions, GlobalOptions } from '../../lib/types.ts'

export type DebugOptions = typeof debugConfig extends Command<any, any, infer Options, any, any> ? Options
: never

export const debugConfig = new Command<GlobalOptions>()
.option('-r, --render', 'Render the config with substitutions')
.description('debug config')
.action((options) => {
const { render } = options as DebugOptions & GlobalOptions
const cfg = config.get(options as CliOptions)

if (render) {
const rendered = config.renderConfig(cfg)
console.dir(rendered, { depth: null })
return
}

console.dir(cfg, { depth: null })
})
8 changes: 8 additions & 0 deletions src/commands/debug/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Command } from '../../deps.ts'
import { GlobalOptions } from '../../lib/types.ts'

import { debugConfig } from './debug-config.ts'

export const debug = new Command<GlobalOptions>()
.description('debug')
.command('config', debugConfig)
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { VERSION } from './version.ts'

import { debug } from './commands/debug.ts'
import { debug } from './commands/debug/index.ts'
import { build } from './commands/build.ts'
import { engine } from './commands/engine/index.ts'
import { init } from './commands/init.ts'
Expand Down
13 changes: 11 additions & 2 deletions src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { CliOptions, RunrealConfig } from '../lib/types.ts'
import { execSync } from '../lib/utils.ts'
import { ConfigSchema, InternalSchema } from '../lib/schema.ts'
import { Source } from './source.ts'
import { renderConfig } from './template.ts'

const env = (key: string) => Deno.env.get(key) || ''

class Config {
export class Config {
private config: Partial<RunrealConfig> = {
engine: {
path: '',
Expand All @@ -19,7 +20,7 @@ class Config {
},
build: {
path: '',
id: '',
id: env('RUNREAL_BUILD_ID') || '',
branch: '',
branchSafe: '',
commit: '',
Expand All @@ -32,6 +33,9 @@ class Config {
buildCheckoutPath: env('BUILDKITE_BUILD_CHECKOUT_PATH') || Deno.cwd(),
buildPipelineSlug: env('BUILDKITE_PIPELINE_SLUG') || '',
},
metadata: {
test: env('RUNREAL_BUILD_ID') || '',
},
workflows: [],
}
private cliOptionToConfigMap = {
Expand Down Expand Up @@ -66,6 +70,11 @@ class Config {
return this.config as RunrealConfig
}

renderConfig(cfg: RunrealConfig): RunrealConfig {
const rendered = renderConfig(cfg)
return rendered
}

async mergeConfig(configPath: string) {
const cfg = await this.readConfigFile(configPath)
if (!cfg) return
Expand Down
20 changes: 11 additions & 9 deletions src/lib/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ import { z } from '../deps.ts'

export const InternalSchema = z.object({
buildkite: z.object({
branch: z.string().describe('Buildkite branch name'),
checkout: z.string().describe('Buildkite commit hash'),
buildNumber: z.string().describe('Buildkite build number'),
buildCheckoutPath: z.string().describe('Buildkite build checkout path'),
buildPipelineSlug: z.string().describe('Buildkite pipeline slug'),
}),
branch: z.string().describe('Buildkite branch name').optional(),
checkout: z.string().describe('Buildkite commit hash').optional(),
buildNumber: z.string().describe('Buildkite build number').optional(),
buildCheckoutPath: z.string().describe('Buildkite build checkout path').optional(),
buildPipelineSlug: z.string().describe('Buildkite pipeline slug').optional(),
}).optional(),
metadata: z.object({
test: z.string().describe('Build id <RUNREAL_BUILD_ID>'),
}).optional(),
})

export const ConfigSchema = z.object({
Expand Down Expand Up @@ -56,7 +59,6 @@ export const ConfigSchema = z.object({
args: z.array(z.string()).optional().describe('Command arguments'),
}),
),
})
.optional(),
),
}),
).optional(),
})
41 changes: 27 additions & 14 deletions src/lib/source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,33 @@ abstract class Base {
abstract clean(): void

safeRef(): string {
return this.ref().replace(/\/\//g, '/').replace(/\//g, '-')
return this.ref().replace(/\/\//g, '/').replace(/\//g, '-').toLowerCase()
}
}

export class Perforce extends Base {
executable: string = 'p4'
stream(): string {
return execSync(this.executable, [
'-F',
'%Stream%',
'-ztag',
'client',
'-o',
], { cwd: this.cwd, quiet: true }).output.trim()
}
changelist(): string {
return execSync(this.executable, [
'-F',
'%change%',
'changes',
'-m1',
], { cwd: this.cwd, quiet: true }).output.trim().replace('Change ', '')
}
ref(): string {
const stream = execSync(this.executable, ['p4', '-F', '"%Stream%"', '-ztag', 'client', '-o'], {
cwd: this.cwd,
quiet: false,
}).output.trim()
const change = execSync(this.executable, ['-F', '%change%', 'changes', '-m1'], { cwd: this.cwd, quiet: false })
.output.trim().replace('Change ', '')
const parts: string[] = []
const stream = this.stream()
const change = this.changelist()
if (stream) {
parts.push(stream)
}
Expand All @@ -42,11 +55,9 @@ export class Perforce extends Base {
}
return parts.join('/')
}

safeRef(): string {
return this.ref().split('//').filter(Boolean).join('-').replace(/\//g, '-')
return this.ref().split('//').filter(Boolean).join('-').replace(/\//g, '-').toLowerCase()
}

clone({
source,
destination,
Expand Down Expand Up @@ -74,12 +85,15 @@ export class Perforce extends Base {

export class Git extends Base {
executable: string = 'git'
private branch(): string {
return execSync(this.executable, ['branch', '--show-current'], { cwd: this.cwd, quiet: false }).output.trim()
branch(): string {
return execSync(this.executable, ['branch', '--show-current'], { cwd: this.cwd, quiet: true }).output.trim()
}
commit(): string {
return execSync(this.executable, ['rev-parse', 'HEAD'], { cwd: this.cwd, quiet: true }).output.trim()
}
ref(): string {
const branch = this.branch()
const commit = execSync(this.executable, ['rev-parse', 'HEAD'], { cwd: this.cwd, quiet: false }).output.trim()
const commit = this.commit()
const parts: string[] = []
if (branch) {
parts.push(branch)
Expand All @@ -89,7 +103,6 @@ export class Git extends Base {
}
return parts.join('/')
}

clone(opts: CloneOpts): string {
const { source, destination, branch, dryRun } = opts
const cmd = branch ? ['clone', '-b', branch, source, destination] : ['clone', source, destination]
Expand Down
78 changes: 62 additions & 16 deletions src/lib/template.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,10 @@
import { RunrealConfig } from './types.ts'

// This helper function will take a command string with placeholders and a substitutions object
// It will replace all placeholders in the command with their corresponding values
// If the key is not found in substitutions, keep the original placeholder
export function render(input: string[], cfg: RunrealConfig) {
// This regular expression matches all occurrences of ${placeholder}
const substitutions: Record<string, string | undefined> = getSubstitutions(cfg)

const placeholderRegex = /\$\{([^}]+)\}/g
return input.map((arg) =>
arg.replace(placeholderRegex, (_, key: string) => {
return key in substitutions ? substitutions[key] || key : _
})
)
}

// Object containing the allowed substitutions
/**
* Get the substitutions object with values from the config.
* @param {RunrealConfig} cfg
* @returns {Record<string, string | undefined>} the substitutions object
*/
export const getSubstitutions = (cfg: RunrealConfig): Record<string, string | undefined> => ({
'engine.path': cfg.engine?.path,
'project.path': cfg.project?.path,
Expand All @@ -26,3 +15,60 @@ export const getSubstitutions = (cfg: RunrealConfig): Record<string, string | un
'build.commit': cfg.build?.commitShort,
'buildkite.buildNumber': cfg.buildkite?.buildNumber,
})

/**
* Regular expression for matching ${placeholders}.
*/
const placeholderRegex = /\$\{([^}]+)\}/g

/**
* Replace all ${placeholders} in the items 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
*/
export function render(input: string[], cfg: RunrealConfig): string[] {
const substitutions: Record<string, string | undefined> = getSubstitutions(cfg)
return input.map((arg) =>
arg.replace(placeholderRegex, (_, key: string) => {
return key in substitutions ? substitutions[key] || key : _
})
)
}

/**
* Replace all ${placeholders} in the items with values from the substitutions object.
* If a placeholder is not found in the substitutions object, it will be kept as is.
* @param {any} item
* @param {Record<string, string | undefined>} substitutions
* @returns {any} the rendered items
*/
function renderItems(item: any, substitutions: Record<string, string | undefined>): any {
if (typeof item === 'string') {
// Replace placeholders in strings
return item.replace(/\$\{([^}]+)\}/g, (_, key: string) => substitutions[key] || _)
} else if (Array.isArray(item)) {
// Recursively process each item in an array
return item.map((subItem) => renderItems(subItem, substitutions))
} else if (typeof item === 'object' && item !== null) {
// Recursively process each property in an object
const result: Record<string, any> = {}
for (const key of Object.keys(item)) {
result[key] = renderItems(item[key], substitutions)
}
return result
}
// Return the item as is if it's not a string, array, or object
return item
}

/**
* Render the config by replacing all ${placeholders} with values from the substitutions object.
* @param {RunrealConfig} cfg
* @returns {RunrealConfig} rendered RunrealConfig
*/
export function renderConfig(cfg: RunrealConfig) {
const substitutions: Record<string, string | undefined> = getSubstitutions(cfg)
return renderItems(cfg, substitutions) as RunrealConfig
}
14 changes: 0 additions & 14 deletions src/lib/template_test.ts

This file was deleted.

61 changes: 61 additions & 0 deletions tests/config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { assert, assertEquals } from 'https://deno.land/std/assert/mod.ts'
import { Config } from '../src/lib/config.ts'
import { ulid } from '../src/deps.ts'
import { CliOptions } from '../src/lib/types.ts'

Deno.test('Config.create should initialize with default values', async () => {
const config = await Config.create()
const id = ulid()
config.determineBuildId = () => id
const expected = {
engine: {
path: '',
repoType: 'git',
},
project: {
name: '',
path: '',
repoType: 'git',
},
build: {
id,
path: '',
branch: '',
branchSafe: '',
commit: '',
commitShort: '',
},
buildkite: {
branch: '',
checkout: '',
buildNumber: '0',
buildCheckoutPath: Deno.cwd(),
buildPipelineSlug: '',
},
metadata: {
test: '',
},
workflows: [],
}
assertEquals(config.get(), expected)
})

Deno.test('Config.create should load environment variables', async () => {
Deno.env.set('RUNREAL_BUILD_ID', 'test-id')
const config = await Config.create()
assertEquals(config.get().build.id, 'test-id')
Deno.env.delete('RUNREAL_BUILD_ID')
})

Deno.test('Config.get should apply CLI options', async () => {
const config = await Config.create()
const id = ulid()
config.determineBuildId = () => id
const cliOptions: CliOptions = {
enginePath: '/path/to/engine' as any,
projectPath: '/path/to/project' as any,
}
const result = config.get(cliOptions)
assert(result.engine.path.includes('/path/to/engine'))
assert(result.project.path.includes('/path/to/project'))
})
Loading